Docker 이미지(Image)와 컨테이너(Container)

Series: Docker

Dockercontains 3

들어가며

Docker를 처음 공부할 때 가장 자주 헷갈리는 개념이 이미지와 컨테이너이다. Docker 명령어를 사용하다 보면 image, container, run, build, pull 같은 단어들이 계속 등장하는데, 처음에는 이 개념들이 머릿속에서 명확하게 분리되지 않는다.

특히 '이미지는 설계도이고, 컨테이너는 실행된 결과다'라는 설명을 많이 보게 된다. 이 비유가 완전히 틀린 것은 아니다. 처음 개념을 잡을 때는 꽤 도움이 된다. 하지만 여기서 멈추면 Docker가 실제로 어떤 방식으로 동작하는지 이해하기 어렵다.

이미지는 단순한 설계도가 아니고, 컨테이너는 단순한 실행 결과도 아니다. 이미지는 애플리케이션을 실행하기 위해 필요한 파일 시스템과 설정을 담고 있는 정적인 실행 단위이고, 컨테이너는 그 이미지를 기반으로 실제로 실행된 프로세스이다.

이 글에서는 이미지와 컨테이너를 단순히 정의로 외우기보다, 왜 Docker에서 이 두 개념이 분리되어 있는지, 그리고 둘이 어떤 관계를 가지는지를 흐름에 따라 정리해보려고 한다.

1. 이미지(Image)

Docker에서 이미지는 애플리케이션을 실행하기 위한 환경을 담고 있는 정적인 파일 묶음이다. 여기에는 애플리케이션 코드뿐만 아니라, 애플리케이션이 실행되기 위해 필요한 런타임, 라이브러리, 설정 파일, 디렉토리 구조 등이 함께 포함된다.

예를 들어 Node.js 애플리케이션을 실행한다고 생각해보자. 이 애플리케이션은 단순히 index.js 파일 하나만 있다고 실행되는 것이 아니다. 특정 버전의 Node.js가 필요하고, package.json에 정의된 의존성들이 설치되어 있어야 하며, 경우에 따라 OS 패키지나 환경 변수 설정도 필요하다.

기존 방식에서는 이 환경을 서버마다 직접 맞춰줘야 했다. 로컬에서는 Node.js 24를 사용하고 있는데 운영 서버에는 Node.js 16이 설치되어 있거나, 로컬에는 특정 시스템 라이브러리가 설치되어 있는데 서버에는 없는 경우 문제가 발생한다. 이런 상황에서 자주 나오는 말이 '내 로컬에서는 잘 되는데?'이다.

Docker 이미지는 이 문제를 줄이기 위해 실행에 필요한 환경을 하나의 단위로 묶는다. 즉, 애플리케이션이 어떤 환경에서 실행되어야 하는지를 이미지 안에 고정해두는 것이다. 조금 더 구체적으로 말하면 이미지는 하나의 파일 시스템 스냅샷에 가깝다. 애플리케이션이 실행될 때 필요한 디렉토리 구조와 파일들이 이미지 안에 포함되어 있고, Docker는 이 이미지를 기반으로 컨테이너를 실행한다.

이때 중요한 특징은 이미지가 기본적으로 변경되지 않는다는 점이다. 이미지는 읽기 전용(Read-only)으로 취급된다. 한 번 만들어진 이미지는 그대로 유지되고, 변경이 필요하면 기존 이미지를 수정하는 것이 아니라 새로운 이미지를 다시 만든다.

이 구조는 Docker의 중요한 장점으로 이어진다. 같은 이미지를 사용하면 어디서 실행하든 동일한 환경을 재현할 수 있다. 내 로컬에서 실행한 이미지와 운영 서버에서 실행한 이미지가 같다면, 적어도 실행 환경 차이로 인한 문제는 크게 줄어든다. 이런 의미에서 이미지는 단순히 파일 묶음이라기보다, 애플리케이션 실행 환경을 고정해둔 불변의 기준점이라고 보는 것이 더 정확하다.

2. 컨테이너(Container)

이미지가 실행 환경을 담고 있는 정적인 단위라면, 컨테이너는 그 이미지를 실제로 실행한 상태이다.

예를 들어 node-app:1.0이라는 이미지가 있다고 해보자. 이 이미지는 Node.js 애플리케이션을 실행하기 위한 모든 파일과 설정을 담고 있다. 하지만 이미지 자체는 실행 중인 프로그램이 아니다. 그저 실행 가능한 상태로 준비되어 있는 정적인 결과물이다.

이 이미지를 docker run 명령어로 실행하면 비로소 컨테이너가 생성된다. 이 컨테이너 안에서 애플리케이션 프로세스가 실행되고, 네트워크 포트를 열거나 로그를 출력하거나 파일을 생성하는 등의 실제 동작이 이루어진다.

graph TD
    A[Docker Image] --> B[Container]
    B --> C[Running Process]

이 관계를 비유로 설명하면 이미지는 게임 설치 파일이나 애플리케이션 설치 패키지에 가깝고, 컨테이너는 그 프로그램을 실제로 실행한 상태에 가깝다. 설치 파일은 여러 번 사용할 수 있지만 실행 중인 프로그램은 각각 독립된 상태를 가진다. Docker에서도 마찬가지다. 하나의 이미지로 여러 개의 컨테이너를 만들 수 있다.

graph TD
    A[Image: node-app:1.0]

    A --> B[Container 1]
    A --> C[Container 2]
    A --> D[Container 3]

여기서 중요한 점은 각 컨테이너가 서로 독립적으로 실행된다는 것이다. 같은 이미지를 기반으로 만들어졌더라도, 컨테이너는 각각 별도의 실행 상태를 가진다. 어떤 컨테이너에서 로그가 쌓이거나 임시 파일이 생성되더라도 다른 컨테이너에는 영향을 주지 않는다. 즉, 이미지는 공통된 실행 기준이고, 컨테이너는 그 기준을 바탕으로 실제로 실행된 각각의 인스턴스라고 볼 수 있다.

3. 이미지와 컨테이너

처음에는 이미지와 컨테이너를 왜 굳이 나눠서 관리하는지 헷갈릴 수 있다. 그냥 이미지를 실행하면 되는 것 아닌가 싶기도 하다. 하지만 Docker가 이 둘을 분리한 이유는 꽤 명확하다.

이미지는 어떤 환경에서 실행할 것인가를 정의하고, 컨테이너는 지금 실제로 실행되고 있는 상태를 나타낸다. 이 둘을 분리했기 때문에 Docker는 재현성과 확장성을 동시에 얻을 수 있다.

예를 들어 하나의 이미지를 만들어두면 개발자의 로컬 환경에서도 실행할 수 있고, 테스트 서버에서도 실행할 수 있고, 운영 서버에서도 실행할 수 있다. 실행 위치가 달라져도 같은 이미지를 사용한다면 같은 환경에서 애플리케이션을 실행할 수 있다.

또한 같은 이미지를 기반으로 컨테이너를 여러 개 띄울 수 있기 때문에 트래픽이 많아졌을 때 컨테이너 수를 늘리는 방식으로 확장할 수 있다. 애플리케이션을 새로 설치하거나 환경을 다시 구성할 필요 없이 이미지를 기반으로 컨테이너만 추가로 실행하면 된다.

이 구조는 배포에도 큰 장점을 준다. 운영 환경에 직접 접속해서 라이브러리를 설치하고 설정 파일을 수정하는 방식은 실수 가능성이 높다. 반면 이미지를 만들어 배포하면, 운영 서버에서는 그 이미지를 받아 실행하기만 하면 된다. 배포 대상은 코드 조각이 아니라 실행 환경 전체가 된다.

결국 이미지와 컨테이너의 분리는 Docker가 단순한 실행 도구가 아니라 환경을 재현하고 배포하고 확장하기 위한 구조라는 점을 보여준다.

4. 이미지의 읽기 전용 구조와 컨테이너의 쓰기 가능 영역

이미지와 컨테이너를 이해할 때 중요한 개념이 하나 더 있다. 바로 이미지가 읽기 전용이라는 점이다. 이미지는 한 번 만들어지면 기본적으로 변경되지 않는다. 그렇다면 컨테이너 안에서 파일을 생성하거나 수정하면 어떻게 될까? 예를 들어 컨테이너 내부에서 로그 파일이 생성되거나, 애플리케이션이 임시 파일을 만든다면 이미지가 바뀌는 것일까?

그렇지 않다.

Docker는 이미지를 그대로 유지하고 컨테이너가 실행될 때 그 위에 쓰기 가능한 레이어를 하나 추가한다. 컨테이너에서 발생하는 변경 사항은 이 쓰기 가능한 레이어에 기록된다.

graph TD
    A[Image Layer - Read Only]
    B[Container Writable Layer]
    C[Running Container]

    A --> B
    B --> C

이 구조 덕분에 같은 이미지를 기반으로 여러 컨테이너를 실행해도 각 컨테이너는 서로 다른 변경 사항을 가질 수 있다. 컨테이너 A에서 파일을 수정해도 컨테이너 B에는 영향을 주지 않고, 원본 이미지도 변하지 않는다.

이 점은 매우 중요하다. 컨테이너는 언제든지 삭제하고 다시 만들 수 있는 존재이기 때문이다. 컨테이너를 삭제하면 그 컨테이너의 쓰기 가능한 레이어도 함께 사라진다. 하지만 원본 이미지는 그대로 남아 있으므로 같은 이미지를 사용해 다시 컨테이너를 실행하면 처음 상태로 돌아갈 수 있다. 이 때문에 Docker 환경에서는 컨테이너를 영구적인 서버처럼 다루기보다 필요하면 버리고 다시 만들 수 있는 실행 단위로 바라보는 것이 자연스럽다.

5. 컨테이너 안에 저장한 데이터는 어떻게 될까?

여기서 자연스럽게 또 하나의 질문이 생긴다. 컨테이너를 삭제하면 쓰기 가능한 레이어가 사라진다면, 데이터베이스 파일이나 업로드 파일처럼 유지되어야 하는 데이터는 어떻게 해야 할까?

이런 데이터를 컨테이너 내부에만 저장하면 위험하다. 컨테이너가 삭제되거나 재생성될 때 데이터가 함께 사라질 수 있기 때문이다. 그래서 Docker에서는 볼륨(Volume)이나 바인드 마운트(Bind Mount)를 사용한다. 컨테이너 내부의 특정 경로를 호스트의 저장 공간이나 Docker가 관리하는 별도 저장 공간과 연결해서, 컨테이너가 사라져도 데이터가 유지되도록 만드는 방식이다.

graph TD
    A[Container]
    B[Writable Layer]
    C[Volume]
    D[Persistent Data]

    A --> B
    A --> C
    C --> D

이 구조를 이해하면 컨테이너를 어떻게 다뤄야 하는지도 자연스럽게 보인다. 애플리케이션 실행에 필요한 코드는 이미지에 포함시키고 실행 중 계속 유지되어야 하는 데이터는 컨테이너 내부가 아니라 외부 저장 공간에 분리해야 한다.

즉, 이미지는 실행 환경을 정의하고, 컨테이너는 그 환경을 실행하며, 영속 데이터는 볼륨으로 분리하는 구조가 Docker를 사용할 때의 기본적인 사고방식이다.

6. 비유

이미지와 컨테이너의 관계는 클래스와 인스턴스에 비유할 수도 있다. 클래스는 객체가 어떤 구조와 동작을 가질지 정의한다. 하지만 클래스 자체가 실제 객체는 아니다. 클래스를 기반으로 인스턴스를 생성해야 실제로 사용할 수 있다. 이미지와 컨테이너도 비슷하다. 이미지는 실행 환경을 정의하고 컨테이너는 그 이미지를 기반으로 실제 실행된 인스턴스다.

graph TD
    A[Class] --> B[Instance 1]
    A --> C[Instance 2]

    D[Image] --> E[Container 1]
    D --> F[Container 2]

물론 이 비유가 완전히 동일한 것은 아니다. 하지만 처음 개념을 잡을 때는 꽤 도움이 된다. 중요한 것은 이미지가 실행 기준이고 컨테이너는 그 기준을 바탕으로 만들어진 실행 상태라는 점이다.

또 다른 비유로는 요리 레시피와 실제 요리를 들 수 있다. 이미지는 레시피처럼 어떤 재료와 과정을 통해 실행 환경을 만들지 정의한다. 컨테이너는 그 레시피를 바탕으로 실제로 만들어진 요리다. 같은 레시피로 여러 접시의 요리를 만들 수 있지만, 각 접시는 서로 다른 상태를 가진다.

이 비유를 통해서도 이미지와 컨테이너가 왜 분리되어 있는지 이해할 수 있다. 기준은 하나지만 실행 결과는 여러 개가 될 수 있다.

마치며

Docker에서 이미지와 컨테이너는 가장 기본이 되는 개념이지만 처음에는 쉽게 헷갈릴 수 있다. 둘이 항상 함께 등장하고 이미지를 실행하면 컨테이너가 만들어지기 때문이다. 하지만 역할을 나눠서 보면 훨씬 명확해진다.

이미지는 애플리케이션을 실행하기 위한 환경을 고정해둔 정적인 기준이고, 컨테이너는 그 이미지를 바탕으로 실제 실행된 상태이다. 이미지는 변경되지 않고 컨테이너는 실행되면서 자신만의 상태를 가진다.

이 구조 덕분에 Docker는 동일한 환경을 반복해서 재현할 수 있고 같은 이미지를 기반으로 여러 컨테이너를 실행할 수 있으며 컨테이너를 삭제하고 다시 만들어도 원본 환경은 유지할 수 있다.

공부하면서 느낀 점은, Docker를 이해할 때 명령어보다 먼저 이 관계를 정확히 잡는 것이 중요하다는 것이다. docker build, docker pull, docker run, docker ps 같은 명령어도 결국 이미지와 컨테이너의 관계 위에서 동작한다.

이미지와 컨테이너의 개념이 잡히면 Docker가 조금 덜 낯설어진다. 그리고 그때부터는 Docker가 단순히 무언가를 실행하는 도구가 아니라, 실행 환경을 재현하고 관리하기 위한 구조로 보이기 시작하는 것 같다.