Tuesday, October 21, 2025

컨테이너 기술, 현대 개발의 표준이 되다

소프트웨어 개발의 역사는 '표준화'를 향한 끊임없는 여정이었습니다. 과거 개발자들은 "내 컴퓨터에서는 잘 되는데..."라는 말을 입에 달고 살았습니다. 개발 환경과 실제 서비스가 운영되는 서버 환경의 미세한 차이가 예상치 못한 오류를 낳았고, 이 문제를 해결하기 위해 수많은 시간과 노력을 쏟아부어야 했습니다. 운영체제, 설치된 라이브러리 버전, 시스템 설정 등 수많은 변수가 애플리케이션의 안정성을 위협하는 시한폭탄과 같았습니다.

이러한 혼돈 속에서 등장한 가상 머신(Virtual Machine, VM)은 한 줄기 빛과 같았습니다. 하드웨어 전체를 가상화하여 호스트 운영체제 위에 독립된 게스트 운영체제를 통째로 설치하는 방식은 완벽한 격리를 보장했습니다. 개발 환경을 VM 이미지로 만들어 그대로 운영 서버에 배포하면, 환경 차이로 인한 문제는 대부분 사라졌습니다. 하지만 이 방식은 완벽하지 않았습니다. VM은 운영체제를 포함하기 때문에 용량이 수십 기가바이트에 달하고, 부팅하는 데 수 분이 소요되는 등 매우 '무거운' 기술이었습니다. 하나의 물리 서버에 올릴 수 있는 VM의 수는 제한적이었고, 이는 곧 자원 낭비로 이어졌습니다.

바로 이 지점에서 도커(Docker)로 대표되는 컨테이너 기술이 혁명적인 대안으로 떠올랐습니다. 컨테이너는 애플리케이션과 그 실행에 필요한 모든 종속성(라이브러리, 프레임워크 등)을 패키징하지만, 운영체제(OS)는 포함하지 않습니다. 대신 호스트 서버의 OS 커널을 공유합니다. 이는 마치 잘 지어진 아파트 단지와 같습니다. 각 세대(컨테이너)는 독립된 생활 공간을 보장받지만, 전기, 수도, 가스 같은 핵심 기반 시설(OS 커널)은 단지 전체가 공유하는 것과 같습니다. 이 구조 덕분에 컨테이너는 VM에 비해 압도적으로 가볍고 빠르며, 뛰어난 이식성을 자랑합니다.

가상 머신과 컨테이너: 근본적인 차이의 이해

컨테이너 기술을 제대로 이해하기 위해서는 가장 많이 비교되는 대상인 가상 머신(VM)과의 구조적 차이를 명확히 인지하는 것이 중요합니다. 두 기술 모두 애플리케이션을 격리된 환경에서 실행한다는 공통된 목표를 가지고 있지만, 그 목표를 달성하는 방식에서 근본적인 차이가 존재하며, 이 차이가 성능, 효율성, 이식성 등 모든 면에서 극명한 대조를 이룹니다.

가상 머신(Virtual Machine)의 아키텍처

가상 머신은 '하이퍼바이저(Hypervisor)'라는 소프트웨어 계층 위에서 동작합니다. 하이퍼바이저는 물리적인 하드웨어(CPU, RAM, 스토리지 등)를 가상화하여 여러 개의 독립된 '가상 컴퓨터'를 생성합니다. 그리고 각 가상 컴퓨터, 즉 VM 위에는 완전한 형태의 게스트 운영체제(Guest OS)가 설치되어야 합니다. 예를 들어, 리눅스 서버 위에 윈도우 VM과 또 다른 버전의 리눅스 VM을 동시에 실행할 수 있습니다. 각 VM은 자신만의 커널을 가진 독립적인 OS를 운영하며, 그 위에 필요한 라이브러리와 애플리케이션이 설치됩니다.

  • 장점:
    • 완벽한 격리: 각 VM은 하드웨어 수준에서부터 격리되므로, 하나의 VM에서 발생한 문제가 다른 VM이나 호스트 시스템에 영향을 미칠 가능성이 거의 없습니다. 보안 측면에서 매우 강력합니다.
    • 다양한 OS 운영: 호스트 OS와 다른 종류의 OS를 게스트로 실행할 수 있습니다. (예: macOS에서 윈도우 실행)
  • 단점:
    • 무거움: 각 VM이 완전한 OS를 포함하므로, 이미지 파일의 크기가 수 GB에서 수십 GB에 달합니다.
    • 느린 속도: VM을 시작하는 것은 컴퓨터 한 대를 부팅하는 것과 같아서 수 분의 시간이 소요됩니다.
    • 자원 비효율성: 여러 VM이 동일한 OS를 사용하더라도, 각 VM은 자신만의 OS 커널과 시스템 라이브러리를 메모리에 중복해서 로드합니다. 이는 상당한 메모리와 CPU 자원 낭비로 이어집니다.

컨테이너(Container)의 아키텍처

컨테이너는 하이퍼바이저 대신 '컨테이너 런타임 엔진(Container Runtime Engine)', 예를 들어 도커 엔진(Docker Engine) 위에서 동작합니다. 가장 큰 차이점은 컨테이너가 호스트 시스템의 OS 커널을 직접 공유한다는 것입니다. 컨테이너 내부에는 애플리케이션을 실행하는 데 필요한 최소한의 라이브러리와 바이너리 파일만 패키징되어 있습니다. OS 커널은 공유하지만, 리눅스의 네임스페이스(Namespace)와 제어 그룹(cgroups) 같은 기술을 사용하여 각 컨테이너의 프로세스, 네트워크, 파일 시스템 등을 논리적으로 격리합니다.

  • 장점:
    • 가벼움: OS를 포함하지 않으므로 이미지 크기가 수십 MB 수준으로 매우 작습니다.
    • 빠른 속도: OS 부팅 과정이 없기 때문에 컨테이너는 단 몇 초 만에 시작할 수 있습니다. 이는 신속한 스케일링(확장/축소)에 매우 유리합니다.
    • 높은 자원 효율성: 여러 컨테이너가 OS 커널을 공유하므로 중복되는 시스템 파일이 메모리에 로드되지 않습니다. 동일한 하드웨어 사양에서 VM보다 훨씬 더 많은 수의 컨테이너를 실행할 수 있습니다.
    • 뛰어난 이식성: 도커가 설치된 환경이라면 어디서든(개발자 노트북, 테스트 서버, 클라우드 등) 동일하게 실행되는 것을 보장합니다.
  • 단점:
    • 상대적으로 낮은 격리 수준: OS 커널을 공유하기 때문에, 만약 커널에 심각한 보안 취약점이 발생할 경우 모든 컨테이너가 위험에 노출될 수 있습니다. VM의 하드웨어 수준 격리보다는 보안 강도가 낮다고 평가됩니다.
    • OS 종속성: 호스트 OS의 커널을 공유하므로, 리눅스 호스트에서는 리눅스 컨테이너를, 윈도우 호스트에서는 윈도우 컨테이너를 실행하는 것이 일반적입니다. (WSL2와 같은 기술로 일부 극복 가능)

비교 요약표

특징 가상 머신 (VM) 컨테이너 (Container)
핵심 기술 하이퍼바이저 (하드웨어 가상화) 컨테이너 엔진 (OS 수준 가상화)
격리 수준 프로세스, 메모리, 파일 시스템, 네트워크, 커널, 하드웨어 프로세스, 파일 시스템, 네트워크 (OS 커널 공유)
크기 수 GB ~ 수십 GB (Guest OS 포함) 수 MB ~ 수백 MB
시작 시간 수 분 (OS 부팅 필요) 수 초 (프로세스 실행)
성능 하이퍼바이저 오버헤드로 인한 약간의 성능 저하 네이티브에 가까운 성능
자원 효율성 낮음 (OS 중복 실행으로 인한 메모리, CPU 낭비) 높음 (OS 커널 공유, 라이브러리 공유)
이식성 하이퍼바이저에 종속적일 수 있음 매우 높음 (컨테이너 엔진만 있으면 어디서든 실행)

이러한 차이점 때문에 현대의 마이크로서비스 아키텍처(MSA) 환경에서는 작고 빠르게 배포하고 확장해야 하는 서비스 단위에 컨테이너를 사용하는 것이 거의 표준으로 자리 잡았습니다. 반면, 레거시 시스템을 그대로 옮기거나, 전혀 다른 OS 환경을 구동해야 하거나, 커널 수준의 강력한 보안 격리가 필수적인 경우에는 여전히 VM이 유효한 선택지가 될 수 있습니다.

도커 생태계의 핵심 구성 요소

도커는 단순히 컨테이너를 실행하는 도구가 아니라, 컨테이너의 생성, 관리, 배포, 공유를 아우르는 거대한 생태계입니다. 이 생태계를 이해하기 위해서는 몇 가지 핵심 구성 요소의 역할과 상호작용을 알아야 합니다.

1. 도커 엔진 (Docker Engine)

도커 엔진은 도커의 심장과 같은 역할을 합니다. 사용자가 도커 명령을 실행하면 실제로 컨테이너를 생성하고 관리하는 주체입니다. 도커 엔진은 크게 세 가지 컴포넌트로 구성된 클라이언트-서버 애플리케이션입니다.

  • 서버 (데몬 프로세스): `dockerd`라는 데몬 프로세스가 백그라운드에서 항상 실행되며, 도커 이미지, 컨테이너, 네트워크, 볼륨 등을 관리하는 실질적인 작업을 수행합니다. REST API를 통해 외부의 요청을 받아 처리합니다.
  • REST API: 클라이언트가 서버(데몬)와 통신하기 위한 인터페이스입니다. `docker` CLI 명령어는 내부적으로 이 REST API를 호출하여 데몬에게 작업을 지시합니다.
  • 클라이언트 (CLI): 사용자가 터미널에서 입력하는 `docker` 명령어를 의미합니다. 사용자가 `docker run ...` 같은 명령을 입력하면, 클라이언트는 이 명령을 해석하여 도커 데몬의 REST API로 전송합니다. 도커 클라이언트는 데몬과 동일한 시스템에 있을 수도 있고, 원격 시스템에 있을 수도 있습니다.

2. 도커 이미지 (Docker Image)

도커 이미지는 컨테이너를 생성하기 위한 '설계도' 또는 '템플릿'입니다. 애플리케이션을 실행하는 데 필요한 모든 것, 즉 코드, 런타임, 시스템 도구, 라이브러리, 설정 등을 포함하는 읽기 전용(read-only) 파일입니다. 이미지는 객체지향 프로그래밍의 '클래스'에, 컨테이너는 그 클래스로부터 생성된 '인스턴스(객체)'에 비유할 수 있습니다.

이미지의 가장 중요한 특징 중 하나는 '계층(Layer)' 구조를 가진다는 점입니다. Dockerfile의 각 명령어는 이미지의 새로운 계층을 만듭니다. 예를 들어, 우분투(Ubuntu) 이미지를 기반으로 하고, 그 위에 파이썬(Python)을 설치하고, 마지막으로 내 애플리케이션 코드를 복사한다면 이미지는 다음과 같은 계층으로 구성됩니다.

  1. Base Layer: Ubuntu OS 파일 시스템
  2. Layer 2: Python 설치로 인해 변경된 파일들
  3. Layer 3: 내 애플리케이션 코드 파일들

이러한 계층 구조는 매우 효율적입니다. 만약 여러 이미지가 동일한 기반 이미지(예: Ubuntu)를 사용한다면, 해당 기반 계층은 디스크에 단 한 번만 저장되고 여러 이미지에서 공유됩니다. 또한, 이미지를 업데이트할 때도 변경된 계층만 새로 다운로드하거나 빌드하면 되므로 시간과 네트워크 대역폭을 크게 절약할 수 있습니다.

3. 도커 컨테이너 (Docker Container)

컨테이너는 도커 이미지의 실행 가능한 인스턴스입니다. 하나의 이미지로부터 수십, 수백 개의 동일한 컨테이너를 생성할 수 있습니다. 각 컨테이너는 격리된 환경을 가지며, 자신만의 파일 시스템, 프로세스 공간, 네트워크 인터페이스를 갖습니다.

이미지는 읽기 전용이지만, 컨테이너가 실행될 때는 이미지의 최상단에 쓰기 가능한 컨테이너 계층(Writable Container Layer)이 추가됩니다. 컨테이너 내부에서 파일이 생성되거나 수정되면 이 쓰기 가능 계층에 저장됩니다. 컨테이너가 삭제되면 이 계층도 함께 사라지므로, 컨테이너 내부에 저장된 데이터는 기본적으로 영속성을 갖지 않습니다. (이를 해결하기 위해 '볼륨'이라는 기술을 사용합니다.)

4. 도커 레지스트리 (Docker Registry)

도커 레지스트리는 도커 이미지를 저장하고 배포하는 '저장소'입니다. Git이 소스 코드를 관리하는 버전 관리 시스템이라면, 레지스트리는 도커 이미지를 관리하는 버전 관리 시스템과 같습니다.

  • Docker Hub: 도커사가 운영하는 공식적인 공개 레지스트리입니다. 수많은 공식 이미지(Ubuntu, Python, Nginx 등)와 사용자들이 만든 공개 이미지가 저장되어 있어 누구나 쉽게 이미지를 받아 사용할 수 있습니다.
  • Private Registry: 보안이나 정책상의 이유로 이미지를 외부에 공개하고 싶지 않을 경우, 기업 내부에 자체적으로 레지스트리를 구축할 수 있습니다. Amazon ECR, Google GCR, Azure CR과 같은 클라우드 제공업체의 관리형 프라이빗 레지스트리 서비스도 널리 사용됩니다.

이러한 구성 요소들은 `docker pull` (레지스트리에서 이미지 다운로드), `docker build` (Dockerfile로부터 이미지 생성), `docker run` (이미지로 컨테이너 실행), `docker push` (이미지를 레지스트리에 업로드)와 같은 명령어를 통해 유기적으로 상호작용하며 도커의 강력한 워크플로우를 완성합니다.

나만의 설계도, Dockerfile 작성법

Dockerfile은 도커 이미지를 어떻게 만들 것인지를 정의하는 텍스트 파일입니다. 이 파일 안에 순차적으로 명령어를 기술하면, `docker build` 명령이 이 파일을 읽어 자동으로 이미지를 생성해줍니다. 인프라를 코드로 관리하는 'Infrastructure as Code(IaC)'의 핵심적인 실천 방법 중 하나입니다.

간단한 Node.js 애플리케이션을 도커라이징하는 과정을 통해 Dockerfile의 주요 명령어들을 심도 있게 살펴보겠습니다.

예제 Node.js 애플리케이션:

package.json


{
  "name": "simple-app",
  "version": "1.0.0",
  "description": "A simple Node.js app for Docker.",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}

server.js


const express = require('express');
const app = express();
const PORT = 8080;
const HOST = '0.0.0.0';

app.get('/', (req, res) => {
  res.send('Hello, Docker World!');
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

이제 이 애플리케이션을 위한 Dockerfile을 작성해 봅시다.


# 1. 베이스 이미지(Base Image) 지정
FROM node:18-alpine

# 2. 작업 디렉토리(Working Directory) 설정
WORKDIR /usr/src/app

# 3. 애플리케이션 종속성 파일 복사
COPY package*.json ./

# 4. 종속성 설치
RUN npm install

# 5. 소스 코드 복사
COPY . .

# 6. 노출할 포트 지정
EXPOSE 8080

# 7. 컨테이너 시작 시 실행될 명령어
CMD [ "npm", "start" ]

이제 각 명령어의 의미와 그 이면에 숨겨진 최적화 원리를 자세히 알아보겠습니다.

1. `FROM`: 모든 것의 시작

FROM 명령어는 생성할 이미지가 어떤 이미지를 기반으로 할 것인지를 지정합니다. 모든 Dockerfile은 `FROM`으로 시작해야 합니다. 위 예제에서는 `node:18-alpine`을 사용했습니다.

  • node: 사용할 이미지의 이름입니다. 여기서는 Node.js 공식 이미지를 의미합니다.
  • 18: 이미지의 버전(태그)입니다. Node.js 18.x 버전을 사용하겠다는 의미입니다. 항상 특정 버전을 명시하는 것이 좋습니다. `latest` 태그를 사용하면 빌드 시점에 따라 다른 버전이 사용될 수 있어 재현성을 해칠 수 있습니다.
  • alpine: 이미지의 변종을 나타냅니다. Alpine Linux는 보안에 중점을 둔 초경량 리눅스 배포판으로, 이를 기반으로 한 이미지는 용량이 매우 작다는 장점이 있습니다. `node:18` (Debian 기반) 이미지가 약 900MB인 반면, `node:18-alpine`은 약 100MB에 불과합니다. 이미지 크기를 작게 유지하는 것은 배포 속도, 저장 공간, 보안 측면에서 매우 중요합니다.

2. `WORKDIR`: 작업 공간 지정

WORKDIR 명령어는 이후의 `RUN`, `CMD`, `ENTRYPOINT`, `COPY`, `ADD` 명령어가 실행될 기본 디렉토리를 설정합니다. 만약 해당 디렉토리가 존재하지 않으면 자동으로 생성합니다.

WORKDIR /usr/src/app을 사용하면, 이후의 모든 명령어는 `/usr/src/app` 디렉토리 내에서 실행되는 것과 같습니다. 이는 `RUN cd /usr/src/app && ...` 와 같이 매번 경로를 지정하는 것보다 훨씬 깔끔하고 안전합니다.

3. `COPY`: 호스트의 파일을 이미지로

COPY` 명령어는 호스트 머신의 파일이나 디렉토리를 이미지의 파일 시스템으로 복사합니다. `COPY <원본 경로> <대상 경로>` 형식으로 사용합니다.

위 예제에서 `COPY package*.json ./`는 `package.json`과 `package-lock.json` 파일을 호스트의 현재 경로에서 이미지의 `WORKDIR`인 `/usr/src/app`으로 복사합니다. 여기서 중요한 점은 `COPY . .`을 하기 전에 `package.json`을 먼저 복사하고 `npm install`을 실행했다는 것입니다. 이는 도커의 계층 캐싱(Layer Caching) 기능을 최적으로 활용하기 위한 전략입니다.

도커는 이미지를 빌드할 때 각 명령어를 하나의 계층으로 만듭니다. 다음 빌드 시, 해당 명령어와 관련된 파일에 변경 사항이 없다면 이전에 만들어 둔 계층을 그대로 재사용(캐시)합니다. `package.json`은 자주 바뀌지 않지만, 소스 코드(`server.js`)는 자주 바뀝니다. 만약 소스 코드 전체를 먼저 복사한 후 `npm install`을 실행하면, 소스 코드가 조금만 변경되어도 `COPY` 계층이 무효화되고, 그 이후의 `npm install` 계층도 매번 새로 실행되어야 합니다. 이는 불필요하게 많은 시간을 낭비합니다. 하지만 위와 같이 구성하면, `package.json`에 변경이 없을 경우 소스 코드만 바뀌어도 `RUN npm install` 계층은 캐시를 사용하게 되어 빌드 속도가 비약적으로 향상됩니다.

4. `RUN`: 이미지 빌드 중 명령어 실행

`RUN` 명령어는 이미지 빌드 과정에서 셸 명령을 실행합니다. 주로 패키지 설치, 디렉토리 생성, 컴파일 등의 작업에 사용됩니다. 각 `RUN` 명령어는 새로운 계층을 생성합니다.

RUN npm install은 `COPY`된 `package.json`을 기반으로 Node.js 종속성 패키지들을 설치합니다.

팁: 여러 개의 `RUN` 명령어를 사용하는 것보다 `&&`를 사용하여 하나의 `RUN` 명령어로 묶는 것이 이미지 계층 수를 줄여주므로 더 효율적일 수 있습니다. 예를 들어:


# 비효율적인 방식
RUN apt-get update
RUN apt-get install -y vim

# 효율적인 방식
RUN apt-get update && apt-get install -y vim

5. `EXPOSE`: 포트 문서화

EXPOSE 명령어는 컨테이너가 실행될 때 특정 네트워크 포트를 리스닝할 것임을 명시하는, 일종의 문서화 기능입니다. `EXPOSE 8080`은 "이 컨테이너는 8080 포트를 사용할 예정입니다."라고 알려주는 역할을 합니다.

중요한 것은 EXPOSE 자체가 실제로 포트를 외부에 개방하지는 않는다는 점입니다. 실제 포트 매핑은 컨테이너를 실행하는 `docker run` 명령어의 `-p` 또는 `-P` 옵션을 통해 이루어집니다.

  • -p 8080:8080: 호스트의 8080 포트를 컨테이너의 8080 포트에 연결합니다.
  • -P: `EXPOSE`로 지정된 모든 포트를 호스트의 임의의 가용 포트에 자동으로 연결합니다.

6. `CMD` vs `ENTRYPOINT`: 컨테이너의 기본 실행 명령

CMD와 `ENTRYPOINT`는 모두 컨테이너가 시작될 때 실행되는 기본 명령어를 지정한다는 점에서 비슷하지만, 작동 방식에 미묘하면서도 중요한 차이가 있습니다.

`CMD` (Command)

  • 역할: 컨테이너의 기본 실행 명령을 제공합니다.
  • 특징: `docker run` 명령어 뒤에 다른 명령어를 추가하면 `CMD`는 무시되고 추가된 명령어가 대신 실행됩니다. 즉, 쉽게 덮어쓸 수 있습니다.
  • 형식:
    • Exec 형식 (권장): `CMD ["실행 파일", "파라미터1", "파라미터2"]`
    • Shell 형식: `CMD 명령어 파라미터1 파라미터2`
  • 예제: CMD [ "npm", "start" ]. 만약 `docker run my-app-image /bin/bash`를 실행하면, `npm start` 대신 `/bin/bash`가 실행되어 컨테이너의 셸에 접속하게 됩니다.

`ENTRYPOINT` (Entrypoint)

  • 역할: 컨테이너를 하나의 실행 파일처럼 만들어 줍니다.
  • 특징: `docker run` 명령어 뒤에 추가되는 인자들은 `ENTRYPOINT` 명령어의 인자로 전달됩니다. 쉽게 덮어써지지 않습니다. (덮어쓰려면 `--entrypoint` 플래그를 사용해야 함)
  • 형식:
    • Exec 형식 (권장): `ENTRYPOINT ["실행 파일", "파라미터1"]`
    • Shell 형식: `ENTRYPOINT 명령어 파라미터1`

`ENTRYPOINT`와 `CMD`의 조합

두 명령어를 함께 사용하는 것이 가장 강력하고 유연한 방법입니다. `ENTRYPOINT`는 고정된 실행 명령을 지정하고, `CMD`는 그 실행 명령에 전달될 기본 파라미터를 지정합니다.

예를 들어, `curl` 명령을 실행하는 컨테이너를 만든다고 가정해봅시다.


FROM alpine
RUN apk add --no-cache curl
ENTRYPOINT ["curl"]
CMD ["-h"]
  • docker run my-curl-image: `CMD`가 기본 인자로 사용되어 `curl -h` (도움말 보기)가 실행됩니다.
  • docker run my-curl-image google.com: `run` 뒤에 추가된 `google.com`이 `CMD`를 덮어쓰고 `ENTRYPOINT`의 인자로 전달되어 `curl google.com`이 실행됩니다.

이처럼 `ENTRYPOINT`로 컨테이너의 주된 목적(여기서는 `curl` 실행)을 고정하고, `CMD`로 기본 동작이나 예시 파라미터를 제공하는 것이 좋은 패턴입니다.

이미지 빌드 및 실행

Dockerfile 작성이 완료되면 다음 명령어로 이미지를 빌드합니다.


# docker build -t <이미지이름>:<태그> <Dockerfile이 있는 경로>
docker build -t my-node-app:1.0 .
  • -t: 이미지에 이름과 태그(버전)를 부여합니다.
  • .: 현재 디렉토리에서 Dockerfile을 찾아 빌드를 진행하라는 의미입니다.

빌드가 성공적으로 완료되면, 다음 명령어로 컨테이너를 실행할 수 있습니다.


# docker run -d -p 8080:8080 --name my-running-app my-node-app:1.0
  • -d (detached): 컨테이너를 백그라운드에서 실행합니다.
  • -p 8080:8080: 호스트의 8080 포트와 컨테이너의 8080 포트를 연결합니다.
  • --name: 컨테이너에 식별하기 쉬운 이름을 부여합니다.

이제 웹 브라우저에서 `http://localhost:8080`에 접속하면 "Hello, Docker World!" 메시지를 확인할 수 있습니다.

컨테이너 생명주기 관리

애플리케이션을 컨테이너화했다면, 그 컨테이너를 효과적으로 관리하는 방법을 알아야 합니다. 컨테이너는 생성, 실행, 중지, 재시작, 삭제의 생명주기를 가지며, 도커는 각 단계를 제어할 수 있는 다양한 명령어를 제공합니다.

컨테이너 목록 확인: `docker ps`

현재 실행 중인 컨테이너의 목록을 확인하려면 `docker ps` 명령을 사용합니다.


$ docker ps
CONTAINER ID   IMAGE             COMMAND                  CREATED         STATUS         PORTS                    NAMES
a1b2c3d4e5f6   my-node-app:1.0   "docker-entrypoint.s…"   5 minutes ago   Up 5 minutes   0.0.0.0:8080->8080/tcp   my-running-app

중지된 컨테이너까지 모두 보려면 `-a` 또는 `--all` 옵션을 추가합니다.


$ docker ps -a

컨테이너 중지 및 재시작

실행 중인 컨테이너를 중지하려면 `docker stop`을, 다시 시작하려면 `docker start`를 사용합니다. 컨테이너 ID 또는 이름을 인자로 전달합니다.


# 컨테이너 중지
docker stop my-running-app

# 컨테이너 재시작
docker start my-running-app

컨테이너 로그 확인: `docker logs`

컨테이너 내부에서 실행되는 애플리케이션의 표준 출력(stdout) 및 표준 에러(stderr) 로그를 확인하는 것은 디버깅에 매우 중요합니다. `docker logs` 명령을 사용합니다.


$ docker logs my-running-app
Running on http://0.0.0.0:8080

실시간으로 로그를 계속해서 보려면 `-f` 또는 `--follow` 옵션을 추가합니다. (마치 `tail -f` 처럼)


docker logs -f my-running-app

실행 중인 컨테이너에 접속: `docker exec`

이미 실행 중인 컨테이너 내부에 들어가서 명령을 실행하거나 상태를 확인해야 할 때가 있습니다. 이럴 때 `docker exec` 명령을 사용합니다. 특히 `-it` 옵션을 함께 사용하여 상호작용이 가능한 터미널(interactive tty)을 활성화하는 것이 일반적입니다.


# my-running-app 컨테이너 내부에서 /bin/sh 셸을 실행
docker exec -it my-running-app /bin/sh

위 명령을 실행하면 컨테이너 내부의 셸 프롬프트가 나타나며, `ls`, `ps`, `cat` 등 리눅스 명령어를 사용하여 컨테이너의 파일 시스템을 탐색하거나 실행 중인 프로세스를 확인할 수 있습니다.

컨테이너 삭제: `docker rm`

더 이상 필요 없는 컨테이너는 `docker rm` 명령으로 삭제하여 시스템 자원을 정리해야 합니다. 단, 실행 중인 컨테이너는 바로 삭제할 수 없으므로 먼저 `docker stop`으로 중지하거나, `-f` (force) 옵션을 사용하여 강제로 삭제할 수 있습니다.


# 중지된 컨테이너 삭제
docker rm <컨테이너_ID_또는_이름>

# 실행 중인 컨테이너 강제 삭제
docker rm -f <컨테이너_ID_또는_이름>

중지된 모든 컨테이너를 한 번에 정리하고 싶다면 다음 명령을 사용할 수 있습니다.


docker container prune

데이터 영속성 확보: 볼륨과 바인드 마운트

앞서 언급했듯이, 컨테이너는 기본적으로 상태 비저장(stateless)이며, 컨테이너가 삭제되면 내부에서 생성되거나 변경된 데이터도 함께 사라집니다. 하지만 데이터베이스, 로그 파일, 사용자 업로드 파일 등 영속적으로 보관해야 하는 데이터도 있습니다. 도커는 이러한 데이터를 컨테이너의 생명주기와 분리하여 관리하기 위해 두 가지 주요 메커니즘을 제공합니다: 볼륨(Volumes)바인드 마운트(Bind Mounts)입니다.

볼륨 (Volumes)

볼륨은 도커가 관리하는 호스트 파일 시스템의 특정 영역(/var/lib/docker/volumes/ 디렉토리 하위)에 데이터를 저장하는 방식입니다. 컨테이너를 생성할 때 볼륨을 연결하면, 컨테이너 내부의 특정 경로가 이 도커 관리 영역에 매핑됩니다. 이는 도커에서 데이터를 영속적으로 저장하기 위해 가장 권장되는 방법입니다.

  • 장점:
    • 도커에 의한 관리: 볼륨 생성, 삭제, 백업 등을 도커 CLI 명령(`docker volume create`, `docker volume ls` 등)으로 쉽게 관리할 수 있습니다.
    • OS 독립성: 볼륨은 도커 내부에서 관리되므로 호스트 OS의 디렉토리 구조에 대해 신경 쓸 필요가 없습니다.
    • 성능: 리눅스 호스트에서 네이티브 파일 시스템 성능을 제공합니다.
    • 안전성: 여러 컨테이너가 동시에 같은 볼륨을 안전하게 공유할 수 있습니다.

사용 예시 (MySQL 컨테이너에 데이터 볼륨 연결):


# 'mysql-data'라는 이름의 볼륨을 생성 (선택 사항, 없으면 자동 생성됨)
docker volume create mysql-data

# 컨테이너 실행 시 볼륨 연결
docker run -d \
  -p 3306:3306 \
  --name my-mysql \
  -e MYSQL_ROOT_PASSWORD=my-secret-pw \
  -v mysql-data:/var/lib/mysql \
  mysql:8.0

위 명령어에서 `-v mysql-data:/var/lib/mysql` 부분이 핵심입니다. `mysql-data`라는 이름의 볼륨을 컨테이너 내부의 `/var/lib/mysql` (MySQL 데이터가 저장되는 기본 경로) 디렉토리에 마운트하라는 의미입니다. 이제 `my-mysql` 컨테이너를 삭제하고 다시 생성하더라도, `mysql-data` 볼륨이 그대로 남아있기 때문에 데이터베이스의 모든 데이터는 그대로 보존됩니다.

바인드 마운트 (Bind Mounts)

바인드 마운트는 호스트 머신의 파일이나 디렉토리를 컨테이너에 직접 마운트하는 방식입니다. 호스트의 경로를 완전히 제어할 수 있다는 장점이 있지만, 그만큼 주의가 필요합니다.

  • 장점:
    • 개발 환경에 유리: 호스트에서 소스 코드를 수정하면 별도의 빌드/배포 과정 없이 즉시 실행 중인 컨테이너에 반영됩니다. 이는 개발 생산성을 크게 향상시킵니다.
    • 직관적인 경로: 호스트의 특정 경로에 데이터가 저장되므로 파일을 직접 확인하고 수정하기 편리합니다.
  • 단점:
    • 호스트 종속성: 호스트의 특정 디렉토리 구조에 의존하게 되므로 이식성이 떨어집니다.
    • 보안 위험: 컨테이너가 호스트의 파일 시스템에 직접 접근할 수 있으므로, 시스템 파일을 마운트하는 등 부주의하게 사용할 경우 보안상 위험할 수 있습니다.
    • 권한 문제: 호스트와 컨테이너 내부의 사용자(UID/GID)가 다를 경우 파일 접근 권한 문제가 발생할 수 있습니다.

사용 예시 (Node.js 개발 환경 구성):


# 호스트의 현재 디렉토리(.)를 컨테이너의 /usr/src/app에 마운트
docker run -d \
  -p 8080:8080 \
  --name my-dev-app \
  -v $(pwd):/usr/src/app \
  my-node-app:1.0

이제 호스트에서 `server.js` 파일의 내용을 변경하고 저장하면, 그 변경 사항이 즉시 `my-dev-app` 컨테이너에 반영되어 서비스에 나타납니다. `nodemon`과 같은 도구와 함께 사용하면 코드 변경 시 자동으로 서버를 재시작해주어 매우 편리한 개발 환경을 구축할 수 있습니다.

결론적으로, 프로덕션 환경의 데이터베이스나 상태 저장 애플리케이션에는 볼륨을 사용하는 것이 표준이며, 로컬 개발 환경에서 소스 코드를 실시간으로 연동할 때는 바인드 마운트가 매우 유용합니다.

도커, 데브옵스 문화의 촉진제

도커는 단순히 기술적인 도구를 넘어, 개발(Development)과 운영(Operations)의 협업을 강조하는 데브옵스(DevOps) 문화를 가속화하는 핵심적인 역할을 합니다. 도커가 어떻게 CI/CD 파이프라인을 혁신하고, 개발과 운영의 경계를 허무는지 이해하는 것은 매우 중요합니다.

과거에는 개발팀이 작성한 코드를 운영팀에 전달하면, 운영팀이 서버 환경에 맞춰 다시 빌드하고 배포하는 복잡한 과정을 거쳤습니다. 이 과정에서 발생하는 수많은 환경 변수로 인해 배포는 항상 어렵고 위험한 작업이었습니다. 하지만 도커 이미지는 애플리케이션과 실행 환경 전체를 하나의 '불변의 아티팩트(Immutable Artifact)'로 패키징합니다. 개발자는 자신의 로컬 환경에서 테스트를 마친 도커 이미지를 그대로 레지스트리에 푸시하고, 운영팀은 그 이미지를 어떤 서버에서든 변경 없이 그대로 가져와 실행하기만 하면 됩니다. '빌드, 배송, 실행(Build, Ship, and Run Any App, Anywhere)'이라는 도커의 슬로건이 바로 이를 의미합니다.

이를 통해 CI/CD(지속적인 통합/지속적인 배포) 파이프라인이 극적으로 단순화되고 안정화됩니다.

  1. 통합(Integration): 개발자가 코드를 Git과 같은 버전 관리 시스템에 푸시합니다.
  2. 빌드(Build): Jenkins, GitLab CI, GitHub Actions 같은 CI 도구가 변경 사항을 감지하고, 자동으로 `docker build`를 실행하여 새로운 버전의 도커 이미지를 생성합니다.
  3. 테스트(Test): 생성된 이미지를 기반으로 컨테이너를 실행하여 자동화된 테스트를 수행합니다.
  4. 릴리스(Release): 테스트를 통과한 이미지는 버전 태그와 함께 도커 레지스트리에 푸시됩니다.
  5. 배포(Deploy): 운영 서버(스테이징 또는 프로덕션)에서는 레지스트리에서 새로운 이미지를 `docker pull`하여 기존 컨테이너를 중단하고 새로운 컨테이너로 교체합니다.

이 모든 과정이 자동화되어 개발자는 코드 작성에만 집중할 수 있고, 운영팀은 안정적이고 예측 가능한 배포를 수행할 수 있게 됩니다. 이것이 바로 도커가 데브옵스의 핵심 기술로 자리 잡은 이유입니다.

도커의 여정은 여기서 멈추지 않습니다. 단일 호스트에서 여러 컨테이너를 관리하는 것을 넘어, 수십, 수백 대의 서버 클러스터에서 수천, 수만 개의 컨테이너를 효율적으로 관리하고 조율(Orchestration)하기 위한 기술로 자연스럽게 발전합니다. 바로 이 영역에서 쿠버네티스(Kubernetes)가 등장하며, 도커와 쿠버네티스는 현대 클라우드 네이티브 애플리케이션의 표준 아키텍처를 형성하고 있습니다. 도커의 기본 개념과 원리를 탄탄히 다지는 것은 결국 더 거대한 컨테이너 생태계로 나아가는 가장 중요한 첫걸음입니다.


0 개의 댓글:

Post a Comment