도커(Docker) 입문편 컨테이너 기초부터 서버 배포까지
들어가며
도커(Docker)는 2013년에 등장한 컨테이너 기반 가상화 도구입니다. 파이콘 US 2013(Pycon US 2013)에서 솔로몬 하이크스(Solomon Hykes)는 The Future of Linux Container라는 라이트닝 토크에서 도커를 처음 소개했습니다.* 2025년 현재 도커와 컨테이너는 클라우드에서 서버를 배포하는 사실상의 표준 형식이 되었습니다.
* 현재 도커는 Go 언어로 개발되고 있으며, 2025년 9월 현재 최신 버전은 28.x입니다.
도커는 리눅스 상에서 컨테이너 방식으로 프로세스를 격리해서 실행하고 관리할 수 있도록 도와주며, 계층화된 파일 시스템에 기반해 효율적으로 이미지(프로세스 실행 환경)를 구축할 수 있도록 해줍니다. 도커를 사용하면 이 이미지를 기반으로 컨테이너를 실행할 수 있으며, 다시 특정 컨테이너의 상태를 변경해 이미지로 만들 수 있습니다. 이렇게 만들어진 이미지는 파일로 보관하거나 원격 저장소를 사용해 쉽게 공유할 수 있으며, 도커만 설치되어 있다면 필요할 때 언제 어디서나 컨테이너로 실행하는 것이 가능합니다.
아직은 이 이야기들이 어렵게 느껴질 수 있지만, 이 글을 읽고나면 이미지와 컨테이너, 그리고 도커의 주요 개념들을 이해할 수 있습니다. 그리고 하나 더, 도커를 사용해서 실제로 서버를 배포까지 해봅니다.
도커를 왜 사용해야하는지 궁금하신 분들은 아래 글도 읽어보세요.
도커(Docker) 시작하기: macOS에서 Rocky Linux로 프로세스 실행하기
사용자용 가상머신(Virtual Machine)은 이제 대중적인 도구가 되었습니다. 가상머신을 실행하는 호스트 머신에 가상화된 하드웨어와 OS를 구축함으로써, 호스트와는 다른 환경을 구축하고 개발과 테스트를 비롯한 다양한 목적으로 사용할 수 있습니다. 하드웨어를 소프트웨어로 에뮬레이션하는 가상머신은 시스템 분리를 통한 프로세스 격리 관점에서는 아주 후한 점수를 줄 수 있지만, 단순히 프로세스를 실행하기 위한 환경으로서는 성능을 비롯한 여러 단점을 가지고 있습니다.
컨테이너는 하드웨어를 소프트웨어로 재구현하는 가상화(= 가상머신)와는 달리 프로세스의 실행 환경을 격리합니다. 컨테이너가 실행되고 있는 호스트 입장에서 컨테이너는 단순히 프로세스에 불과합니다만, 사용자나 컨테이너 입장에서는 호스트와는 격리되어 동작하는 가상머신처럼 보입니다. 그래서 컨테이너형 가상화라고 부르기도 합니다.
간단한 예를 살펴보겠습니다. 현재 macOS에서 작업 중이라고 가정하고, Rocky Linux 환경을 구축하는데 시간이 얼마나 걸릴까요?
먼저 현재 시스템을 확인해보겠습니다.
$ uname -s
Darwin # macOS
이미 도커가 설치되어 있는 환경이라면 명령어 하나로 Rocky Linux 머신을 실행할 수 있습니다.
Rocky Linux란?
Rocky Linux는 2020년 말 CentOS 프로젝트가 종료되면서 등장한 RHEL(Red Hat Enterprise Linux) 호환 배포판입니다. CentOS의 원 개발자가 만든 공식 후속 프로젝트로, 기업 환경에서 안정적으로 사용할 수 있는 무료 엔터프라이즈 리눅스입니다. 이 튜토리얼에서는 macOS/Windows와 다른 리눅스 환경을 체험하기 위한 예제로 Rocky Linux를 사용합니다.
docker로 다음 명령어를 실행해봅니다.
$ docker run --rm rockylinux:9 cat /etc/rocky-release
다음과 같이 출력이 나옵니다.
Unable to find image 'rockylinux:9' locally
9: Pulling from library/rockylinux
4c81ef64b3e1: Pull complete
Digest: sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6
Status: Downloaded newer image for rockylinux:9
Rocky Linux release 9.3 (Blue Onyx)
명령어가 길지만 겁먹을 필요는 없습니다. 먼저 명령어 구조가 조금 복잡해보입니다만, rockylinux:9
이미지로부터 cat /etc/rocky-release
명령어를 실행하라는 단순한 명령어입니다. 아직 이미지가 없다면, 도커의 공식 저장소에서 이 이미지를 pull 받아옵니다. 다음 내용은 이미지를 다운로드가 끝났다는 의미입니다.
Status: Downloaded newer image for rockylinux:9
그리고 마지막 줄은 cat /etc/rocky-release
를 실행한 결과입니다.
Rocky Linux release 9.3 (Blue Onyx)
안정적인 네트워크 환경에서는 Rocky Linux 이미지를 받는 데 채 1분이 걸리지 않습니다.*
* `--rm` 플래그는 컨테이너가 종료되면 자동으로 삭제하라는 의미입니다. 일회성 실행에 유용합니다.
다시 실행하면? 거의 바로 결과가 나옵니다.
$ docker run --rm rockylinux:9 cat /etc/rocky-release
Rocky Linux release 9.3 (Blue Onyx)
우리는 분명히 맥을 사용중인데요, 어떤 마법이 일어진 걸까요?
자 이제 이 마법을 직접 펼쳐보시기 바랍니다.
이 튜토리얼에서는 컨테이너의 원리보다는 도구로서 도커를 사용하는 법에 집중적으로 소개합니다. 원리적인 부분도 중요하지만 이 도구를 실제로 활용하려면 컨테이너와 이미지의 차이를 이해해야 하고, Dockerfile
을 사용해 이미지 만드는 법을 익힐 필요가 있습니다.
도커(Docker) 설치하고 기본적인 설정하기
도커는 다양한 환경에서 사용할 수 있습니다. 2025년 현재, 개발 환경에서는 주로 Docker Desktop을 사용합니다.* Docker Desktop은 도커 엔진과 관련 도구들을 포함한 올인원 패키지로, GUI 관리 도구와 Kubernetes까지 포함되어 있습니다.
먼저 자신의 운영체제를 찾아서 설치를 진행해주세요.
* Docker Desktop은 개인 사용, 교육, 오픈소스 프로젝트에는 무료입니다. 하지만 일정 규모를 넘는 기업에서 사용하는 경우 유료 구독이 필요할 수 있습니다.
맥OS 도커 설치 방법
도커 데스크탑은 공식 웹사이트에서 운영체제 별 버전을 다운로드 받을 수 있습니다.
macOS에서는 Docker Desktop을 다운로드 받아 설치하는 것이 가장 간단합니다. 공식 웹사이트에서 다운로드하거나, Homebrew를 사용해서 설치할 수 있습니다.
# Homebrew를 사용한 설치
$ brew install --cask docker
이 튜토리얼을 실행한 환경에서는 Docker Desktop이 아니라 Orbstack을 사용하고 있습니다. Orbstack은 Docker 엔진을 제공하고, Docker와 인터페이스가 호환되는 맥 전용 애플리케이션입니다. Docker Desktop과 어느 쪽을 사용해도 무방합니다. Orbstack도 Homebrew로 설치할 수 있습니다.
$ brew install --cask orbstack
리눅스 도커 설치 방법
리눅스 환경에서도 Docker Desktop을 사용할 수 있습니다. 공식적으로는 Ubuntu, Debian, RHEL, Fedora를 지원하고 있습니다.
Docker는 리눅스 커널의 기능을 사용하기 때문에, 리눅스에서는 Docker Engine을 직접 설치할 수도 있습니다. 다음 명령어로 한 방에 Docker Engine을 설치할 수 있습니다.
$ curl -fsSL https://get.docker.com | sh
Docker Engine 설치에 대한 더 자세한 내용은 다음 공식 문서를 참고해주세요.
설치가 완료되면 도커가 정상적으로 설치되었는지 확인합니다.
$ docker --version
Docker version 28.3.3, build 980b856
2025년 9월 현재 최신 버전인 28.x 버전이 설치되었습니다.
Docker Desktop vs Docker Engine
Docker Desktop은 Docker Engine을 포함한 올인원 패키지로, GUI 관리 도구, Docker Compose, Kubernetes 등이 포함되어 있습니다. macOS와 Windows에서는 가상머신을 통해 Linux 커널 기능을 제공합니다.
Docker Engine은 컨테이너를 실행하는 핵심 런타임으로, Linux에서 직접 설치할 수 있습니다. CLI만 제공되며 추가 도구는 별도로 설치해야 합니다.
개발 환경에서는 Docker Desktop이 편리하고, 서버 환경에서는 Docker Engine만 설치하는 것이 일반적입니다.
Linux에서 Docker 권한 설정하기
Linux에서는 처음 도커를 설치한 후
docker ps
명령어를 실행하면 권한 에러가 발생할 수 있습니다.$ docker ps Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/containers/json: dial unix /var/run/docker.sock: connect: permission denied
일반적으로 이러한 에러가 발생하는 이유는 사용자에게 도커 소켓에 접근할 권한이 없기 때문입니다.
관리자 권한이 있는 경우 명령어 앞에
sudo
를 붙이면 정상적으로 실행됩니다.$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
하지만 매번
sudo
를 입력하는 것은 번거로우니, 사용자 계정에서도 도커를 직접 사용할 수 있도록docker
그룹에 사용자를 추가해줍니다. 여기서$USER
는 현재 사용자를 의미합니다.$ sudo usermod -aG docker $USER $ newgrp docker
이제
sudo
명령어 없이도 도커 명령어를 바로 사용할 수 있습니다.$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
아직 컨테이너를 실행하지 않아서 목록에 출력되는 것은 없지만 정상적으로 실행되는 것을 확인할 수 있습니다.
도커 이미지(Docker Image) 기초
이미지와 컨테이너는 도커를 이해하는 데 있어 가장 중요한 개념입니다. 여기서는 먼저 이미지 개념에 대해서 같이 살펴보겠습니다. 이미지는 가상머신에서 사용하는 이미지와 비슷한 역할을 합니다. 한 마디로 정의해보자면 이미지는 어떤 애플리케이션을 실행하기 위한 환경이라고 할 수 있습니다. 그리고 이 환경은 파일들의 집합입니다. 도커에서는 애플리케이션을 실행하기 위한 파일들을 모아놓고, 애플리케이션과 함께 이미지로 만들 수 있습니다. 그리고 이 이미지를 기반으로 애플리케이션을 바로 실행할 수 있습니다.
처음에 간단히 살펴본 예제에서는 rockylinux
컨테이너를 실행했습니다. 이 과정을 좀 더 풀어써보면 다음과 같습니다.
- 도커 레지스트리에서
rockylinux
이미지를 pull 받는다. - 이 이미지를 통해서 컨테이너를 실행한다.
이번에는 이 과정을 나눠서 진행해보겠습니다. 먼저 1의 이미지를 pull 받는 부분만을 진행합니다. 이제 막 도커를 설치했으니 이미지가 없다는 것을 확인해봅니다.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
아무것도 출력되지 않습니다. 그렇다면 ubuntu
이미지를 도커 레지스트리 상에서 pull 받아보겠습니다. 도커에서는 docker pull <IMAGE_NAME>
명령어로 이미지를 pull 받을 수 있습니다.
$ docker pull ubuntu:24.04
24.04: Pulling from library/ubuntu
9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8: Pull complete
Digest: sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8
Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04
이미지 이름은 :
을 구분자로 이미지 이름과 태그로 구분됩니다. 태그를 지정하지 않으면 기본값으로 latest
가 사용됩니다. 따라서 ubuntu
는 ubuntu:latest
와 같습니다. 도커는 먼저 이 이미지를 로컬에서 찾아보고, 찾을 수 없으면 도커 공식 저장소에서 찾아봅니다. 저장소에 해당하는 이미지가 존재하면 이미지를 pull 받아옵니다. 마지막 줄의 메시지로부터 ubuntu:24.04
이미지가 다운로드된 것을 알 수 있습니다.
여기서도 알 수 있는 도커의 가장 큰 매력 중 하나는 별다른 설정 없이 곧바로 공식 저장소를 통해서 이미지를 받아올 수 있다는 점입니다. 리눅스의 apt
, dnf
나 프로그래밍 언어의 npm
, pip
, gem
같은 패키지 매니저에 익숙하다면 이 장점을 어렵지 않게 이해하실 수 있을 것입니다.
한 가지 재미있는 점은 pull
이라는 명령어 이름입니다. 도커에서는 이미지를 다운 받을 때 install
이나 download
와 같은 명령 대신 pull
을 사용합니다. 앞으로 살펴보겠지만 이는 단순히 이미지를 다운로드 받는 데서만 그런 것은 아닙니다. 이미지를 업로드 할 때는 push
라는 명령어를 쓰고, 새로운 이미지를 생성할 때는 commit
, 이미지의 차이를 확인할 때는 diff
라는 명령어를 사용합니다. 이러한 명령어 이름은 Git에서 사용하는 서브커맨드로 개발자들에게는 친숙한 이름들입니다. 기능적으로는 이미지를 다운로드 받아온다고 이해해주시기 바랍니다.*
* `latest` 태그는 꼭 최신 버전을 의미하는 것은 아닙니다. 이미지 제작자가 지정한 기본 태그일 뿐입니다. 프로덕션에서는 명시적인 버전 태그(예: `ubuntu:24.04`)를 사용하는 것이 좋습니다.
앞서 사용해본 image ls
명령어로 다운 받은 이미지를 확인해보겠습니다.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 24.04 59a383896b55 4 weeks ago 78.1MB
ubuntu:24.04
이미지가 추가된 것을 볼 수 있습니다. 앞서 pull
명령어가 VCS의 명령어와 비슷한 것처럼 도커에서는 하나의 이미지를 저장소repository라고 부릅니다. TAG
는 임의로 붙여진 추가적인 이름입니다. 일반적으로 이미지의 버전을 구분하기 위해서 사용됩니다. 앞서 이야기했듯이 도커 명령어에서 이미지를 지정할 때 태그를 생략하면 latest
가 사용됩니다. IMAGE ID
는 이미지를 가리키는 고유한 해시 값입니다. CREATED
는 이미지가 생성된 시간, SIZE
는 이미지의 용량을 나타냅니다.
docker image ls
는 앞으로 자주 사용하게 될 명령어이니 꼭 기억해두시기 바랍니다.
도커 허브(Docker Hub) - 공식 이미지 레지스트리
도커 허브(Docker Hub)는 도커에서 제공하는 이미지 호스팅 서비스입니다. 이미지를 저장하고 공유할 수 있는 중앙 저장소 역할을 합니다.
앞서 ubuntu 이미지를 pull 받을 때 마지막 줄을 다시 봅시다.
docker.io/library/ubuntu:24.04
docker.io
가 도커의 기본 레지스트리이며, 이미지를 pull 할 때 별도의 레지스트리를 지정하지 않으면 자동으로 이 도커 허브에서 이미지를 찾습니다.
이미지와 관련된 정보는 도커 허브에서 확인할 수 있습니다.
도커 허브에는 아주 많은 이미지들이 등록되어 있습니다. 이 이미지들은 도커에서 공식적으로 제공하는 이미지와 사용자들이 직접 만들어 올린 이미지들로 나눠집니다.
도커에서는 Ubuntu, Rocky Linux, AlmaLinux와 같은 리눅스 운영체제 이미지와 MySQL, PostgreSQL, Redis, Node.js와 같이 자주 사용되는 애플리케이션에 대한 공식 이미지를 제공하고 있습니다. 일반적으로 도커가 제공하는 공식 이미지에는 네임스페이스가 없습니다.
컨테이너(Container) 이해하기 - 격리된 환경에서 실행되는 프로세스
앞서 rockylinux
컨테이너를 실행하는 것은 다음과 같은 두 단계를 거친다고 이야기했습니다.
- 도커 레지스트리에서 rockylinux 이미지를 pull 받아서 로컬로 다운로드 받는다.
- 이 이미지를 통해서 컨테이너를 실행한다.
이번에는 두 번째 단계에 대해서 좀 더 자세히 다뤄보겠습니다. 이미지는 어떤 환경이 구성되어있는 상태를 저장해놓은 파일 집합이라고 이야기했습니다. 바로 이 이미지의 환경 위에서 특정한 프로세스를 격리시켜 실행한 것을 컨테이너라고 부릅니다. 컨테이너를 실행하려면 반드시 이미지가 있어야합니다.
다시 한 번 정리합니다. 이미지는 파일들의 집합이고, 컨테이너는 이 파일들의 집합 위에서 실행된 특별한 프로세스입니다.
컨테이너는 docker run
을 통해서 실행할 수 있습니다. 여기서는 셸을 실행하기 위해서 docker run <이미지이름:태그> <명령어>
형태로 Ubuntu 컨테이너에서 간단한 명령어를 실행해보겠습니다.
$ docker run --rm ubuntu:24.04 cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=24.04
DISTRIB_CODENAME=noble
DISTRIB_DESCRIPTION="Ubuntu 24.04.1 LTS"
이 예제에서는 ubuntu:24.04
이미지로부터 컨테이너를 생성했고, 이 격리된 환경에서 cat /etc/lsb-release
라는 명령어로 컨테이너를 실행했습니다. 실행 결과로 Ubuntu 버전 정보가 출력되었습니다.
이제 새로운 명령어를 하나 배워보도록 하겠습니다. 앞서 사용가능한 이미지들을 확인하는 명령어는 docker image ls
였습니다. 이번에 사용할 명령어는 현재 실행중인 컨테이너 목록을 출력하는 명령어 docker container ls
입니다.* 위의 명령어를 실행한 후 바로 실행해보겠습니다.
* `docker ps`로도 같은 결과를 볼 수 있지만, Docker는 2017년부터 리소스 타입을 명시하는 새로운 명령어 체계를 권장합니다. `docker container ls`가 `docker ps`보다 더 명확하게 '컨테이너 목록'임을 나타냅니다.
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
컨테이너 목록이 비어있는 것을 볼 수 있습니다. 왜냐하면 cat
명령어가 실행되고 바로 종료되었기 때문입니다. 종료된 컨테이너까지 보기 위해서는 -a
옵션을 사용해야합니다. 확인해보겠습니다.
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8f3d5c2a1b4e ubuntu:24.04 "cat /etc/lsb-release" 10 seconds ago Exited (0) 9 seconds ago inspiring_turing
10초 전에 만들어진 8f3d5c2a1b4e
컨테이너가 9초 전에 종료된 것을 알 수 있습니다. container ls
명령어의 실행 결과로부터 실행했던 컨테이너 정보를 확인할 수 있습니다. 이 예제에서는 ubuntu:24.04
이미지로부터 컨테이너를 생성했고, 이 격리된 환경에서 cat /etc/lsb-release
라는 명령어로 컨테이너를 실행했습니다. 그 외에도 맨 앞의 컨테이너 ID는 앞으로 도커에서 컨테이너를 조작할 때 사용하는 ID이기 때문에 필수적으로 알아둘 필요가 있습니다. 마지막 컬럼은 임의로 붙여진 컨테이너의 이름입니다.
여기서 중요한 점은 컨테이너는 SSH 서버가 아니라 cat
프로세스이기 때문에, 명령어가 종료 되면 컨테이너도 종료된다는 점입니다. 컨테이너란 사실 프로세스에 불과합니다. 이 명령어가 종료되면 컨테이너도 종료 상태(Exit)에 들어갑니다.
이제 좀 더 오래 실행되는 컨테이너를 만들어보겠습니다. 이번에는 nginx 웹 서버를 컨테이너로 실행해봅시다.
$ docker run -d --name my-nginx nginx:alpine
f5db775a0e676b555e753f5300c58989c032cde5569c325df6e45ecec4eaaa5a
여기서 사용한 -d
옵션은 백그라운드에서 실행하라는 의미입니다 (detached mode).* 이 옵션을 사용하지 않으면 컨테이너가 포그라운드에서 실행되어 터미널을 차지하게 됩니다. 웹 서버처럼 계속 실행되어야 하는 서비스는 보통 -d
플래그와 함께 실행합니다.
--name my-nginx
는 컨테이너에 이름을 지정하는 옵션입니다. 이름을 지정하지 않으면 Docker가 임의의 이름을 자동으로 부여합니다. 이름이 있으면 나중에 컨테이너를 조작할 때 긴 ID 대신 이름을 사용할 수 있어서 편리합니다.
마지막으로 nginx:alpine
은 사용할 이미지 이름입니다. Alpine Linux 기반의 경량 nginx 이미지로, 일반 nginx 이미지보다 용량이 작아서 빠르게 다운로드할 수 있습니다.
* detached mode는 "분리된 모드"라는 뜻으로, 컨테이너가 터미널과 독립적으로 백그라운드에서 실행됨을 의미합니다.
이제 실행 중인 컨테이너를 확인해보겠습니다.
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f5db775a0e67 nginx:alpine "/docker-entrypoint.…" 5 seconds ago Up 4 seconds 80/tcp my-nginx
5초 전에 만들어진 컨테이너가 실행되고 있는 것을 알 수 있습니다. 위의 예제에서는 직접 명령어를 넘겨서 이미지를 컨테이너로 실행시켰습니다만, 보통 이미지들은 명령어 기본값이 지정되어 있습니다. 예를 들어 Redis, MariaDB, nginx 애플리케이션을 담고 있는 이미지라면, 기본적으로 각각의 애플리케이션을 실행하거나 애플리케이션을 실행하는 스크립트가 기본 명령어로 지정되어 있습니다. 여기서 nginx:alpine은 nginx 웹 서버를 실행하는 것이 기본 명령어로 설정되어 있습니다.
컨테이너를 조작할 때는 컨테이너 ID를 사용할 수도 있고, 이름을 사용할 수도 있습니다. 이름은 docker run
을 할 때 --name
으로 옵션을 사용해 명시적으로 지정할 수 있지만, 지정하지 않으면 임의의 이름이 자동적으로 부여됩니다.
컨테이너를 종료하려면 docker stop
명령어를 사용합니다.
$ docker stop my-nginx
my-nginx
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
컨테이너가 종료되었습니다. 종료된 컨테이너를 삭제하려면 docker container rm
명령어를 사용합니다.
$ docker container rm my-nginx
my-nginx
컨테이너는 가상머신이 아니라 프로세스
이쯤에서 이미지와 컨테이너를 명확하게 짚고 넘어가겠습니다.
- 이미지: 미리 구성된 환경을 저장해 놓은 파일들의 집합 (템플릿)
- 컨테이너: 이미지를 기반으로 실행된 격리된 프로세스 (인스턴스)
이미지는 가상머신 이미지와 비슷합니다. 하지만 가상머신에서는 저장된 이미지를 기반으로 가상머신을 특정 상태로 복원합니다. 컨테이너는 가상머신처럼 보이지만 가상머신은 아닙니다. 가상머신이 컴퓨터라면, 컨테이너는 단지 격리된 프로세스에 불과합니다.
보통 도커 컨테이너를 처음 다루는 예제에서 셸을 사용하는 경우가 많아서 컨테이너를 가상머신이라고 생각하기 쉽습니다. 다시 한 번 강조합니다. 컨테이너는 가상머신이라기보다는 프로세스입니다. 이 사실을 꼭 기억해주시기 바랍니다.
Git 명령어와의 유사성
앞서 pull
이라는 명령어가 Git과 비슷하다고 언급했는데, 실제로 도커의 많은 명령어들이 Git과 비슷합니다. 이미 Git에 익숙한 개발자라면 도커를 이해하기가 훨씬 쉬울 겁니다.
Git 명령어 | Docker 명령어 | 공통 기능 |
---|---|---|
git pull |
docker pull |
원격 저장소에서 가져오기 |
git commit |
docker commit |
변경사항을 새 버전으로 저장 |
git push |
docker push |
원격 저장소로 업로드 |
git diff |
docker diff |
변경사항 확인 |
git tag |
docker tag |
버전 태그 지정 |
이렇게 명령어 이름뿐만 아니라 기능도 매우 유사합니다. Git에서 git pull
으로 원격 저장소에서 코드를 가져오는 것처럼, 도커에서는 docker pull
로 원격 저장소에서 이미지를 가져옵니다. Git에서 git commit
으로 변경사항을 새로운 커밋으로 저장하는 것처럼, 도커에서도 docker commit
으로 컨테이너의 변경사항을 새로운 이미지로 저장할 수 있습니다.
명령어뿐만 아니라 생태계도 비슷합니다. Git에는 GitHub이라는 대표적인 원격 저장소 서비스가 있다면, Docker에는 Docker Hub라는 이미지 저장소 서비스가 있습니다. 둘 다 공개적으로 코드와 이미지를 공유하는 플랫폼 역할을 하고 있습니다.
컨테이너 변경사항을 이미지로 저장하기
지금까지 이미지로부터 컨테이너를 실행하는 방법을 배웠습니다. 그런데 한 가지 중요한 특징이 있습니다. 컨테이너를 아무리 변경해도 원본 이미지는 전혀 변하지 않습니다. 직접 확인해보겠습니다.
먼저 Ubuntu 컨테이너를 실행하고 curl
을 설치해보겠습니다. Ubuntu 24.04 이미지에는 기본적으로 curl
이 설치되어 있지 않습니다.
$ docker run -it --name my-container ubuntu:24.04 bash
root@db7732a5:/# which curl
# 아무것도 출력되지 않음 (curl이 없음)
root@db7732a5:/# apt-get update && apt-get install -y curl
# ... 설치 과정 ...
root@db7732a5:/# which curl
/usr/bin/curl
root@db7732a5:/# echo "Hello Docker!" > /myfile.txt
root@db7732a5:/# cat /myfile.txt
Hello Docker!
root@db7732a5:/# exit
컨테이너에서 나왔습니다. 이제 원본 ubuntu:24.04
이미지로 새로운 컨테이너를 실행해서 curl이 있는지 확인해보겠습니다.
$ docker run --rm ubuntu:24.04 which curl
# 아무것도 출력되지 않음 (curl이 없음)
파일도 확인해봅시다.
$ docker run --rm ubuntu:24.04 cat /myfile.txt
cat: /myfile.txt: No such file or directory
흥미롭게도 앞서 우리가 설치한 curl
도 없고, 직접 생성한 파일도 없습니다! 이는 컨테이너의 변경사항이 원본 이미지에 전혀 영향을 주지 않는다는 것을 보여줍니다.
docker commit으로 변경사항을 새 이미지로 저장하기
그렇다면 컨테이너에서 작업한 내용을 어떻게 이미지로 저장할 수 있을까요? 도커는 이를 위한 commit
명령어를 제공합니다. 이를 통해 컨테이너의 현재 상태를 새로운 이미지로 저장할 수 있습니다.
먼저 중지된 컨테이너의 상태를 확인합니다.
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
db7732a5 ubuntu:24.04 "bash" 2 minutes ago Exited (0) 1 minute ago my-container
이제 이 컨테이너를 새로운 이미지로 저장해보겠습니다. 먼저 커밋 서브 커맨드는 다음과 같이 사용합니다.
docker commit <CONTAINER> <NEW_IMAGE>
commit
명령어를 사용해 새로운 이미지를 만들어보겠습니다.
$ docker commit my-container my-ubuntu:with-curl
sha256:b024b8012959f0e83aca8fd70c6200f6aa832b17b285498d3f2d78974e18a172
새로 만든 이미지를 확인해봅니다.
$ docker image ls | grep ubuntu
my-ubuntu with-curl b024b801295 10 seconds ago 186MB
ubuntu 24.04 59a383896b55 4 weeks ago 101MB
원본 이미지보다 용량이 커진 것을 볼 수 있습니다. 이제 새 이미지로 컨테이너를 실행해서 확인해봅시다.
먼저 curl이 설치되어 있는지 확인합니다.
$ docker run --rm my-ubuntu:with-curl which curl
/usr/bin/curl
파일도 확인해봅니다.
$ docker run --rm my-ubuntu:with-curl cat /myfile.txt
Hello Docker!
성공입니다! 새 이미지에는 curl
도 설치되어 있고, 우리가 만든 파일도 있습니다.
정리하기
다음 명령어로 앞에서 생성한 컨테이너와 이미지를 삭제할 수 있습니다.
$ docker container rm my-container
$ docker image rm my-ubuntu:with-curl
이미지의 불변성
이미지는 읽기 전용(read-only)입니다. 컨테이너에서 아무리 많은 변경을 해도 원본 이미지는 변하지 않습니다. 이것이 도커의 핵심 특징 중 하나입니다.
docker commit
을 사용하면 컨테이너의 변경사항을 새로운 이미지로 저장할 수 있지만, 실제 프로덕션에서는Dockerfile
을 사용하는 것이 더 일반적입니다. Dockerfile을 사용하면 이미지 생성 과정을 코드로 관리할 수 있기 때문입니다.
Dockerfile로 이미지 만들기
도커 이미지를 만드는 가장 일반적인 방법은 Dockerfile
을 작성하는 것입니다. Dockerfile은 도커만의 특별한 DSL(Domain Specific Language)로 이미지를 정의하는 파일입니다.
앞서 docker commit
으로 curl이 설치된 이미지를 만들었는데, 이번에는 Dockerfile로 같은 작업을 해보겠습니다. Dockerfile을 저장할 디렉터리를 만듭니다.
$ mkdir my-ubuntu-curl
$ cd my-ubuntu-curl
다음 내용으로 Dockerfile
을 작성합니다.
FROM ubuntu:24.04
# 패키지 목록 업데이트와 curl 설치
RUN apt-get update
RUN apt-get install -y curl
# 기본 명령어 - curl 버전 확인
CMD ["curl", "--version"]
Dockerfile의 각 지시자를 살펴보겠습니다.
-
FROM ubuntu:24.04
: 베이스 이미지로 Ubuntu 24.04를 사용합니다. -
RUN apt-get update
: 패키지 목록을 업데이트합니다. -
RUN apt-get install -y curl
: curl을 설치합니다.-y
옵션은 설치 확인을 자동으로 승인합니다. -
CMD ["curl", "--version"]
: 컨테이너가 실행될 때 기본적으로 curl 버전을 출력합니다. CMD는 JSON 배열 형식(exec form)으로 작성하는 것이 권장됩니다. 배열 형식은 명령어를 직접 실행하지만, 문자열 형식(CMD curl --version
)은 셸(/bin/sh -c
)을 통해 실행되어 불필요한 프로세스가 추가되고 시그널 처리에 문제가 생길 수 있습니다.
이제 Dockerfile을 빌드해봅니다.
$ docker build -t my-ubuntu:curl .
[+] Building 15.2s (6/6) FINISHED
=> [internal] load build definition from Dockerfile
=> [internal] load metadata for docker.io/library/ubuntu:24.04
=> [1/3] FROM docker.io/library/ubuntu:24.04
=> [2/3] RUN apt-get update
=> [3/3] RUN apt-get install -y curl
=> exporting to image
=> naming to docker.io/library/my-ubuntu:curl
빌드한 이미지를 테스트합니다.
$ docker run --rm my-ubuntu:curl
curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13
Release-Date: 2023-12-06
Protocols: dict file ftp ftps gopher gophers http https imap imaps
Features: AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos
curl이 정상적으로 설치되어 작동하는 것을 확인할 수 있습니다!
이미지 크기를 확인해봅시다.
$ docker image ls | grep my-ubuntu
my-ubuntu curl c8d9f3b2a1e 1 minute ago 184MB
my-ubuntu with-curl b024b801295 5 minutes ago 186MB
docker commit
으로 만든 이미지(with-curl)와 Dockerfile로 만든 이미지(curl)의 크기가 비슷한 것을 볼 수 있습니다.
Dockerfile을 사용하면 이미지 생성 과정을 코드로 관리할 수 있어 더 권장되는 방법입니다. 설명을 위해 사용해보았습니다만, docker commit
을 직접 사용하는 일은 거의 없습니다.
도커 이미지: Python FastAPI 애플리케이션 예제
이번에는 실제 웹 애플리케이션을 도커 이미지로 만들어보겠습니다. 간단한 Python FastAPI 애플리케이션을 예제로 사용하겠습니다.
먼저 앱 디렉터리를 만들고 FastAPI 애플리케이션을 작성합니다.
$ mkdir fastapi-simple && cd fastapi-simple
app.py 파일을 작성합니다. 다음 내용을 그대로 저장해주세요.
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from datetime import datetime
import os
import platform
app = FastAPI()
# HTML 템플릿
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html>
<head>
<title>Docker Demo App</title>
<style>
body {{
font-family: sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
min-height: 100vh;
}}
.container {{
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
h1 {{ text-align: center; color: #333; }}
.info {{ background: #f5f5f5; padding: 20px; border-radius: 5px; margin-top: 20px; }}
.info p {{ margin: 10px 0; }}
</style>
</head>
<body>
<div class="container">
<h1>Hello from Docker!</h1>
<div class="info">
<p><strong>현재 시간:</strong> {timestamp}</p>
<p><strong>호스트명:</strong> {hostname}</p>
<p><strong>Python 버전:</strong> {python_version}</p>
<p><strong>운영체제:</strong> {os_info}</p>
</div>
</div>
</body>
</html>
'''
@app.get("/", response_class=HTMLResponse)
def hello():
html_content = HTML_TEMPLATE.format(
timestamp=datetime.now().strftime('%Y년 %m월 %d일 %H:%M:%S'),
hostname=os.environ.get('HOSTNAME', 'unknown'),
python_version=platform.python_version(),
os_info=f"{platform.system()} {platform.release()}"
)
return html_content
@app.get("/health")
def health():
return {"status": "healthy"}
@app.get("/api")
def api():
return {
"message": "Hello Docker!",
"timestamp": datetime.now().isoformat(),
"hostname": os.environ.get('HOSTNAME', 'unknown')
}
requirements.txt 파일을 작성합니다.
fastapi==0.115.5
uvicorn==0.32.1
먼저 로컬에서 실행해보겠습니다. macOS에서는 Homebrew로 Python을 설치할 수 있습니다.
# macOS에서 Python 설치
$ brew install python3
# Python 버전 확인
$ python3 --version
Python 3.13.1
# 가상환경 생성 및 활성화
$ python3 -m venv venv
$ source venv/bin/activate
# 패키지 설치
$ pip install -r requirements.txt
# FastAPI 앱 실행
$ uvicorn app:app --host 0.0.0.0 --port 8000
INFO: Started server process [96684]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
브라우저에서 http://localhost:8000
으로 접속하면 앱이 실행되는 것을 확인할 수 있습니다. 로컬 개발이 끝났으니, 이제 도커 이미지로 만들어보겠습니다.
Dockerfile을 작성합니다.
# Python 3.12 slim 이미지 사용
FROM python:3.12-slim
# 작업 디렉터리 설정
WORKDIR /app
# 의존성 파일 복사
COPY requirements.txt .
# 패키지 설치
RUN pip install --no-cache-dir -r requirements.txt
# 애플리케이션 코드 복사
COPY app.py .
# 포트 노출
EXPOSE 8000
# 앱 실행 (uvicorn 사용)
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfile의 각 지시자를 간단히 살펴보겠습니다:
-
FROM python:3.12-slim: Python 3.12가 설치된 리눅스 이미지를 기반으로 시작합니다.
-
WORKDIR /app: 컨테이너 내부의 작업 폴더를
/app
으로 설정합니다. -
COPY requirements.txt .: 필요한 패키지 목록 파일을 컨테이너로 복사합니다.
-
RUN pip install –no-cache-dir -r requirements.txt: 필요한 Python 패키지들(FastAPI, uvicorn)을 설치합니다.
-
COPY app.py .: 우리가 작성한 애플리케이션 코드를 컨테이너로 복사합니다.
-
EXPOSE 8000: 이 컨테이너가 8000번 포트를 사용한다고 알려줍니다.
-
CMD [“uvicorn”, “app:app”, “–host”, “0.0.0.0”, “–port”, “8000”]: 컨테이너가 시작되면 FastAPI 앱을 실행합니다.
이제 이미지를 빌드하고 실행합니다.
$ docker build -t fastapi-demo .
[+] Building 4.2s FINISHED
=> [1/5] FROM docker.io/library/python:3.12-slim
=> [2/5] WORKDIR /app
=> [3/5] COPY requirements.txt .
=> [4/5] RUN pip install --no-cache-dir -r requirements.txt
=> [5/5] COPY app.py .
=> exporting to image
=> naming to docker.io/library/hello-docker
$ docker run -d --name fastapi-app -p 8000:8000 fastapi-demo
fa54ef7bfcc0326dbd5295c92980e5ebdca8b9f28b1e8a18da35665574b3b4ba
여기서 새로운 옵션인 -p 8000:8000
이 등장했습니다. 이것은 포트 포워딩(Port Forwarding) 설정입니다. 컨테이너는 독립된 네트워크 환경에서 실행되기 때문에, 기본적으로 외부에서 접근할 수 없습니다. -p
옵션을 사용하면 호스트의 포트를 컨테이너의 포트로 연결할 수 있습니다.*
* 포트 포워딩 형식은 -p [호스트포트]:[컨테이너포트]
입니다. 예를 들어 -p 9000:8000
이면 호스트의 9000번 포트로 접근하면 컨테이너의 8000번 포트로 연결됩니다.
FastAPI 앱이 8000번 포트에서 실행되고 있으므로, 호스트의 8000번 포트를 컨테이너의 8000번 포트로 연결했습니다. 이제 브라우저에서 http://localhost:8000
으로 접속할 수 있게 되었습니다.
앱이 정상 작동하는지 확인합니다.
$ curl http://localhost:8000/api
{
"hostname": "fa54ef7bfcc0",
"message": "Hello Docker!",
"timestamp": "2025-09-05T12:40:46.312429"
}
$ curl http://localhost:8000/health
{"status": "healthy"}
컨테이너 로그를 확인할 수도 있습니다.
$ docker logs fastapi-app
INFO: Started server process [1]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Docker Compose로 실제 서비스처럼 배포하기
실제 서비스는 단일 컨테이너로 동작하는 경우가 드뭅니다. 대부분 웹 서버, 데이터베이스, 캐시 서버 등 여러 컴포넌트가 함께 동작합니다. Docker Compose는 이런 멀티 컨테이너 애플리케이션을 쉽게 관리할 수 있게 해줍니다.*
* Docker Desktop을 설치하면 Docker Compose가 포함되어 있습니다. 실제 서버에 배포할 때도 Docker Compose만 있으면 복잡한 애플리케이션을 한 번에 실행할 수 있습니다.
실제 서비스처럼 웹 애플리케이션과 Redis 캐시 서버를 함께 실행하는 예제를 만들어보겠습니다.
새로운 폴더를 만들고 Redis를 사용하는 버전으로 작성합니다.
$ mkdir fastapi-redis && cd fastapi-redis
app.py 파일을 작성합니다. 이번에는 Redis 방문 카운터가 포함된 버전입니다.
# app.py에 Redis 지원 추가 부분
import redis
redis_host = os.environ.get('REDIS_HOST', 'localhost')
try:
r = redis.Redis(host=redis_host, port=6379, decode_responses=True)
r.ping()
redis_connected = True
except:
r = None
redis_connected = False
@app.get("/api")
def api():
visit_count = 0
if redis_connected:
visit_count = r.incr('visit_count')
return {
'message': 'Hello Docker!',
'timestamp': datetime.now().isoformat(),
'hostname': os.environ.get('HOSTNAME', 'unknown'),
'visit_count': visit_count,
'redis_connected': redis_connected
}
requirements.txt
에 Redis 클라이언트를 추가합니다.
fastapi==0.115.5
uvicorn==0.32.1
redis==5.0.8
이제 Docker Compose 설정 파일을 작성해보겠습니다. Docker Compose는 compose.yml* 파일에 여러 컨테이너의 설정을 정의합니다. 이 파일이 있으면 복잡한 docker run
명령어들을 기억할 필요 없이 docker compose up
하나의 명령어로 모든 컨테이너를 실행할 수 있습니다.
* 예전에는 `docker-compose.yml`이 표준이었지만, Docker Compose v2부터는 더 간단한 `compose.yml`을 권장합니다. 두 파일명 모두 작동하며, 파일 상단의 `version: '3'` 같은 버전 명시도 이제는 선택사항입니다.
compose.yml 파일을 작성합니다.
services:
web:
build: .
ports:
- "8000:8000"
environment:
- REDIS_HOST=redis
depends_on:
- redis
redis:
image: redis:7-alpine
compose.yml 파일을 차근차근 살펴보겠습니다.
services 섹션은 실행할 컨테이너들을 정의하는 부분입니다. 여기서는 web
과 redis
두 개의 서비스를 정의합니다.
web 서비스 설정:
build: . # 현재 디렉터리의 Dockerfile로 이미지 빌드
ports:
- "8000:8000" # 호스트:8000 → 컨테이너:8000 포트 매핑
environment:
- REDIS_HOST=redis # 환경변수 설정 (Redis 호스트명)
depends_on:
- redis # Redis 컨테이너가 먼저 실행되도록 순서 지정
redis 서비스 설정:
image: redis:7-alpine # Docker Hub에서 기존 이미지 사용
Docker Compose 없이 같은 환경을 구성하려면 다음과 같은 복잡한 docker run
명령어들이 필요합니다:
# 네트워크 생성
$ docker network create app-network
# Redis 컨테이너 실행
$ docker run -d \
--name redis \
--network app-network \
redis:7-alpine
# 웹 애플리케이션 빌드 및 실행
$ docker build -t fastapi-app .
$ docker run -d \
--name web \
--network app-network \
-p 8000:8000 \
-e REDIS_HOST=redis \
fastapi-app
Docker Compose를 사용하면 이 모든 설정을 compose.yml
파일에 구조화해서 관리할 수 있고, 단 한 줄의 명령어(docker compose up
)로 실행할 수 있습니다. 이것이 Docker Compose의 가장 큰 장점입니다.
Docker Compose는 기본적으로 프로젝트 이름을 기반으로 사용자 정의 네트워크를 생성하고, 모든 서비스를 해당 네트워크에 연결합니다. 따라서 별도의 네트워크 설정 없이도 컨테이너들은 서비스 이름으로 서로를 찾을 수 있습니다.
여기서 중요한 점은 같은 Compose 프로젝트의 컨테이너들은 서비스 이름으로 서로를 찾을 수 있다는 것입니다. 즉, 웹 컨테이너에서 redis
라는 이름으로 Redis 컨테이너에 접속할 수 있습니다.
YAML 파일 검증하기
Docker Compose를 실행하기 전에 YAML 파일이 올바른지 검증하는 것이 좋습니다. YAML은 들여쓰기에 매우 민감하기 때문에 실수하기 쉽습니다.
# YAML 파일 검증
$ docker compose config --quiet
# 또는 상세한 설정 확인
$ docker compose config
--quiet
옵션을 사용하면 오류가 있을 때만 메시지가 표시됩니다. 오류가 없다면 아무것도 출력되지 않습니다.
자주 발생하는 YAML 오류와 해결법
yaml: line X: mapping values are not allowed in this context
- 원인: 콜론(:) 다음에 공백 누락
- 해결: 콜론 다음에 공백 추가 (예:
key: value
)
yaml: line X: found character that cannot start any token
- 원인: 탭 문자 사용
- 해결: 탭을 공백 2개로 변경
services.web additional properties 'X' not allowed
- 원인: 오타 또는 잘못된 키
- 해결: Docker Compose 문서에서 올바른 키 이름 확인
Bind for 0.0.0.0:8000 failed: port is already allocated
- 원인: 포트 충돌
- 해결: 다른 포트 사용 또는 기존 프로세스 종료
이제 Docker Compose로 모든 서비스를 한 번에 실행해보겠습니다.
$ docker compose up -d
[+] Running 3/3
✔ Network sample-app_app-network Created
✔ Container sample-app-redis-1 Started
✔ Container sample-app-web-1 Started
docker compose up -d
명령어를 실행하면 Docker Compose가 다음과 같은 작업을 수행합니다.
먼저 sample-app_app-network
라는 가상 네트워크를 생성합니다. 그 다음 sample-app-redis-1
컨테이너가 시작되고, depends_on
설정에 따라 Redis가 준비된 후에 sample-app-web-1
컨테이너가 시작됩니다. 이렇게 의존성 순서에 따라 컨테이너들이 순차적으로 실행됩니다.
-d
플래그는 모든 컨테이너를 백그라운드에서 실행하라는 의미입니다 (detached mode).
실행 중인 서비스를 확인해보겠습니다. Docker Compose 프로젝트의 컨테이너를 확인할 때는 두 가지 명령어를 사용할 수 있습니다:
# Compose 프로젝트의 컨테이너만 보기
$ docker compose ps
NAME IMAGE STATUS PORTS
sample-app-redis-1 redis:7-alpine Up 6379/tcp
sample-app-web-1 sample-app-web Up 0.0.0.0:8000->8000/tcp
# 시스템 전체의 모든 컨테이너 보기
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
db7732a5 redis:7-alpine "docker-entrypoint.s…" 10 seconds ago Up 9 seconds 6379/tcp sample-app-redis-1
a5b3c1d2 sample-app-web "uvicorn app:app --h…" 10 seconds ago Up 9 seconds 0.0.0.0:8000->8000/tcp sample-app-web-1
e4f5g6h7 mysql:8 "docker-entrypoint.s…" 2 hours ago Up 2 hours 3306/tcp my-database
docker compose ps
는 현재 디렉터리의 Compose 프로젝트에 속한 컨테이너만 보여주므로 더 집중적으로 관리할 수 있습니다. 반면 docker container ls
는 시스템의 모든 실행 중인 컨테이너를 보여줍니다.
두 컨테이너 모두 정상적으로 실행되고 있습니다. sample-app-web-1
컨테이너의 8000번 포트가 호스트의 8000번 포트로 연결되어 있는 것을 확인할 수 있습니다. Redis는 내부적으로만 사용되므로 외부 포트 연결이 없습니다.
이제 앱이 Redis와 연동되어 작동하는지 확인해보겠습니다.
jq 설치하기
jq는 JSON 데이터를 처리하기 위한 경량 커맨드라인 도구입니다. JSON 출력을 보기 좋게 포맷팅하거나 특정 필드를 추출할 때 편리합니다. jq의 더 자세한 사용법은 커맨드라인 JSON 프로세서 jq 기초 문법과 작동 원리를 참고하세요.
# macOS $ brew install jq # Ubuntu/Debian $ sudo apt-get install jq # Rocky/RHEL/CentOS $ sudo dnf install jq
$ curl http://localhost:8000/api | jq .
{
"hostname": "d07a98b3aec4",
"message": "Hello Docker!",
"redis_connected": true,
"timestamp": "2025-09-05T13:12:02.690954",
"visit_count": 1
}
# 다시 호출하면 방문 횟수가 증가합니다
$ curl http://localhost:8000/api | jq .visit_count
2
정상 동작을 확인했으니, 서비스를 중지하겠습니다.
$ docker compose down
[+] Running 3/3
✔ Container sample-app-web-1 Removed
✔ Container sample-app-redis-1 Removed
✔ Network sample-app_default Removed
실전: 도커 이미지로 서버 애플리케이션 배포하기
지금까지 도커의 기본 개념을 배우고, 직접 이미지를 만들고, Docker Compose로 여러 컨테이너를 관리하는 방법을 익혔습니다. 이제 실제로 만든 애플리케이션을 인터넷에서 접속 가능한 서버에 배포해보겠습니다.
도커 배포의 전체 흐름 이해하기
도커를 사용한 배포는 크게 4단계로 이루어집니다:
- Build - 로컬에서 도커 이미지 빌드
- Push - Docker Hub에 이미지 업로드
- Pull - 서버에서 이미지 다운로드
- Run - 서버에서 컨테이너 실행
이제 각 단계를 실습해보겠습니다.
도커 허브 계정 생성 및 이미지 푸시
도커 허브는 도커 이미지를 공유할 수 있는 공식 레지스트리입니다. 퍼블릭 이미지는 무료로 호스팅할 수 있습니다.
먼저 Docker Hub에 계정을 생성합니다.
이제 앞서 만든 FastAPI 애플리케이션을 Docker Hub에 올려보겠습니다. Redis를 사용하지 않는 단순한 버전으로 진행합니다.
먼저 처음 만든 fastapi-simple 폴더로 돌아가서 이미지를 빌드해보겠습니다.
$ cd ../fastapi-simple
이미지를 빌드합니다.
$ docker build -t fastapi-demo:v1 .
[+] Building 7.2s FINISHED
=> [1/5] FROM docker.io/library/python:3.12-slim
=> [2/5] WORKDIR /app
=> [3/5] COPY requirements.txt .
=> [4/5] RUN pip install --no-cache-dir -r requirements.txt
=> [5/5] COPY app.py .
=> exporting to image
=> => naming to docker.io/library/fastapi-demo:v1
macOS 사용자를 위한 아키텍처 주의사항
Apple Silicon Mac에서는 이미지가 기본적으로 ARM64 아키텍처으로 빌드됩니다. 이는 Apple Silicon이 ARM64 아키텍처이기 때문입니다. 하지만 Fly.io를 포함한 많은 클라우드 서비스는 AMD64(x86_64) 아키텍처를 사용합니다.
Docker Hub에 푸시할 이미지를 빌드할 때 아키텍처를 지정해서 크로스 플랫폼 빌드를 할 수 있습니다.
# AMD64 아키텍처로 빌드 (Apple Silicon에서) $ docker buildx build --platform linux/amd64 -t fastapi-demo:v1 .
이를 통해 AMD64 서버에서도 정상적으로 실행되는 이미지를 만들 수 있습니다. 하지만 이러한 크로스 플랫폼 빌드는 안정성이 떨어지는 편입니다. 프로덕션 환경에서는 배포하려는 서버와 같은 아키텍처의 서버에서 먼저 이미지를 빌드하는 게 일반적입니다.
Fly.io 배포를 위해 AMD64 아키텍처로도 이미지를 빌드해보겠습니다. Apple Silicon Mac 사용자가 아니더라도 클라우드 호환성을 위해 함께 진행하는 것이 좋습니다.
# AMD64 아키텍처로 추가 빌드
$ docker buildx build --platform linux/amd64 -t fastapi-demo:v1-amd64 .
[+] Building 12.3s FINISHED
=> [internal] load build definition from Dockerfile
=> [internal] load metadata for docker.io/library/python:3.12-slim
=> [1/5] FROM docker.io/library/python:3.12-slim
=> [2/5] WORKDIR /app
=> [3/5] COPY requirements.txt .
=> [4/5] RUN pip install --no-cache-dir -r requirements.txt
=> [5/5] COPY app.py .
=> exporting to image
=> => naming to docker.io/library/fastapi-demo:v1-amd64
이제 두 개의 이미지가 준비되었습니다:
-
fastapi-demo:v1
- 로컬 아키텍처 (Intel Mac은 AMD64, Apple Silicon은 ARM64) -
fastapi-demo:v1-amd64
- AMD64 아키텍처 (Fly.io 배포용)
다음으로 Docker Hub에 로그인합니다.
$ docker login
Login with your Docker ID to push and pull images from Docker Hub.
Username: <YOUR_DOCKER_ID>
Password:
Login Succeeded
Docker Hub의 이미지는 <계정명>/<이미지명>:<태그>
형식을 따릅니다. 이 형식에 맞춰서 이미지 이름을 변경합니다. 아래 예제들에서 <YOUR_DOCKER_ID>
는 Docker Hub에 가입할 때 사용한 ID를 지정해줍니다. 예를 들어 ID가 44bits라면 44bits/fastapi-demo:v1
이 됩니다.
두 이미지 모두 태깅합니다:
# 로컬 아키텍처 이미지 태깅
$ docker tag fastapi-demo:v1 <YOUR_DOCKER_ID>/fastapi-demo:v1
# AMD64 이미지 태깅
$ docker tag fastapi-demo:v1-amd64 <YOUR_DOCKER_ID>/fastapi-demo:v1-amd64
이제 이미지를 도커 허브에 실제로 Push해보겠습니다.
Docker Hub repository 자동 생성
Docker Hub에서는 repository를 미리 만들 필요가 없습니다.
docker push
명령어를 실행하면 해당 이름의 repository가 없을 경우 자동으로 생성됩니다. GitHub과 달리 Docker Hub는 이미지를 푸시하는 즉시 repository를 만들어주므로 매우 편리합니다.
두 이미지를 모두 푸시합니다:
# 로컬 아키텍처 버전 푸시
$ docker push <YOUR_DOCKER_ID>/fastapi-demo:v1
The push refers to repository [docker.io/<YOUR_DOCKER_ID>/fastapi-demo]
62f14d387019: Pushed
d4c7da855a63: Pushed
44c1f5774e31: Pushed
v1: digest: sha256:a4056a304abe5c40587e8512b04cb13125ef2501fbae52d4ed23d756e72ba1ce size: 1990
# AMD64 버전 푸시 (Fly.io 배포용)
$ docker push <YOUR_DOCKER_ID>/fastapi-demo:v1-amd64
The push refers to repository [docker.io/<YOUR_DOCKER_ID>/fastapi-demo]
62f14d387019: Layer already exists
d4c7da855a63: Layer already exists
44c1f5774e31: Layer already exists
v1-amd64: digest: sha256:b5167b405abe5c40587e8512b04cb13125ef2501fbae52d4ed23d756e72ba1ce size: 1990
이제 다음 페이지에서 Docker Hub에서 업로드된 이미지를 확인할 수 있습니다.
Fly.io에 도커 컨테이너 배포하기
지금까지 로컬에서 도커 이미지를 만들고 실행해보았습니다. 이제 정말로 인터넷에서 접속 가능한 서버에 배포해볼 차례입니다!
예전에는 서버를 직접 구성하고 도커를 설치하고 컨테이너를 실행해야 했지만, 요즘은 도커 컨테이너를 바로 배포할 수 있는 PaaS(Platform as a Service) 서비스들이 많이 있습니다. 이 튜토리얼에서는 Fly.io를 사용해보겠습니다.
Fly.io 시작하기
Fly.io는 도커 컨테이너를 전 세계 어디서나 접속 가능한 서버로 만들어주는 서비스입니다. 마치 Docker Hub가 이미지를 저장하는 창고라면, Fly.io는 그 이미지를 기반으로 컨테이너를 실행하는 서버라고 생각하면 됩니다.
여기서는 Fly.io를 사용해 도커 컨테이너를 두 가지 방법으로 배포해보겠습니다.
- 방법 1: Docker Hub에 올린 이미지를 가져와서 배포 (Push & Pull 방식)
- 방법 2: 소스 코드를 올리면 Fly.io가 직접 빌드해서 배포 (직접 빌드 방식)
방법 1은 이미 빌드/푸시된 이미지를 바로 사용할 수 있다는 장점이 있습니다만, 실제로는 방법 2를 더 선호합니다. 왜냐면 방법 2를 사용하면 소스코드가 업데이트 될 때마다 이미지를 빌드하고, 푸시하고, 풀하고, 실행하는 과정 전체를 자동화해주기 때문입니다. 여기서는 차례대로 배포해보겠습니다.
먼저 Fly.io를 사용하기 위한 명령어 도구(CLI)를 설치해야 합니다. 이 도구로 터미널에서 바로 배포할 수 있습니다. 운영체제에 따라서 명령어를 실행합니다.
# macOS
$ brew install flyctl
# Linux
$ curl -L https://fly.io/install.sh | sh
# Windows (PowerShell)
$ powershell -Command "iwr https://fly.io/install.ps1 -useb | iex"
다음으로 fly.io로 회원 가입을 진행합니다.
Fly.io 요금 정책 (2025년 기준)
2024년 10월 7일부터 Fly.io의 무료 티어가 폐지되었습니다. 신규 가입자는 Pay-As-You-Go(사용한 만큼 지불) 방식만 이용 가능합니다.
- 최소 비용: shared-cpu-1x 256MB VM 기준 월 약 $1-2
- 회원가입 시 필요: 신용카드 등록 또는 최소 $25 크레딧 선불 충전
- 자동 중지 기능: 트래픽이 없으면 자동으로 중지되어 비용 절약
테스트 목적이라면 실습 후 바로 앱을 삭제하는 것을 권장합니다. 이 튜토리얼 마지막에 앱 삭제 방법을 안내합니다.
$ flyctl auth signup # 브라우저가 열리면서 회원가입 페이지로 이동
회원 가입을 진행합니다. 그리고 로그인을 진행합니다.
$ flyctl auth login # 로그인
방법 1: Docker Hub 이미지를 Fly.io에 배포
앞서 Docker Hub에 올린 이미지를 Fly.io에서 실행해보겠습니다. 이 방법은 별도의 빌드나 푸시 과정 없이 미리 빌드된 이미지를 사용한다는 장점이 있습니다.
먼저 Fly.io를 위한 새 디렉터리를 만들고 이동합니다:
$ mkdir flyio-hub
$ cd flyio-hub
다음으로 Fly.io 설정 파일인 fly.toml
을 생성합니다. 이 파일은 도커 컨테이너를 어떻게 실행할지 알려주는 설정 파일입니다:
app = "fastapi-hub-<YOUR_NAME>"
primary_region = "nrt"
[build]
image = "<YOUR_DOCKER_ID>/fastapi-demo:v1-amd64"
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[[vm]]
memory = "256mb"
cpu_kind = "shared"
cpus = 1
위 파일에서 <YOUR_NAME>
과 <YOUR_DOCKER_ID>
를 실제 값으로 변경해야 합니다.
-
app = "fastapi-hub-john"
(앱 이름은 전 세계에서 유일해야 합니다) -
image = "johndoe/fastapi-demo:v1-amd64"
(Docker Hub에 올린 AMD64 이미지)
다른 부분도 자세히 살펴보겠습니다.
-
primary_region = "nrt"
는 도쿄 지역에 서버를 배치한다는 의미입니다 -
internal_port = 8000
은 컨테이너 내부에서 앱이 8000번 포트로 실행된다는 의미입니다 -
auto_stop_machines = "stop"
은 트래픽이 없을 때 자동으로 중지해서 비용을 절약합니다
이제 드디어 배포해볼 차례입니다! 먼저 앱을 생성하고 배포합니다:
$ flyctl apps create fastapi-hub-<YOUR_NAME>
$ flyctl deploy
배포가 완료되면 몇 초 후에 https://fastapi-hub-<YOUR_NAME>.fly.dev/
에서 앱에 접속할 수 있습니다.
축하합니다! 방금 여러분의 도커 컨테이너가 인터넷에서 접속 가능한 서버로 배포되었습니다. 브라우저에서 URL을 열어보면 로컬에서 본 것과 똑같은 화면이 나타납니다.
방법 2: Fly.io에서 직접 빌드하여 배포
이번에는 Docker Hub를 거치지 않고, Fly.io가 직접 도커 이미지를 빌드해서 배포하는 방법을 사용해보겠습니다. 이 방법은 소스 코드만 있으면 바로 배포할 수 있어 편리합니다.
Apple Silicon에서도 문제 없어요
Fly.io는 Depot 빌더를 사용하여 자동으로 AMD64 아키텍처로 이미지를 빌드합니다. Apple Silicon Mac에서도 별도 설정 없이 배포가 가능합니다.
$ cd fastapi-simple
$ flyctl launch --no-deploy
Scanning source code
Detected a Dockerfile app
Creating app in /path/to/fastapi-simple
? Choose an app name (leave blank to generate one): fastapi-build-<YOUR_NAME>
? Choose a region for deployment: Tokyo, Japan (nrt)
? Would you like to set up a Postgresql database now? No
? Would you like to set up an Upstash Redis database now? No
Created app 'fastapi-build-<YOUR_NAME>' in organization 'personal'
Admin URL: https://fly.io/apps/fastapi-build-<YOUR_NAME>
Hostname: fastapi-build-<YOUR_NAME>.fly.dev
Wrote config file fly.toml
flyctl launch
명령어는 Dockerfile을 자동으로 발견하고, 필요한 설정을 물어본 다음, fly.toml
파일을 자동으로 생성해줍니다.
생성된 fly.toml
파일을 확인해보겠습니다.
app = "fastapi-build-<YOUR_NAME>"
primary_region = "nrt"
[build]
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[[vm]]
memory = "256mb"
cpu_kind = "shared"
cpus = 1
이제 실제로 배포해보겠습니다. 이 명령어 하나로 Fly.io가 Dockerfile을 읽고, 이미지를 빌드하고, 서버에 배포하는 모든 과정을 자동으로 처리합니다.
$ flyctl deploy
==> Verifying app config
==> Building image
==> Building image with Depot
[+] Building 11.8s (10/10) FINISHED
=> [1/5] FROM docker.io/library/python:3.12-slim
=> [2/5] WORKDIR /app
=> [3/5] COPY requirements.txt .
=> [4/5] RUN pip install --no-cache-dir -r requirements.txt
=> [5/5] COPY app.py .
--> Building image done
image: registry.fly.io/fastapi-build-<YOUR_NAME>:deployment-01K4DXE...
Visit your newly deployed app at https://fastapi-build-<YOUR_NAME>.fly.dev/
배포가 완료되었습니다. 배포된 앱의 상태를 확인해보겠습니다.
$ flyctl status
App
Name = fastapi-demo-tutorial
Owner = personal
Hostname = fastapi-demo-tutorial.fly.dev
Platform = machines
Machines
ID STATE REGION HEALTH
286e236c453ed8 started nrt passing
d8d3014a143258 started nrt passing
$ flyctl logs # 실시간 로그 확인
2025-09-05T22:38:45Z app[286e236c453ed8] nrt [info] * Running on http://0.0.0.0:8000
2025-09-05T22:38:46Z app[286e236c453ed8] nrt [info] 172.16.0.1 - - [05/Sep/2025 22:38:46] "GET / HTTP/1.1" 200 -
이번에도, 앞에서 배포한 경우와 마찬가지로 인터넷 어디에서나 접속이 가능합니다.
실습 후 정리하기
Fly.io는 Pay-As-You-Go 방식으로 과금되므로, 실습이 끝나면 즉시 앱을 정리해야 불필요한 비용이 발생하지 않습니다. 트래픽이 없을 때 자동으로 멈추도록 설정했지만, 완전히 삭제하는 것이 가장 안전합니다.
# 앱 일시 중지 (인스턴스를 0으로 줄여서 비용 절약)
$ flyctl scale count 0 --yes
App 'fastapi-demo-tutorial' is going to be scaled according to this plan:
-2 machines for group 'app' on region 'nrt' of size 'shared-cpu-1x'
Executing scale plan
Destroyed 286e236c453ed8 group:app region:nrt size:shared-cpu-1x
Destroyed d8d3014a143258 group:app region:nrt size:shared-cpu-1x
# 앱 완전 삭제 (더 이상 필요하지 않을 때)
$ flyctl apps destroy fastapi-demo-tutorial --yes
Destroyed app fastapi-demo-tutorial
Fly.io를 사용한 도커 배포에 대한 더 자세한 내용은 도커 컨테이너 5분 만에 무료로 배포하기(feat. fly.io)를 참고하세요.
마치며
축하합니다! 지금까지 도커의 핵심 개념과 실전 활용법을 알아보았습니다. 이 튜토리얼을 통해 배운 것들을 정리하면 다음과 같습니다.
- 이미지와 컨테이너 - 도커의 가장 중요한 두 개념의 차이와 관계
- Dockerfile - 코드로 관리하는 이미지 생성 방법
- Docker Compose - 여러 컨테이너를 한 번에 관리하는 방법
- 실제 배포 - Docker Hub와 Fly.io를 통한 클라우드 배포
이 튜토리얼은 시작에 불과합니다. 더 깊이 있는 학습을 원한다면 아래 자료들을 참고해보세요.
더 읽으면 좋은 글들
컨테이너의 동작 원리가 궁금하다면
- 도커(Docker) 컨테이너는 가상머신인가요? 프로세스인가요?
- 컨테이너 기초 - chroot를 사용한 프로세스의 루트 디렉터리 격리
- 컨테이너 기초 - 정적 링크 프로그램을 chroot와 도커(Docker) scratch 이미지로 실행하기
- 만들면서 이해하는 도커(Docker) 이미지: 도커 이미지 빌드 원리와 OverlayFS
도커로 개발 환경을 구성하고 싶다면
프로덕션 수준의 컨테이너 운영이 궁금하다면