컨테이너 기술이란?
컨테이너는 애플리케이션을 실행하는 데 필요한 모든 요소(소스코드, 구성 파일, 라이브러리, 종속성 등)를 하나의 단위로 패키징한 논리적 실행 환경입니다. 이를 통해 어떤 환경에서도 일관되게 애플리케이션을 실행할 수 있으며, 배포와 운영이 용이해집니다.
컨테이너 실행 환경을 구성하는 단위를 이미지(Image)라고 부릅니다. 이미지는 도커에서는 Dockerfile을 통해 정의하며, 애플리케이션의 실행에 필요한 설정과 명령어들이 코드 형태로 담겨 있습니다. Dockerfile을 기반으로 이미지를 빌드하는 과정은 인프라 구성을 코드로 관리하는 Infrastructure as Code (IaC)에 해당합니다.
컨테이너는 호스트 운영체제의 커널을 공유하면서 실행되는 격리된 프로세스입니다. VM처럼 전체 운영체제를 가상화하는 것이 아니라, 운영체제 수준의 가상화를 통해 더 가볍고 빠르게 동작합니다. 단, 컨테이너 자체에는 커널이 없으며, 호스트의 커널을 공유합니다. 따라서 도커는 운영체제(OS)가 아닌, OS 위에서 실행되는 컨테이너 런타임 플랫폼에 가깝습니다.
이러한 격리성과 경량성 덕분에 컨테이너는 서로 영향을 주지 않는 독립적인(stateless) 환경을 제공합니다. 예를 들어, 컨테이너 내부의 서비스는 명시적으로 포트를 열지 않으면 외부에서 접근할 수 없기 때문에 보안성도 높습니다.
개발자는 더 이상 서버 세팅, OS 설치, 네트워크 구성 같은 반복적이고 복잡한 작업에 시간을 쓰지 않고, 애플리케이션 로직 개발에 집중할 수 있게 됩니다.
컨테이너의 특징
컨테이너는 애플리케이션 실행에 꼭 필요한 최소 구성만 포함한 이미지를 사용하기 때문에 경량입니다. Dockerfile은 이 이미지의 핵심으로, 컨테이너 실행에 필요한 모든 인프라 요소(런타임, 라이브러리 등)를 정의합니다.
컨테이너는 프로세스 수준에서 빠르게 실행 및 종료할 수 있고, 동시에 여러 개의 컨테이너를 실행하는 것도 가능합니다. Docker에서는 이러한 여러 컨테이너를 한 번에 구성하고 실행하기 위해 Docker Compose를 사용합니다.
또한 컨테이너는 OS, 클라우드, 로컬 환경 등 어떤 플랫폼에서도 동일하게 동작하는 높은 이식성을 지니고 있으며, 애플리케이션 단위로 격리되어 있어 지속적인 서버 관리 비용을 절감할 수 있습니다.
운영환경을 표준화할 수 있기 때문에, 개발팀(Dev)과 운영팀(Ops)의 역할을 분리하고, 각각의 작업에 집중할 수 있습니다. 이처럼 DevOps 워크플로우에 적합한 구조를 제공합니다.
컨테이너의 타입
컨테이너는 기능과 목적에 따라 시스템 컨테이너, 애플리케이션 컨테이너, 라우터 컨테이너 등으로 나뉩니다.
이 중 가장 대표적인 구분은 시스템 컨테이너 vs 애플리케이션 컨테이너입니다.
이 둘을 구분하는 핵심 기준은 컨테이너 내부의 PID 1번 프로세스, 즉 “누가 컨테이너의 주체인가”입니다.
- 리눅스에서 PID 1번은 init 프로세스(systemd 등)이며, 이는 운영체제의 핵심 프로세스입니다.
- 시스템 컨테이너는 PID 1번으로 OS 프로세스를 실행시켜, 가상 머신과 유사하게 전체 OS 환경을 구성할 수 있습니다.
- 반면, 애플리케이션 컨테이너는 하나의 애플리케이션 프로세스가 PID 1번이 되며, 애플리케이션 실행에 필요한 환경만 제공합니다.
시스템 컨테이너
시스템 컨테이너는 Ubuntu와 같은 리눅스 배포판 전체를 포함한 이미지로 실행되며, 마치 가상 머신처럼 여러 애플리케이션과 도구를 설치하고 사용할 수 있습니다. 대표적인 시스템 컨테이너 기술로는 LXC, LXD, OpenVZ, Linux VServer, BSD Jails 등이 있습니다.
애플리케이션 컨테이너
애플리케이션 컨테이너는 특정 단일 애플리케이션의 실행을 목적으로 설계되며, MSA 구조에서 흔히 사용됩니다. 예를 들어, 프론트엔드, 백엔드, 데이터베이스를 각각 컨테이너로 분리하여 3-tier 아키텍처를 구성할 수 있습니다. 대표적인 애플리케이션 컨테이너 런타임으로는 Docker, rkt(Rocket) 등이 있습니다.
도커란?
도커(Docker)는 컨테이너 이미지를 빌드하고 실행하며, 컨테이너의 수명 주기를 관리할 수 있게 도와주는 대표적인 컨테이너 런타임 도구입니다.
Docker는 애플리케이션 실행에 필요한 인프라(runtime 환경)를 이미지로 패키징하고, 이를 Docker Hub와 같은 플랫폼에서 공개 또는 비공개로 공유할 수 있도록 지원합니다. 이 과정은 GitHub에서 소스코드를 관리하는 방식과 유사합니다.
이미지를 기반으로 실행된 컨테이너는 독립된 환경에서 애플리케이션을 실행하며, 여러 계층의 애플리케이션을 컨테이너로 분리하고 연결하는 마이크로서비스 아키텍처(MSA)에서도 매우 유용하게 사용됩니다.
컨테이너 트렌드의 역사
- 1991~: Linux 프로세스 격리
- 2010~: VM 가상화 기술
- 2013~: Container 가상화 기술
- 2015~: Container Orchestration tool
가상화란?
가상화(Virtualization)는 서버, 스토리지, 네트워크, 애플리케이션 등 물리적인 리소스를 소프트웨어적으로 추상화하여 운영하는 기술입니다. 주요 목적은 자원의 효율적인 사용, 자동화된 IT 관리, 확장성, 빠른 장애 복구 등입니다.
가상화는 전통적으로 하이퍼바이저(Hypervisor) 기반의 VM(Virtual Machine) 가상화를 통해 이루어져 왔습니다. 이 방식에서는 물리적인 서버 위에 여러 개의 게스트 OS를 설치할 수 있고, 각 OS는 독립적인 애플리케이션 환경을 제공합니다. 대표적인 하이퍼바이저 도구로는 VMware, VirtualBox, KVM 등이 있습니다.
이러한 VM 기반 가상화는 유연하지만, 각 게스트 OS마다 별도의 커널과 시스템 자원을 요구하기 때문에 상대적으로 무겁고 느리며, 자원 소모가 큽니다.
컨테이너 가상화 vs VM 가상화
컨테이너 가상화와 VM 가상화는 모두 애플리케이션과 그 종속 요소를 패키징하여 격리된 환경에서 실행한다는 공통점을 가지고 있습니다. 하지만 이 두 방식은 가상화 계층과 리소스 효율성 측면에서 큰 차이를 보입니다.
- VM 가상화는 하이퍼바이저 위에 완전한 Guest OS를 설치하며, 각 OS는 커널을 포함합니다. 이 방식은 하드웨어 수준의 가상화를 통해 애플리케이션을 실행합니다. VM의 이미지 포맷은 주로 OVF(Open Virtualization Format) 등이 사용됩니다.
- 컨테이너 가상화는 호스트 OS의 커널을 공유하면서 동작하는 방식으로, VM에 비해 경량화된 운영체제 수준의 가상화를 구현합니다. 컨테이너 이미지 포맷은 OCI(Open Container Initiative) 표준을 따릅니다.
컨테이너는 별도의 부팅 단계 없이 바로 애플리케이션 실행 환경을 구성할 수 있어, 더 빠른 시작 속도, 높은 자원 효율성, 간편한 배포라는 이점을 제공합니다.
서버 vs 가상 머신 vs 컨테이너
애플리케이션의 배포 방식은 물리 서버 → 가상 머신 → 컨테이너 순으로 진화해왔습니다.
- 물리 서버는 애플리케이션이 직접 OS에서 실행되기 때문에 환경 간 이식성이 떨어지고, 리소스가 고정적이며 유연성이 낮습니다.
- 가상 머신은 하이퍼바이저를 통해 여러 OS를 하나의 물리 서버에서 동시에 운영할 수 있으나, 각각의 게스트 OS는 자체 커널을 가지고 있어 무겁고 리소스를 많이 소비합니다.
- 컨테이너는 OS 수준에서 가상화되며, 호스트 OS의 커널을 공유하기 때문에 훨씬 가볍고 빠르게 배포할 수 있습니다. 또한 애플리케이션 단위로 격리되므로 DevOps 및 마이크로서비스 환경에 적합합니다.
컨테이너화(Containerization)
컨테이너화는 애플리케이션과 실행 환경을 함께 패키징하여 어디서나 동일하게 실행될 수 있도록 만드는 기술입니다. 초기에는 LXC(Linux Containers)를 통해 시스템 수준의 컨테이너화를 구현했으며, 이는 리눅스 커널의 cgroups, namespaces 기능을 활용한 OS 수준 가상화였습니다.
이후 2013년 Docker가 등장하면서, 애플리케이션 단위의 컨테이너화가 본격화되었습니다. Docker는 초기에는 내부적으로 LXC를 사용했으나, 이후 독립적인 런타임 구성요소로 발전했습니다:
- runC: 컨테이너 생성 및 실행을 위한 표준 런타임 (Open Container Initiative에서 관리)
- containerd: 컨테이너의 생명주기(시작, 정지, 삭제 등)를 관리하는 데몬
- dockerd: 사용자 명령을 수신하고 containerd로 전달하는 Docker 데몬
즉, Docker는 단순한 도구가 아니라 여러 컴포넌트(runC, containerd, dockerd)가 유기적으로 연결된 아키텍처입니다.
데몬(Daemon)이란?
- 데몬은 항상 대기 상태에서 요청을 처리하는 백그라운드 프로세스
dockerd의 역할
dockerd는 Docker의 핵심 데몬 프로세스로, 사용자 요청을 수신하고 containerd 및 기타 하위 컴포넌트에 전달하는 역할을 합니다. 다음과 같은 기능을 포함합니다:
- docker CLI 처리: 사용자의 명령어를 수신하고 파싱
- SwarmKit: Docker의 내장 오케스트레이션 기능
- 로그 관리 (logs mgmt): 컨테이너 로그 수집 및 출력
- 스토리지 관리 (storage mgmt): 볼륨, 이미지 저장소 관리
- libnetwork: 컨테이너 간 네트워크 구성 및 통신
- buildkit: 고성능 이미지 빌더
- DCT (Docker Content Trust): 이미지 서명 및 검증 기능
- 이미지 관리 (image mgmt): 이미지 생성, 삭제, 레지스트리 연동 등
간단한 도커 실습
centos 이미지 기반 실습
1. centOS 7 이미지 받아오기
docker pull centos:7
2. 받아온 이미지 기반으로 컨테이너 실행
docker run -it --name=sys-container-1 centos:7 echo 'welcome centOS!'
위의 명령어로 실행하면 centos:7 이미지를 기반으로 새 컨테이너를 생성하고, 컨테이너 내부에서 `echo 'welcome centOS!'`명령을 실행한다. 메시지를 출력하고 나서 작업이 끝났기 때문에 컨테이너가 바로 종료된다.
실행 결과
welcome CentOS가 잘 출력되었다.
실제로 컨테이너가 출력 후 종료되었는지 확인해보자
3. 도커 컨테이너 확인하기
docker ps -a
실행 결과
그렇다면 bash 쉘로 컨테이너 내부의 bash 환경으로 들어가보자.
4. 이미 만들어진 컨테이너 삭제
docker rm sys-container-1
5. centOS:7 컨테이너를 bash 환경으로 실행하기
docker run -it --name=sys-container-1 centos:7 bash
OS 환경 확인해보기
cat /etc/os-release
실행 결과
exit 명령어로 종료하기 > bash 쉘을 종료하면서 컨테이너도 같이 종료된다.
6. 이미 생성된 컨테이너에 bash 환경으로 진입하기
exit 명령어로 종료한 경우, 컨테이너는 stop 상태가 되어버리므로 start로 컨테이너를 시작한 후에 진입해야 한다.
docker start sys-container-1
docker exec -it sys-container-1 bash
컨테이너를 종료하지 않고, 빠져나오는 방법은?
ctrl + p + q. > 이렇게 종료를 하면 컨테이너는 백그라운드에서 계속 실행되고, 터미널 세션만 분리된다.
nginx 이미지 기반 실습
이번에는 nginx 이미지를 기반으로 실습을 진행해보도록 하자.
1. nginx 이미지 받아오기
docker pull nginx:1.25.0-alpine
이러한 이미지는 Dockerfile로 만들어야 한다. 실제로 nginx 이미지를 빌드한 Dockerfile 링크를 확인해볼 수도 있다.
2. 이미지 히스토리를 확인해보도록 하자.
docker image history nginx:1.25.0-alpine
그 결과 다음과 같은 결과를 받아볼 수 있다.
IMAGE CREATED CREATED BY SIZE COMMENT
2e776a66a355 23 months ago /bin/sh -c set -x && apkArch="$(cat /etc… 30.9MB
<missing> 23 months ago /bin/sh -c #(nop) ENV NJS_VERSION=0.7.12 0B
<missing> 23 months ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
<missing> 23 months ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
<missing> 23 months ago /bin/sh -c #(nop) EXPOSE 80 0B
<missing> 23 months ago /bin/sh -c #(nop) ENTRYPOINT ["/docker-entr… 0B
<missing> 23 months ago /bin/sh -c #(nop) COPY file:e57eef017a414ca7… 16.4kB
<missing> 23 months ago /bin/sh -c #(nop) COPY file:36429cfeeb299f99… 12.3kB
<missing> 23 months ago /bin/sh -c #(nop) COPY file:5c18272734349488… 12.3kB
<missing> 23 months ago /bin/sh -c #(nop) COPY file:7b307b62e82255f0… 8.19kB
<missing> 23 months ago /bin/sh -c set -x && addgroup -g 101 -S … 7.12MB
<missing> 23 months ago /bin/sh -c #(nop) ENV PKG_RELEASE=1 0B
<missing> 23 months ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.25.0 0B
<missing> 2 years ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
<missing> 2 years ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 2 years ago /bin/sh -c #(nop) ADD file:e51d4089e73ad6dee… 8.14MB
여기서 `EXPOSE 80`의 뜻은 해당 컨테이너가 내부적으로 80번 포트를 사용한다는 뜻이며, 이 포트를 외부(호스트)에서 접근 가능하게 하려면 `docker run` 시에 `-p` 또는 `-publish` 옵션으로 포트를 바인딩해야 한다.
3. 이미지를 실행해보도록 하자.
docker run -d -p 8001:80 --name=webserver1 nginx:1.25.0-alpine
4. 실행 중인 컨테이너를 확인해보기
docker ps -a
// 결과
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e412942e2714 nginx:1.25.0-alpine "/docker-entrypoint.…" 37 seconds ago Up 36 seconds 0.0.0.0:8001->80/tcp webserver1
PORTS를 보면 `0.0.0.0:8081->80/tcp`로 되어있는데 이는 호스트와 컨테이너 간 포트 매핑을 의미한다.
호스트의 모든 ip 주소에서 8001번 포트로 들어오는 요청이 컨테이너 내부의 80번 포트로 전달된다는 뜻이다.
nginx가 동작하는지 확인해보도록 하자.
curl localhost:8001
// 결과
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
5. nginx에 사용자 정의 Index.html로 교체하기
linux의 `scp` 명령어처럼 도커에서도 외부의 파일을 컨테이너 내부의 파일에 복사하는 방법이 있다.
변경하고자 하는 index.html 파일을 만든 후 다음 명령어를 통해 nginx에서 사용 중인 html을 교체해줄 수 있다.
docker cp index.html webserver1:/usr/share/nginx/html/index.html
http://localhost:8001에 접속해보도록 하자.
이번에는 이미지를 배경으로 한 html을 옮겨주도록 해보자.
docker cp index2.html webserver1:/usr/share/nginx/html/index.html
docker cp docker_logo.png webserver1:/usr/share/nginx/html/docker_logo.png
도커 이미지를 배경으로 한 html이 정상적으로 작동되게 된다.
웹소스를 보유한 도커 이미지 만들기
앞에서는 nginx 이미지 기반의 컨테이너에 직접 사용자 정의 index.html과 필요한 웹 리소스를 cp 명령어를 통해 넣어줬는데 이번에는 리소스를 보유한 이미지를 만들어보도록 하자.
1. Dockerfile을 만들기
FROM nginx:1.25.0-alpine
COPY index2.html /usr/share/nginx/html/index.html
COPY docker_logo.png /usr/share/nginx/html/docker_logo.png
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
- nginx:1.25.0-alpine 이미지를 베이스 이미지로 한다.
- 2개의 리소스(index2.html, docker_logo.png)를 복사한다.
- 80번 포트를 열어준 다음, nginx daemon을 foreground로 실행한다.
nginx가 백그라운드로 실행되어야 하는 이유
nginx가 백그라운드로 실행되면 메인 프로세스가 끝나버려 컨테이너가 바로 꺼지게 된다.
컨테이너는 격리된 환경 + 하나의 메인 프로세스(PID 1)과 같은 구조로 동작하게 된다.
즉, 도커는 컨테이너를 실행할 때 내부에서 가장 처음 실행되는 메인 프로세스(PID 1)가 종료되면 컨테이너 전체를 종료시킨다.
nginx는 리눅스 서버에서 메인 프로세스를 백그라운드로 fork 하고 메인 프로세스(PID 1)를 종료시킨다.
nginx # ← 내부적으로 fork 후, 부모 종료
└── nginx # ← 백그라운드에서 살아있는 워커 프로세스
이 구조는 Docker에서 컨테이너를 바로 종료시키는 원인이 된다. 부모 (PID 1)이 종료되면 Docker는 컨테이너 안에 메인 프로세스가 없다고 판단하여 컨테이너 자체를 종료시킨다.
2. Dockerfile 기반으로 이미지 빌드하기
docker build -t myweb:v1.0 .
빌드된 이미지 확인해보기
docker build -t myweb:v1.0 .
REPOSITORY TAG IMAGE ID CREATED SIZE
myweb v1.0 4fa889faddeb 30 seconds ago 62.7MB
3. 빌드된 이미지 기반으로 도커 컨테이너를 실행하기
docker run -d --name=webserver2 -p 8002:80 myweb:v1.0
localhost:8002으로 접속해보면 이전과 동일한 결과를 확인할 수 있다.
mysql 이미지 기반 실습
1. mysql 이미지 pull
docker pull mysql:8.4.5
이미지 확인
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mysql 8.4.5 2764fe573c51 3 weeks ago 1.06GB
2. 이미지 기반 컨테이너 실행하기
docker run -it -e MYSQL_ROOT_PASSWORD=pass123# mysql:8.4.5 /bin/bash
- -e 옵션은 환경변수를 설정하는 것이고, `MYSQL_ROOT_PASSWORD`는 필수 환경변수이다.
이제 bash 환경으로 접속하였다.
3. mysql 실행하기
cat /etc/os-release
// 결과
NAME="Oracle Linux Server"
VERSION="9.5"
ID="ol"
ID_LIKE="fedora"
VARIANT="Server"
VARIANT_ID="server"
VERSION_ID="9.5"
PLATFORM_ID="platform:el9"
PRETTY_NAME="Oracle Linux Server 9.5"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:oracle:linux:9:5:server"
HOME_URL="https://linux.oracle.com/"
BUG_REPORT_URL="https://github.com/oracle/oracle-linux"
ORACLE_BUGZILLA_PRODUCT="Oracle Linux 9"
ORACLE_BUGZILLA_PRODUCT_VERSION=9.5
ORACLE_SUPPORT_PRODUCT="Oracle Linux"
ORACLE_SUPPORT_PRODUCT_VERSION=9.5
우리는 /bin/bash로 컨테이너를 실행했으므로 원래 실행되었어야 할 mysqld 명령을 무시하고, /bin/bash를 실행하도록 override 했으므로, docker-entrypoint.sh 스크립트가 실행되지 않게 되었다. (즉, 단순히 bash 쉘만 켜진 “정적인” 컨테이너 상태이다)
다음 명령어로 컨테이너 내부에서 mysql을 실행해주도록 하자.
rm -rf /var/lib/mysql/* (오류가 있는 경우에 실행)
mysqld --initialize-insecure --user=mysql
mysqld &
새 쉘을 하나 더 띄워서 mysql을 실행한 컨테이너로 접속하자.
docker exec -it faed7a4dc511 bash
# mysql 접속하기
mysql -uroot
# 비밀번호 설정하기
ALTER USER 'root'@'localhost' IDENTIFIED BY 'pass123#';
FLUSH PRIVILEGES;
그 다음부터는 mysql 관련 명령을 실행해볼 수 있게 된다.
# DB 확인
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)
# DB 생성
mysql> create database dockerdb;
Query OK, 1 row affected (0.01 sec)
# DB 접속
mysql> use dockerdb;
Database changed
# 테이블 생성
mysql> create table t1 (a int, b varchar(10));
Query OK, 0 rows affected (0.02 sec)
# 데이터 삽입
mysql> insert into t1 values (10, 'docker');
Query OK, 1 row affected (0.01 sec)
# 데이터 조회
mysql> select * from t1;
+------+--------+
| a | b |
+------+--------+
| 10 | docker |
+------+--------+
1 row in set (0.01 sec)
# 종료
mysql> exit
Bye
도커 컨테이너를 exit 해서 나온 경우 다시 해당 컨테이너로 재접속 하려면 run 명령어가 아닌 start + exec 명령으로 접속해야 한다.
docker start faed7a4dc511
docker exec -it faed7a4dc511 bash
그 이후에 해당 컨테이너에서 mysql을 다시 실행한 뒤 접속을 해서 db를 확인해보면 기존에 만들어두었던 database가 존재함을 확인할 수 있다.
# mysql 실행
mysqld &
# mysql 접속
mysql -uroot -p
# DB 확인
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| dockerdb |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.01 sec)
# DB 접속
mysql> use dockerdb;
# 테이블 확인
mysql> show tables;
+--------------------+
| Tables_in_dockerdb |
+--------------------+
| t1 |
+--------------------+
1 row in set (0.00 sec)
만약, [Server] Plugin mysqlx reported: 'Setup of socket: '/var/run/mysqld/mysqlx.sock' failed, another process with PID 48 is using UNIX socket file' 오류가 발생한 경우 아래의 명령어로 socket을 삭제하고 실행해주도록 하자.
bash-5.1# rm -f /var/run/mysqld/mysqld.sock
bash-5.1# rm -f /var/run/mysqld/mysqlx.sock