기사 원문: How to Use to Docker with Node.js: A Handbook for Developers
이 핸드북에서는 Docker가 무엇인지, 그리고 Docker가 왜 백엔드 및 풀스택 개발자에게 반드시 필요한 기술인지를 배우게 됩니다. 그리고 가장 중요한 것은, 실제 프로젝트 전 과정을 통해 Docker를 어떻게 활용하는지 익히게 된다는 점입니다.
우리는 흔한 “Hello World” 예제를 훨씬 뛰어넘어, 완전한 풀스택 자바스크립트 애플리케이션(Node.js + Express 백엔드, HTML/CSS/JS 프론트엔드, MongoDB 데이터베이스, Mongo Express 관리자 UI)을 컨테이너화하는 과정을 처음부터 끝까지 함께 따라가 볼 것입니다.
여러 개의 컨테이너를 네트워킹(통신)하는 방법, Docker Compose로 모든 서비스를 오케스트레이션하는 방법, 나만의 이미지를 빌드하고 버전을 관리하는 방법, 볼륨을 사용해 데이터를 영구적으로 보존하는 방법, 그리고 이미지를 안전하게 AWS ECR 개인 레포지토리에 푸시해 공유와 프로덕션 배포에 활용하는 방법 등을 배우게 됩니다.
튜토리얼을 끝낼 즈음에는 “내 컴퓨터에서는 잘 되는데” 문제를 없애고, 멀티 서비스 애플리케이션을 자신 있게 관리하며, 어디서든 일관된 환경을 배포하고, 일상적인 개발 워크플로와 CI/CD 파이프라인에 Docker를 능숙하게 통합할 수 있게 될 것입니다.
Docker는 백엔드 개발자에게 매우 중요한 기술이기 때문에, 먼저 Docker의 기본 개념부터 차근차근 살펴보겠습니다.
전제조건
이 기술 핸드북은 풀스택 개발에 대해 어느 정도 실무 경험이 있는 개발자를 대상으로 합니다. 독자는 애플리케이션 배포에 익숙하고, CI/CD 파이프라인에 대한 기본적인 이해를 갖추고 있어야 합니다. 비록 이 가이드에서 Docker를 기초부터 다루긴 하지만, 완전 초보 개발자를 위한 내용은 아니며, 실제 개발 경험이 있고 Docker로 워크플로를 한 단계 끌어올리고 싶은 분들을 전제로 합니다.
마지막으로, AWS에 대한 기본적인 지식과 일반적인 배포 개념을 알고 있다면 도움이 되지만, 전문가일 필요는 없습니다. 이 핸드북은 프로덕션급 역량을 강화하고, Docker를 자신의 프로젝트에 자신 있게 통합하고자 하는 개발자에게 이상적인 자료입니다.
목차
-
컨테이너란 무엇인가?
-
Docker와 가상 머신
-
Docker 설치
-
기본 Docker 명령어
-
JavaScript로 연습하기
-
MongoDB 이미지 가져오기
-
Mongo Express 이미지 가져오기
-
Docker 네트워크
-
-
Mongo 컨테이너 실행 방법
-
Mongo Express 컨테이너 실행 방법
-
Node.js를 MongoDB에 연결하는 방법
-
Docker Compose 사용 방법
- 왜 Docker Compose를 사용하는가?
-
나만의 Docker 이미지 빌드하기
-
해결책
-
MongoDB가 동작하는 이유
-
앱을 Docker Compose에 추가하기
-
모든 서비스 시작하기
-
모든 것이 제대로 동작하는지 확인하기
-
무엇이 바뀌었고 왜 동작하는가
-
-
컨테이너 관리 방법
-
프라이빗 Docker 저장소를 만드는 방법
-
1단계: AWS 액세스 키 받기
-
2단계: AWS CLI 설치 여부 확인
-
3단계: AWS CLI 설정
-
4단계: AWS 설정 테스트
-
5단계: ECR(Docker 레지스트리)에 로그인
-
Docker 저장소의 이미지 이름 규칙 이해하기
-
6단계: 이미지 빌드, 태그, 푸시하기
-
-
과제: 새 버전을 만들어 푸시하기
-
이미지 배포하기
-
왜 ECR에서 전체 이미지 URL을 사용해야 하는가?
-
Docker Compose로 앱 배포하기
-
프라이빗 Docker 이미지 공유하기
-
-
Docker 볼륨
-
Docker 볼륨 동작 방식
-
Docker 볼륨의 종류
-
볼륨을 사용하는 Docker Compose 예시 파일
-
애플리케이션 시작하기
-
-
결론
컨테이너란 무엇인가?
컨테이너는 애플리케이션이 실행되는 데 필요한 모든 것(의존성, 라이브러리, 설정 파일 등)을 함께 묶어서 패키징하는 방법입니다.
컨테이너는 이식성이 좋아 팀 간에 쉽게 공유할 수 있고, 호환성 문제를 걱정하지 않고 어떤 머신에든 배포할 수 있습니다.

컨테이너는 어디에 있나요?
컨테이너는 팀과 시스템 사이에서 자유롭게 이동하고 공유될 수 있기 때문에, 컨테이너가 “머무를” 장소가 필요합니다. 여기에서 컨테이너 저장소(레지스트리)가 등장하는데, 이는 컨테이너 이미지를 보관하는 특수한 저장 공간입니다. 조직은 내부용 비공개 저장소를 둘 수 있고, Docker Hub 같은 공개 저장소를 통해 누구나 공유된 컨테이너 이미지를 찾아보고 사용할 수 있습니다.

Docker Hub의 카탈로그 페이지를 방문해 보면, Redis, Jenkins 같은 개발자와 팀이 만든 공식 및 커뮤니티 기반 컨테이너 저장소들을 다양하게 볼 수 있습니다.
예전에는 여러 개발자가 서로 다른 프로젝트를 진행할 때, 각자 자신의 시스템에 필요한 서비스를 직접 설치해야 했습니다. 개발자마다 리눅스, macOS, Windows처럼 사용하는 운영체제가 다르다 보니, 설정 과정도 제각각이었습니다. 이 때문에 시간이 많이 들고 오류도 자주 발생했으며, 특히 여러 서비스를 반복해서 세팅해야 할 때는 새로운 환경을 구성하는 일이 큰 골칫거리였습니다.
Docker는 이런 상황을 완전히 바꾸어 놓았습니다. 이제는 모든 서비스와 의존성을 하나씩 직접 설치하는 대신, Docker 명령 한 줄만 실행해서 컨테이너를 띄우면 됩니다. 각 컨테이너는 자신에게 필요한 모든 것을 갖춘 격리된 환경이기 때문에, Windows든 macOS든 Linux든 어떤 머신에서도 동일하게 동작합니다. 덕분에 협업이 훨씬 매끄러워지고, 서로 다른 설정, 누락된 의존성, 버전 불일치 때문에 생기던 병목 현상이 사라집니다.
요약하면, Docker는 애플리케이션과 그 의존성을 하나의 이식 가능한 컨테이너로 포장해서 어디서나 동일한 방식으로 실행할 수 있도록 해주는 플랫폼입니다.
Docker vs 가상 머신
Docker와 가상 머신(VM)은 모두 애플리케이션을 “가상” 환경에서 실행하는 방법이지만, 동작 방식은 서로 다릅니다. 이런 차이를 이해하려면, 먼저 컴퓨터가 소프트웨어를 어떻게 실행하는지 간단히 살펴볼 필요가 있습니다.
레이어를 빠르게 훑어보면:
- 커널(Kernel): 운영체제에서 CPU, 메모리, 디스크 같은 하드웨어와 직접 통신하는 부분입니다. 앱과 컴퓨터 사이를 이어 주는 중개자라고 생각하면 됩니다.
- 애플리케이션 계층(Application layer): 실제 프로그램과 앱이 실행되는 영역입니다. 커널 위에 올라가 있으며, 커널을 통해 하드웨어 자원에 접근합니다.
이제 가상 머신을 좀 더 자세히 보겠습니다. VM은 운영체제 전체를 가상화하기 때문에, 자체 커널과 자체 애플리케이션 계층을 모두 포함합니다. VM을 하나 다운로드한다는 것은, 사실상 수 기가 바이트에 이르는 완전한 운영체제를 내 컴퓨터 안에 또 하나 들여오는 것과 같습니다.
자체 OS를 부팅해야 하므로 VM은 시작 속도가 느립니다. 하지만 필요한 것을 모두 가지고 있기 때문에 호스트 환경과 상관없이 거의 어떤 시스템에서도 잘 돌아가는 높은 호환성을 제공합니다.
반면 Docker는 전체 OS가 아니라 애플리케이션 계층만 가상화합니다. 컨테이너는 호스트 시스템의 커널을 공유하지만, 애플리케이션이 필요로 하는 의존성, 라이브러리, 설정 파일 등은 컨테이너 안에 함께 포함합니다.
Docker 이미지는 보통 수 메가바이트 수준으로 작고, 자체 OS를 부팅하지 않기 때문에 컨테이너는 거의 즉시 시작됩니다. Docker가 설치되어 있기만 하면, 호스트가 어떤 운영체제이든(Windows, macOS, Linux) 컨테이너는 동일하게 실행될 수 있습니다.
간단히 정리하면:
- VM은 내 컴퓨터 안에 “또 하나의 완전한 컴퓨터”를 띄우는 느낌으로, 크고 무겁고 느립니다.
- Docker 컨테이너는 애플리케이션을 담은 독립 패키지에 가까워, 작고 빠르며 이식성이 좋습니다.
간단한 비교 표입니다:
| 기능 | 가상 머신 | Docker 컨테이너 |
| 크기 | GB 단위 (큼) | MB 단위 (작음) |
| 시작 속도 | 느림 | 빠름 |
| OS 계층 | 전체 OS + 커널 | 호스트 커널을 공유 |
| 이식성 | 호환되는 호스트에서 실행됨 | 도커가 설치된 곳이라면 어디든 실행됨 |
Docker 설치
이제 Docker가 무엇인지 알게 되었으니, 직접 내 컴퓨터에서 Docker를 실행해 봅시다. Docker는 Windows, macOS, Linux에서 모두 동작하지만, 운영체제마다 설치 과정에 약간씩 차이가 있습니다. Docker 공식 문서에는 모든 운영체제별 설치 방법이 정리되어 있습니다.
시각적으로 보는 것을 더 선호한다면, Windows와 Linux에서 Docker를 설치하는 과정을 단계별로 보여주는 YouTube 영상도 참고할 수 있습니다.
간단한 설치 로드맵은 다음과 같습니다:
먼저 시스템 요구 사항을 확인합니다. Docker는 모든 컴퓨터에서 돌아가는 것이 아니므로, 사용하는 OS 버전이 지원 대상인지 반드시 확인해야 합니다.(공식 문서에 체크리스트가 있습니다.)
- Windows 및 macOS 사용자:
- 새로운 시스템: Docker Desktop을 다운로드해 설치하는 것이 가장 쉽습니다.
- 구형 시스템: Docker Desktop을 지원하지 않는 경우(예: Hyper-V 미지원, 오래된 OS 버전 등)에는 Docker Toolbox를 사용할 수 있습니다. Toolbox는 가벼운 가상 머신 위에 Docker를 올려 주기 때문에, 오래된 컴퓨터에서도 컨테이너를 실행할 수 있습니다.
2. Linux 사용자
- 보통 각 배포판의 패키지 관리자(예: Ubuntu/Debian은
apt, CentOS/Fedora는yum등)를 사용해 Docker를 설치합니다. 공식 문서에 배포판별 설치 명령이 정리되어 있습니다.
설치가 끝나면, 터미널이나 명령 프롬프트를 열고 다음 명령어를 입력해 설치를 확인합니다:
docker --version
여기에 Docker 버전 정보가 출력되면, 축하합니다! 정상적으로 설치가 완료된 것입니다.

이제 도커가 설치되었으니, 컨테이너를 실행하고, 이미지를 가져오고, 애플리케이션을 안전하고 격리된 환경에서 실험할 준비가 된 것입니다.
초보자를 위한 팁:
만약 구형 시스템에서 Docker Toolbox를 사용 중이라면, 명령어 자체는 대부분 동일하지만, Docker Quickstart Terminal 안에서 명령을 실행해야 합니다. 이 터미널이 가상 머신 환경을 자동으로 설정해 주기 때문입니다.
기본 Docker 명령어
지금까지 우리는 ‘이미지(images)’와 ‘컨테이너(containers)’라는 용어를 여러 번 사용해 왔습니다. 때로는 이 두 용어를 서로 바꿔 쓰기도 했습니다. 하지만 이 둘 사이에는 중요한 차이가 있습니다.
- Docker 이미지: 이미지는 설계도(blueprint)나 패키지라고 생각하면 됩니다. 앱이 실행되는 데 필요한 코드, 라이브러리, 의존성, 설정 등이 모두 들어 있지만, 아직 “실행 중”인 상태는 아닙니다.
- Docker 컨테이너: 컨테이너는 이미지를 실제로 실행한 “실행 인스턴스”입니다. 컨테이너를 시작하면, Docker가 이미지를 가져와 자신만의 격리된 환경에서 그 내용을 실행합니다.
이렇게 기억하면 도움이 됩니다: 이미지(image)는 레시피이고, 컨테이너(container)는 케이크입니다. 하나의 레시피(이미지)로 케이크(컨테이너)를 여러 개 만들 수 있는 것입니다.
중요한 점: Docker Hub에는 컨테이너가 아니라 이미지가 저장됩니다. 따라서 Docker Hub에서 무언가를 pull 한다는 것은 이미지를 다운로드한다는 뜻입니다.
예를 들어:
docker pull redis
(명령어를 입력하면) 아래 내용을 확인할 수 있습니다.

이 명령은 Redis 이미지를 내 컴퓨터로 다운로드받습니다. 다운로드가 끝나면, 다음 명령으로 로컬에 있는 모든 이미지를 확인할 수 있습니다.
docker images

이제 필요할 때마다 이미지에서 컨테이너를 시작할 수 있습니다.
docker run -d --name my-redis redis
이 명령은 방금 내려받은 redis 이미지로부터, my-redis 라는 컨테이너를 하나 띄웁니다.
docker run은 도커에게 “이 이미지로 새 컨테이너를 시작하라”고 지시하는 명령입니다.d는 “detached mode”의 약자로, 컨테이너를 백그라운드에서 돌려서 터미널을 계속 쓸 수 있게 해 줍니다.--name my-redis는 도커가 임의의 이름을 붙이는 대신my-redis라는 알아보기 쉬운 이름을 컨테이너에 지정합니다. 이렇게 하면 나중에 관리가 더 편해집니다.redis는 컨테이너를 시작하는 데 사용할 이미지 이름입니다.
현재 실행 중인 컨테이너를 모두 보고 싶다면:
docker ps

이 명령은 다음과 같은 정보를 포함해 컨테이너 목록을 보여 줍니다.
- 컨테이너 ID
- 이름
- 상태(실행 중인지, 중지 상태인지)
- 어떤 이미지로부터 실행되고 있는지 등
실행 중이 아닌 컨테이너까지 모두 보고 싶다면, -a 옵션을 추가합니다.
docker ps -a
이미지 버전 지정 방법:
기본적으로 Docker는 이미지의 최신 버전을 가져옵니다. 하지만 특정 버전이 필요할 때는 콜론(:) 뒤에 버전 태그를 붙여서 지정할 수 있습니다. 예를 들어:
docker pull redis:7.2
docker run -d --name my-redis redis:7.2어떤 버전들이 있는지 알고 싶다면 Docker Hub나 관련 페이지에서 태그 목록을 확인할 수 있습니다. 또한 로컬에서 docker images 명령을 실행하면, 내려받은 이미지들과 각 버전을 확인할 수 있습니다.
컨테이너 중지, 시작, 삭제 방법:
실행 중인 컨테이너를 멈추고 싶다면:
docker stop my-redis
다시 시작하려면:
docker start my-redis
더 이상 필요 없는 컨테이너는 삭제할 수 있습니다.
docker rm my-redis
컨테이너 재시작 방법
무언가 충돌이 발생했거나, 새 설정을 반영하고 싶거나, 단순히 한 번 새로고침하고 싶을 때는 컨테이너 ID(또는 이름)으로 재시작할 수 있습니다. 예를 들어:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c002bed0ae9a redis "docker-entrypoint.s…" 3 minutes ago Up 3 minutes 6379/tcp my-redis이 컨테이너를 다음처럼 재시작할 수 있습니다.
docker restart c002bed0ae9a
또는 이름으로:
docker restart my-redis
기억해 두면 좋은 다른 방법들입니:
- 먼저 중지 후 시작
docker stop c002bed0ae9adocker start c002bed0ae9a - 로그를 보면서 시작
docker start c002bed0ae9a && docker logs -f c002bed0ae9a

여러 Redis 컨테이너를 실행하고 포트를 이해하는 방법
현재 Redis 컨테이너가 한개 실행 중인 상태라고 가정해 보겠습니다:
docker ps
출력 예시는 다음과 같습니다:
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
c002bed0ae9a redis "docker-entrypoint.s…" Up 20 minutes 6379/tcp my-redisPORTS 열을 보면 6379/tcp 로 표시되어 있습니다. 이는 컨테이너 내부에서 Redis가 기본 포트 6379로 실행 중임을 의미합니다. 기본적으로 이 포트는 컨테이너 내부 포트이며, 명시적으로 지정하지 않는 한 여러분의 컴퓨터(호스트)에는 자동으로 노출되지 않습니다. Docker는 사용자가 명시적으로 지정해야만 포트를 매핑합니다.
동일한 포트에서 다른 Redis 컨테이너 실행 시도하기
다음과 같이 시도하면:
docker run -d --name my-redis2 redis:7.4.7-alpine
첫 번째 컨테이너가 이미 호스트 포트 6379를 사용하고 있기 때문에 매핑에 실패합니다. 여기서 포트 바인딩이 필요합니다.
포트 바인딩이란 무엇인가?
포트 바인딩(포트 매핑이라고도 함)은 Docker가 컨테이너 내부의 포트를 호스트 머신(여러분의 노트북/데스크톱/서버)의 포트에 연결하는 데 사용되는 원리입니다.
포트 바인딩이 없으면 컨테이너 내부에서 실행되는 모든 서비스는 완전히 격리됩니다: 내부 포트(예: Redis는 6379, Node.js 앱은 3000, MongoDB는 27017)에서 수신 대기할 수 있지만, 브라우저, 컴퓨터의 다른 앱, 심지어 다른 네트워크에 있는 다른 컨테이너를 포함하여 컨테이너 외부의 어떤 것도 이에 접근할 수 없습니다.
- 컨테이너 포트: 앱이 실행되고 있는 컨테이너 내부의 포트(Redis는 기본적으로
6379) - 호스트 포트: 해당 컨테이너에 액세스하는 데 사용하려는 컴퓨터의 포트.
Docker는 -p 플래그를 사용하여 컨테이너 포트를 다른 호스트 포트에 매핑할 수 있게 해줍니다.
다른 호스트 포트에서 두 번째 Redis 컨테이너 실행하기
docker run -d --name my-redis2 -p 6380:6379 redis:7.4.7-alpine
-p 6380:6379는 호스트 포트 6380을 컨테이너 포트 6379에 매핑(연결)합니다.
- 이제
localhost:6380을 사용하여 두 번째 컨테이너의 Redis에 연결할 수 있습니다. - 컨테이너 내부에서 Redis는 여전히 포트 6379에서 실행됩니다.
두 컨테이너를 확인해보세요:
docker ps
출력은 다음과 같습니다:
CONTAINER ID IMAGE STATUS PORTS NAMES
c002bed0ae9a redis Up 20 minutes 6379/tcp my-redis
d123abcd5678 redis Up 1 minute 0.0.0.0:6380->6379/tcp my-redis2
첫 번째 컨테이너는 6379에서 내부적으로 실행되고(호스트 포트가 노출되지 않음), 두 번째 컨테이너는 호스트 포트 6380이 컨테이너 포트 6379로 트래픽을 전달하도록 매핑되어 있습니다.
각 컨테이너를 전화선(컨테이너 포트)이 있는 방으로 생각해보세요.
- 외부(호스트)에서 그 방으로 전화를 걸고 싶습니다.
- 두 개의 방에 동시에 같은 외부 전화선을 사용할 수 없습니다.
- 포트 바인딩을 사용하면 내부 전화번호가 같더라도, 각 방에 다른 외부 회선을 할당할 수 있습니다.
포트 바인딩이 존재하는 이유
- 호스트의 포트 충돌 방지: 컴퓨터에서 한 번에 하나의 프로세스만 지정된 포트를 사용할 수 있습니다. 이미 호스트 포트 6379를 사용하는 Redis 컨테이너가 하나 있다면, 두 번째 컨테이너도 같은 호스트 포트에 바인딩할 수 없습니다. 포트 바인딩을 사용하면 각각을 다른 호스트 포트(6379 → 6380, 6381 등)에 매핑하여 여러 동일한 컨테이너를 나란히 실행할 수 있습니다.
- 호스트에서 컨테이너화된 서비스에 액세스: 브라우저, Postman, MongoDB Compass, redis-cli, curl 등은 모두 호스트에서 실행됩니다. -p 플래그 없이는 컨테이너 내부의 서비스와 통신할 방법이 없습니다.
- 선택적 노출: 컨테이너가 사용하는 모든 포트를 노출할 필요는 없습니다. 실제로 외부에서 필요한 포트만 매핑하여 나머지는 비공개 상태로 안전하게 유지할 수 있습니다.
또한 개발 및 프로덕션 환경에서 더 많은 유연성을 제공합니다. 개발 환경에서는 컨테이너 3000을 호스트 3000에 매핑할 수 있습니다. 하지만 프로덕션 환경에서는(예: 리버스 프록시 뒤에서) 컨테이너 3000을 호스트 80 또는 443에 매핑하거나, 전혀 노출하지 않고 다른 컨테이너가 Docker의 내부 네트워크를 통해 통신하도록 할 수 있습니다.
컨테이너 탐색 방법
컨테이너를 탐색하려면 다음을 실행하세요:
docker exec -it my-redis2 /bin/sh
docker exec는 컨테이너에서 명령을 실행합니다.it대화형 터미널(입력하고 출력을 볼 수 있게 해줌)./bin/sh는 컨테이너 내부에서 셸을 시작합니다.
내부에 들어가면 프롬프트가 다음과 같이 변경됩니다:
/data #
이제 호스트 머신에 영향을 주지 않고 전부 컨테이너 내부에서 파일을 나열하고, 디렉토리를 탐색하거나, 프로그램을 실행할 수 있습니다.

docker run vs docker start
이 문서 전체에서 docker run과 docker start를 사용해 왔는데, 그 차이점이 중요한 이유는 다음과 같습니다:
- 우발적인 중복 방지: 매번
docker run을 사용하면 새 컨테이너가 생성됩니다. 이미 설정한 것을 단지 다시 시작하려면docker start가 더 빠르고 안전합니다. - 구성 유지:
docker start는 컨테이너의 원래 설정, 포트, 볼륨 및 이름 등을 보존하므로 옵션을 변경하여 문제가 발생할 위험이 없습니다. - 여러 컨테이너로 효율적으로 작업: 여러 서비스 또는 동일한 앱의 다른 버전을 실행할 때,
run과start를 언제 사용할지 아는 것은 리소스를 관리하고 포트 충돌을 피하며 워크플로우를 원활하게 유지하는 데 도움이 됩니다. - 워크플로우 속도 향상: 기존 컨테이너를 시작하는 것은 거의 즉각적이지만, 새 컨테이너를 생성하는 것은 약간 더 오래 걸립니다.
요약하자면 docker run = 새로운 것을 생성, docker start = 이미 가지고 있는 것을 다시 시작하는 것입니다.
JavaScript로 연습하기
이제 핵심 Docker 개념을 다루었으니 실제로 적용해봅시다. 이 섹션에서는 다음으로 구성된 간단한 JavaScript 프로젝트를 컨테이너화합니다:
- 프론트엔드: HTML, CSS 및 JavaScript로 구축
- 백엔드: 간단한 Node.js 서버(
server.js) - 데이터베이스: Docker Hub에서 직접 가져온 MongoDB 인스턴스
- MongoDB용 UI: Mongo Express를 사용하여 데이터베이스를 시각화하고 관리
이 예제는 Docker가 어떻게 격리되고 일관된 환경에서 코드, 종속성 및 서비스 등을 포함한 애플리케이션의 여러 컴포넌트(구성 요소)를 관리하는지를 보여줍니다.
GitHub에서 스타터 프로젝트를 가져올 수 있습니다.
또는 터미널을 사용하여 직접 클론하세요:
git clone https://github.com/Oghenekparobo/docker_tut_js.git
cd docker_tut_js
여기에는 Node.js 백엔드와 함께 기본 HTML 및 JavaScript 파일이 포함되어 있습니다.
다음으로 데이터베이스 설정을 준비합니다. Docker Hub로 이동하여 검색창에 "mongo"를 입력하세요. Docker에서 게시한 공식 MongoDB 이미지가 표시됩니다.

MongoDB 이미지를 가져오는 방법
이제 Docker Hub에서 공식 MongoDB 이미지를 탐색했으니 실제로 로컬 환경으로 가져와봅시다.
터미널을 열고 프로젝트 디렉토리(예: docker_tut_js)로 이동한 다음 실행하세요:
docker pull mongo
이 명령은 Docker에게 Docker Hub에서 최신 버전의 MongoDB 이미지를 다운로드하도록 지시합니다.
다음과 유사한 출력이 표시됩니다:
Using default tag: latest
latest: Pulling from library/mongo
b8a35db46e38: Already exists
a637dbfff7e5: Pull complete
0c9047ace63c: Pull complete
02cd4cf70021: Pull complete
dfb5d357a025: Pull complete
007bf0024f67: Pull complete
67fd8af3998d: Pull complete
d702312e8109: Pull complete
Digest: sha256:7d1a1a613b41523172dc2b1b02c706bc56cee64144ccd6205b1b38703c85bf61
Status: Downloaded newer image for mongo:latest
docker.io/library/mongo:latest
무슨 일이 일어나고 있는지 설명하면:
- "Using default tag: latest": 특정한 버전이 제공되지 않았으므로 Docker가 MongoDB의 최신 버전을 가져옵니다.
- "Pulling from library/mongo": Docker의 공식 이미지 라이브러리에서 다운로드하고 있습니다.
- "Pull complete": 각 줄은 성공적으로 다운로드 중인 이미지의 레이어를 나타냅니다.
- "Downloaded newer image for mongo:latest": MongoDB 이미지가 이제 시스템의 로컬에 저장되었음을 확인합니다.
다음을 실행하여 사용 가능한지 확인할 수 있습니다:
docker images
repository 열에 mongo가 나열되어 있어야 합니다.

Mongo Express 이미지 가져오기 방법
이제 MongoDB 이미지가 준비되었으니 Mongo Express 이미지를 가져와봅시다.
Mongo Express는 브라우저를 통해 MongoDB 컬렉션을 보고 관리할 수 있는 경량 웹 기반 인터페이스로, phpMyAdmin이 MySQL에서 작동하는 방식과 유사합니다.
터미널을 열고(여전히 프로젝트 디렉토리에서) 다음을 실행하세요:
docker pull mongo-express
다음과 유사한 출력이 표시됩니다:
Using default tag: latest
latest: Pulling from library/mongo-express
b8a35db46e38: Already exists
a637dbfff7e5: Pull complete
4e0e0977e9c3: Pull complete
02cd4cf70021: Pull complete
Digest: sha256:3d6dbac587ad91d0e2eab83f09a5b31a1c8f9d91a8825ddaa6c7453c25cb4812
Status: Downloaded newer image for mongo-express:latest
docker.io/library/mongo-express:latest
이것이 의미하는 바는 다음과 같습니다:
docker pull mongo-express는 Docker Hub에서 공식 Mongo Express 이미지를 다운로드합니다.- 각 "Pull complete" 줄은 성공적으로 다운로드된 이미지의 레이어를 나타냅니다.
mongo-express:latest는 최신 버전이 이제 로컬에 저장되었음을 확인합니다.
두 이미지가 모두 사용 가능한지 확인하려면 다음을 실행하세요:
docker images
출력에 mongo와 mongo-express가 나열되어 있어야 합니다.

이제 두 이미지가 모두 다운로드되었으므로, 다음 단계는 MongoDB가 실행되고 접근 가능한지 확인한 다음, 브라우저를 통해 관리할 수 있도록 Mongo Express에 연결하는 것입니다.
그 전에, 이 두 컨테이너가 어떻게 통신할지 간단히 살펴봅시다.
Docker 네트워크
MongoDB와 Mongo Express가 별도의 컨테이너에서 실행될 때 서로 통신할 방법이 필요합니다. Docker는 Docker 네트워크라는 것을 사용하여 이를 처리합니다. 이는 내부 포트를 외부 세계에 노출하지 않고 컨테이너가 안전하게 통신할 수 있게 해주는 가상 브리지입니다.
Docker에서 컨테이너를 실행하면 자동으로 격리된 네트워크가 생성됩니다. 이를 마치 컨테이너들이 외부 세계에 모든 것을 노출하지 않고 안전하게 서로 통신할 수 있는 개인 공간처럼 생각하면 됩니다.
예를 들어, MongoDB 컨테이너와 Mongo Express 컨테이너가 동일한 Docker 네트워크에 있으면 컨테이너 이름(예: mongo 또는 mongo-express)만 사용하여 통신할 수 있습니다. Docker가 이를 내부적으로 처리하므로localhost나 포트 번호를 사용할 필요가 없습니다.
하지만 Docker 네트워크 외부의 모든 것(여러분의 호스트 머신이나 Node.js 앱 같은)은 노출된 포트를 통해 연결됩니다.
따라서 나중에 전체 애플리케이션, Node.js 백엔드, MongoDB, Mongo Express, 심지어 프론트엔드(index.html)까지 Docker로 패키징하면, 이 모든 컨테이너가 Docker 네트워크를 통해 원활하게 상호 작용합니다. 그러면 컴퓨터의 브라우저는 우리가 노출한 호스트 주소와 포트를 사용하여 Node.js 앱에 연결됩니다.
기본적으로 Docker는 이미 몇 가지 내장 네트워크를 제공합니다. 다음을 실행하여 확인할 수 있습니다:
docker network ls
다음과 같은 결과가 나올 것입니다:
NETWORK ID NAME DRIVER SCOPE
712a7144f1a0 bridge bridge local
4ae27eedea5b host host local
4806000201ce none null local
이것들은 Docker에 의해 자동으로 생성됩니다. 지금은 이것들에 대해 너무 걱정할 필요가 없습니다 – 우리는 커스튬(자체 사용자 정의) 네트워크를 만드는 데 집중하겠습니다.
설정을 위해 MongoDB와 Mongo Express가 공유할 수 있는 별도의 네트워크를 만들겠습니다. 이를 mongo-network라고 부르겠습니다:
docker network create mongo-network

Mongo 컨테이너 실행 방법
MongoDB와 Mongo Express 컨테이너가 통신할 수 있도록 하려면 동일한 Docker 네트워크 내에서 실행해야 합니다. 그래서 앞서 mongo-network를 만든 것입니다.
MongoDB부터 시작하겠습니다. docker run 명령은 이미지에서 컨테이너를 시작하는 데 사용된다는 것을 기억하세요. 이번에는 공식 MongoDB 이미지를 실행하고 네트워크에 연결할 것입니다.
또한 컨테이너 외부에서 접근할 수 있도록 기본 MongoDB 포트 27017을 노출하고, 루트 사용자 이름과 비밀번호에 대한 환경 변수를 설정합니다.
다음은 명령어입니다:
docker run -p 27017:27017 -d \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=password \
--name mongo \
--network mongo-network \
mongo
각 부분이 하는 일은 다음과 같습니다:
-p 27017:27017은 컨테이너의 MongoDB 포트를 호스트 머신에 매핑합니다.-d는 컨테이너를 분리 모드(백그라운드)에서 실행합니다.-e는 데이터베이스의 루트 크레덴셜(루트 자격 증명)에 대한 환경 변수를 설정합니다.--name mongo는 컨테이너에 사용자 정의 이름을 부여해 더 쉽게 참조할 수 있도록 합니다.--network mongo-network는 우리가 만든 네트워크에 컨테이너를 연결합니다.
성공적으로 실행되면 MongoDB 인스턴스가 Docker 네트워크 내에서 실행되며, Mongo Express와 같은 다른 컨테이너가 연결할 준비가 됩니다.
MongoDB 컨테이너를 생성한 후 실행 중이고 정상인지 쉽게 확인할 수 있습니다.
먼저 docker ps를 실행하여 모든 활성 컨테이너를 확인하세요. MongoDB 컨테이너(mongo)가 포트 27017이 노출된 상태로 나열되어야 합니다. 컨테이너 내부에서 무슨 일이 일어나고 있는지에 대한 자세한 정보를 얻으려면 docker logs mongo를 사용하거나, 원한다면 컨테이너 ID를 사용하여(예: docker logs 7abb38175ae28) 로그를 확인할 수 있습니다. 로그는 MongoDB의 시작 메시지를 보여줄 것이며, 데이터베이스가 성공적으로 시작되었고 연결될 준비가 되었음을 나타내는 명령줄을 찾아야 합니다.
이것은 Mongo Express와 같은 다른 서비스를 연결하기 전에 모든 것이 올바르게 작동하는지 확인하는 빠른 방법입니다.
docker ps
이것은 실행 중인 모든 컨테이너를 나열합니다. MongoDB 컨테이너(mongo)가 포트 27017이 노출된 상태로 표시되어야 합니다.
docker logs mongo 또는 컨테이너의 id 예시: docker logs 7abb38175ae283429354609866c8d97521f37b535c475ae448295f8fc0ed947f
이것은 시작 메시지를 보여줍니다. MongoDB가 성공적으로 시작되었고 연결될 준비가 되었음을 나타내는 줄을 찾아보세요.

Mongo Express 컨테이너 실행 방법
이제 MongoDB가 실행 중이므로 MongoDB 데이터베이스를 관리하고 보기 위한 웹 기반 인터페이스인 Mongo Express를 실행할 수 있습니다. MongoDB와 통신할 수 있도록 동일한 네트워크(mongo-network)에 연결하겠습니다.
다음은 명령어입니다:
docker run -d \
-e ME_CONFIG_MONGODB_ADMINUSERNAME=admin \
-e ME_CONFIG_MONGODB_ADMINPASSWORD=password \
-e ME_CONFIG_MONGODB_SERVER=mongo \
--name mongo-express \
--network mongo-network \
-p 8081:8081 \
mongo-express
각 부분이 하는 일은 다음과 같습니다:
-d는 컨테이너를 분리 모드(백그라운드)에서 실행합니다.-e ME_CONFIG_MONGODB_ADMINUSERNAME=admin은 Mongo Express가 사용할 MongoDB 어드민(관리자) 사용자 이름을 설정합니다.-e ME_CONFIG_MONGODB_ADMINPASSWORD=password는 해당 MongoDB 비밀번호를 설정합니다.-e ME_CONFIG_MONGODB_SERVER=mongo는 Mongo Express에 연결할 MongoDB 서버를 알려줍니다. 두 컨테이너가 동일한 네트워크에 있기 때문에 여기서는 컨테이너 이름mongo를 사용합니다.--name mongo-express는 더 쉽게 참조할 수 있도록 컨테이너에 친숙한 이름을 부여합니다.--network mongo-network는 MongoDB와 동일한 Docker 네트워크에 컨테이너를 연결하여 서로 통신할 수 있게 합니다.-p 8081:8081은 호스트 머신의 포트 8081에서 Mongo Express 웹 인터페이스를 노출합니다.mongo-express는 실행 중인 Docker 이미지의 이름입니다.
컨테이너가 실행되면 브라우저를 열고 http://localhost:8081에 접속하여 Mongo Express에 액세스하고 MongoDB 인스턴스와 상호작용할 수 있습니다.
사용 가능한 환경 변수 및 옵션에 대한 자세한 내용은 Mongo Express의 공식 Docker Hub 페이지에서 확인할 수 있습니다.
브라우저에서 http://localhost:8081을 열기 전에 Mongo Express 컨테이너가 제대로 실행되고 있는지 확인하는 것이 좋습니다. 로그를 보면 확인할 수 있습니다:
docker logs <container-id>
# 또는
docker logs mongo-express
다음과 유사한 출력이 표시되어야 합니다:
Waiting for mongo:27017...
No custom config.js found, loading config.default.js
Welcome to mongo-express 1.0.2
------------------------
Mongo Express server listening at http://0.0.0.0:8081
Server is open to allow connections from anyone (0.0.0.0)
basicAuth credentials are "admin:pass", it is recommended you change this in your config.js!
이것은 Mongo Express가 실행 중이며 MongoDB 인스턴스에 연결할 준비가 되었음을 확인하는 것입니다.
로그에 표시된 basicAuth 자격 증명(admin:pass)을 기록해두세요. 이러한 자격 증명이 있는 경우 브라우저에서 Mongo Express에 액세스할 때 사용해야 합니다. 나중에 보안을 강화하기 위해 사용자 정의 config.js 파일에서 변경할 수 있습니다.
로그에서 모든 것이 정상으로 보이면 http://localhost:8081에 안전하게 접속하여 Mongo Express 인터페이스에 액세스할 수 있습니다.

Mongo Express에 액세스할 때 브라우저에서 사용자 이름과 비밀번호를 묻는 경우 컨테이너 로그에 표시된 basicAuth 자격 증명을 사용하세요:
사용자 이름: admin
비밀번호: pass이것들은 기본 자격 증명이며, 보안을 강화하기 위해 나중에 사용자 정의 config.js 파일에서 변경하는 것을 강력히 권장합니다.
Mongo Express를 열면 이미 생성된 일부 기본 데이터베이스가 표시됩니다. 이 프로젝트에서는 todos라는 새 데이터베이스를 만들 것입니다. 생성되면 Node.js 애플리케이션이 이 데이터베이스에 연결하여 데이터를 저장하고 검색할 수 있습니다.
Node.js를 MongoDB에 연결하는 방법
이미 Docker 컨테이너(mongo) 내에서 MongoDB가 실행되고 있습니다. 컨테이너는 기본 MongoDB 포트 27017을 호스트에 노출하므로 노트북/데스크톱의 모든 프로세스가 localhost:27017을 통해 접근할 수 있습니다.
중요: Node.js 앱은 Docker 외부에 있습니다(터미널에서 시작하는 일반 node server.js 프로세스입니다).
앱이 외부에 있기 때문에 호스트 이름으로 컨테이너 이름 mongo가 아닌 localhost(또는 127.0.0.1)를 사용해야 합니다.
나중에 Node.js 앱을 컨테이너화하고 동일한 Docker 네트워크에 배치하면 호스트를 mongo로 전환할 것입니다. 지금은 localhost로 유지하세요.
Node.js 백엔드
다음은 MongoDB를 사용하는 server.js 버전입니다:
const express = require("express");
const multer = require("multer");
const path = require("path");
const fs = require("fs");
const { MongoClient, ObjectId } = require("mongodb");
const app = express();
const PORT = 3000;
// Host = localhost → 노출된 포트를 통해 MongoDB 컨테이너와 통신
// Port = 27017 → 기본 MongoDB 포트
// User / Pass → admin / password (컨테이너에 제공한 자격 증명)
const mongoUrl = "mongodb://admin:password@localhost:27017";
const dbName = "todos";
let db;
MongoClient.connect(mongoUrl)
.then((client) => {
db = client.db(dbName);
console.log("Connected to MongoDB →", dbName);
})
.catch((err) => console.error("MongoDB connection error:", err));
const uploadDir = path.join(__dirname, "uploads");
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadDir),
filename: (req, file, cb) => {
const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);
cb(null, "photo-" + unique + path.extname(file.originalname));
},
});
const upload = multer({ storage });
app.use(express.static(__dirname));
app.use("/uploads", express.static(uploadDir));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get("/todos", async (req, res) => {
const todos = await db.collection("todos").find().toArray();
res.json(todos);
});
app.post("/todos", upload.single("photo"), async (req, res) => {
const text = req.body.text?.trim();
if (!text) return res.status(400).json({ error: "Text required" });
const todo = {
text,
image: req.file ? `/uploads/${req.file.filename}` : null,
createdAt: new Date(),
};
const result = await db.collection("todos").insertOne(todo);
todo._id = result.insertedId;
res.json(todo);
});
// 서버 시작
app.listen(PORT, () => {
console.log(`Server → http://localhost:${PORT}`);
});
프론트엔드
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Todo + Image</title>
<style>
body {
font-family: sans-serif;
margin: 2rem;
max-width: 800px;
}
.todo {
border: 1px solid #ccc;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 8px;
}
.todo img {
max-height: 150px;
margin-top: 0.5rem;
}
.error {
color: red;
}
input[type="text"] {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.5rem;
}
#preview {
max-width: 300px;
margin-top: 0.5rem;
display: none;
}
</style>
</head>
<body>
<h1>Todo List with Images</h1>
<div id="addForm">
<input type="text" id="textInput" placeholder="What needs to be done?" />
<input type="file" id="imageInput" accept="image/*" />
<img id="preview" alt="preview" />
<button id="addBtn">Add Todo</button>
<p id="status"></p>
</div>
<h2>Todos</h2>
<div id="todos"></div>
<script>
const $ = document.querySelector.bind(document);
const textInput = $("#textInput");
const imageInput = $("#imageInput");
const preview = $("#preview");
const addBtn = $("#addBtn");
const status = $("#status");
const todosDiv = $("#todos");
imageInput.addEventListener("change", () => {
const file = imageInput.files[0];
if (!file) {
preview.style.display = "none";
return;
}
const reader = new FileReader();
reader.onload = (e) => {
preview.src = e.target.result;
preview.style.display = "block";
};
reader.readAsDataURL(file);
});
addBtn.addEventListener("click", async () => {
const text = textInput.value.trim();
if (!text) {
status.textContent = "Please enter a todo text.";
status.className = "error";
return;
}
const form = new FormData();
form.append("text", text);
if (imageInput.files[0]) form.append("photo", imageInput.files[0]);
try {
const res = await fetch("/todos", { method: "POST", body: form });
const data = await res.json();
if (!res.ok) throw new Error(data.error || "failed");
status.textContent = "Todo added!";
status.className = "";
textInput.value = "";
imageInput.value = "";
preview.style.display = "none";
loadTodos(); // 목록 새로고침
} catch (err) {
status.textContent = "Error: " + err.message;
status.className = "error";
}
});
async function loadTodos() {
const res = await fetch("/todos");
const todos = await res.json();
todosDiv.innerHTML = "";
todos.forEach((t) => {
const div = document.createElement("div");
div.className = "todo";
div.innerHTML = `<strong>${escapeHtml(t.text)}</strong>`;
if (t.image) {
div.innerHTML += `<br><img src="${t.image}" alt="todo image">`;
}
todosDiv.appendChild(div);
});
}
function escapeHtml(s) {
const div = document.createElement("div");
div.textContent = s;
return div.innerHTML;
}
loadTodos();
</script>
</body>
</html>
이제 Node.js 앱이 Docker에서 실행 중인 MongoDB 컨테이너에 연결할 수 있습니다. 앱이 현재 Docker 외부에서 실행되고 있으므로 설정한 자격 증명(admin / password)을 사용하여 localhost:27017을 통해 연결합니다.
연결되면 Node.js 백엔드는 메모리 내 배열을 대체하여 MongoDB의 todos 데이터베이스에서 직접 todos를 저장하고 검색합니다. 나중에 Node.js 앱을 컨테이너화하고 MongoDB와 동일한 Docker 네트워크에 배치하면 호스트를 localhost에서 컨테이너 이름 mongo로 전환할 수 있습니다. 우리는 그 방향으로 가고 있습니다.
코드를 실행하고 설정에 맞게 수정할 준비가 된 전체 백엔드 및 프론트엔드 코드는 이곳에서 얻을 수 있습니다: GitHub 저장소.
Docker Compose 사용 방법
이제 Node.js 앱이, 컨테이너 안에서 실행 중인 MongoDB와 Mongo Express에 연결되었습니다. 우리는 네트워크를 만들고 컨테이너를 시작했으며, 모든 서비스가 완벽하게 통신하고 있습니다.
하지만 솔직히 말해서: 매번 길고 복잡한 docker run 명령어를 입력하기란 꽤 번거롭습니다. 명령어 한 번에 모든 것을 실행할 수 있는 간단하고 깔끔한 방법이 있으면 좋겠죠? 바로 Docker Compose가 그 역할을 합니다.
Docker Compose는 하나의 명령으로, 여러 컨테이너로 구성된 애플리케이션을 정의하고 실행할 수 있는 도구입니다. 여러 개의 docker run 명령을 수동으로 실행하는 대신, 간단한 docker-compose.yml 파일에 각 서비스(예: Node.js 앱, MongoDB, Mongo Express)와 해당 구성, 환경 변수, 공유 네트워크를 지정하여, 설정을 정의할 수 있습니다.
즉, 여러 컨테이너를 하나의 프로젝트처럼 관리할 수 있게 도와주며,
시작, 중지, 유지 보수를 모두 하나의 파일과 명령으로 처리할 수 있습니다.
docker-compose.yml 또는 docker-compose.yaml 중 어느 것이나 사용이 가능하지만, 관례상 .yml 확장자가 더 일반적입니다.
Docker는 실행 시 이를 자동으로 감지합니다:
docker compose up
네, 관례대로 docker-compose.yml 파일을 사용하는 것이 좋습니다.
이제 MongoDB와 Mongo Express 컨테이너를 실행하려면 각각 다음 두 명령어를 사용하면 됩니다.
# MongoDB container
docker run -p 27017:27017 -d \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=password \
--name mongo \
--network mongo-network \
mongo
# Mongo Express container
docker run -d \
-e ME_CONFIG_MONGODB_ADMINUSERNAME=admin \
-e ME_CONFIG_MONGODB_ADMINPASSWORD=password \
-e ME_CONFIG_MONGODB_SERVER=mongo \
--name mongo-express \
--network mongo-network \
-p 8081:8081 \
mongo-express
이제 매번 이러한 긴 명령어를 입력하는 대신, Docker Compose 파일을 사용하여 모든 명령어를 한 번에 실행할 것입니다.
docker-compose.yml 파일은 Node.js 프로젝트의 루트 디렉터리에 위치해야 합니다.

docker-compose.yml 파일은 다음과 같습니다:
version: "3.8"
services:
mongodb:
image: mongo
container_name: mongo
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
mongo-express:
image: mongo-express
container_name: mongo-express
ports:
- "8081:8081"
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: admin
ME_CONFIG_MONGODB_ADMINPASSWORD: password
ME_CONFIG_MONGODB_SERVER: mongodb
depends_on:
- mongodb
지금 무슨 일이 벌어지고 있는지 하나씩 살펴보도록 하겠습니다.
version: "3.8"→ Compose 파일의 버전을 정의합니다. 각 버전마다 조금씩 다른 문법 규칙과 기능이 있으며, 3.8 버전은 최신 Docker Engine과 호환되는 현대적인 버전입니다.services:→ 실행할 모든 컨테이너를 정의하는 영역입니다. 여기서는mongodb와mongo-express두 서비스입니다.
MongoDB 서비스
image: mongo: Docker Hub에서 공식 MongoDB 이미지를 가져옵니다.container_name: mongo: 컨테이너에 친근한 이름을 지정합니다.ports: "27017:27017": MongoDB의 기본 포트를 호스트에 노출하여 Node.js 또는 다른 애플리케이션이 연결할 수 있도록 합니다.environment: 초기 루트 사용자 이름과 비밀번호를 설정합니다.
Mongo Express 서비스
image: mongo-express: 공식 Mongo Express 이미지를 사용합니다.container_name: mongo-express: 컨테이너 이름을 더 쉽게 참조할 수 있도록 친근하게 지정합니다.ports: "8081:8081": 호스트의 8081 포트에서 Mongo Express 웹 인터페이스를 노출합니다.environment: Mongo Express에게 MongoDB를 연결하는 방법(사용자 이름, 비밀번호, 호스트)를 알려줍니다.depends_on: - mongodb: MongoDB가 먼저 실행된 후 Mongo Express가 즉시 시작되도록 보장합니다.
Docker Compose를 사용하는 이유
- 한 번의 명령으로 시작 가능: 여러 개의
docker run명령 대신, 그저 아래 명령어를 실행하세요:
docker compose up -d
- 자동 네트워킹: Compose는 기본 네트워크를 생성하여 서비스들이 서비스 이름(우의 경우
mongodb)을 사용하여 통신할 수 있도록 합니다. - 쉽고 유연한 유지보수: 모든 서비스를 함께 중지, 재시작, 재빌드할 수 있습니다.
새로운 docker-compose.yml 파일을 실행하기 전에 충돌을 일으키는 컨테이너가 현재 실행 중이지 않은지 확인하는 것이 중요합니다. 이전 docker run 명령으로 MongoDB와 Mongo Express가 이미 실행 중이었음을 기억하세요.
충돌을 방지하려면(이미 사용 중인 포트처럼) 실행 중인 컨테이너를 모두 중지하고 제거해야 합니다.
방법은 다음과 같습니다.
# 실행 중인 컨테이너 목록 보기
docker ps
# 개별 컨테이너 중지(<container_name> 부분을 mongo 또는 mongo-express로 대체)
docker stop mongo
docker stop mongo-express
# 중지된 컨테이너 삭제
docker rm mongo
docker rm mongo-express
# (선택) 실행 중인 모든 컨테이너 중지 및 삭제
docker stop $(docker ps -q)
docker rm $(docker ps -a -q)
docker ps명령은 현재 실행 중인 컨테이너들을 보여줍니다.docker stop <name>명령은 지정한 이름의 컨테이너를 정상적으로 중지합니다.docker rm <name>명령은 중지된 컨테이너를 Docker에서 삭제합니다.docker stop $(docker ps -q)명령은 현재 실행 중인 모든 컨테이너를 중지합니다.docker rm $(docker ps -a -q)명령은 실행 중이든 중지 중이든 모든 컨테이너를 삭제합니다.
이전 단계의 컨테이너를 모두 중지하고 제거했다면,
이제 포트 충돌 없이 안전하게 Docker Compose 환경을 실행할 준비가 된 것입니다.
이제 모든 기존 컨테이너가 중지되었으므로,docker-compose.yml 파일을 이용해 MongoDB와 Mongo Express를 함께 실행할 수 있습니다.
Node.js 프로젝트 루트(즉, docker-compose.yml 파일이 위치한 디렉터리)에서 다음 명령을 실행하세요:
docker compose up -d
이 명령이 하는 일:
docker compose→ Docker에게 Compose를 사용하라고 지시합니다.up→ Compose 파일에 정의된 모든 서비스를 빌드(필요 시)하고 시작합니다.-d→ 컨테이너를 백그라운드(분리 모드)로 실행시켜, 즉, (터미널을 차지하지 않고) 백그라운드에서 실행된다는 의미입니다.
이 명령을 실행하면 Docker는 MongoDB와 Mongo Express를 동시에 시작하고,
두 컨테이너를 같은 내부 네트워크에 연결하며, 각 서비스에 대해 우리가 지정한 포트를 노출합니다.(MongoDB는 27017, Mongo Express는 8081)
모든 것이 정상적으로 작동했다면, 다음 명령을 실행한 후:
docker compose up -d정상적으로 실행되었다면 다음과 유사한 출력이 나타납니다:
[+] Running 3/3
✔ Network docker_tut_default Created 0.0s
✔ Container mongo Started 0.6s
✔ Container mongo-express Started 0.8s
stephenjohnson@Oghenekparobo docker_tut %
이 출력이 의미하는 것
Network docker_tut_default Created→ Docker Compose가 서비스 간 통신을 위해 새 네트워크를 자동으로 생성했습니다.Container mongo Started→ MongoDB 컨테이너가 실행되었습니다.Container mongo-express Started→ Mongo Express 컨테이너가 실행되었습니다.
컨테이너가 실제로 실행 중인지 확인하려면 다음 명령을 입력합니다:
docker ps
이 명령은 현재 활성 상태의 모든 컨테이너를 나열합니다. mongo와 mongo-express 컨테이너가 각각 MongoDB(포트 27017)와 Mongo Express(포트 8081)로 표시되어 있어야 합니다.
- Mongo Express에 접속하려면 브라우저를 열고 http://localhost:8081로 이동하여 웹 인터페이스를 통해 MongoDB와 상호작용하세요.
- MongoDB에 접속하려면 Node.js 앱에서 Compose 파일에 설정한 자격 증명을 사용하여
localhost:27017로 MongoDB에 연결할 수 있습니다.
docker run 명령을 여러 번 사용하는 것과 비교했을 때, Docker Compose를 사용하는 것이 더 쉬운 이유는 다음과 같습니다:
- 여러 컨테이너를 한 번의 명령으로 실행할 수 있습니다.
- 컨테이너 간 자동 네트워크 설정이 이루어집니다.
- 나중에 컨테이너를 중지, 삭제, 재빌드하기가 훨씬 쉽습니다.
간단히 말해, Docker Compose는 모든 것을 단순화하고 체계화하여 개발 환경 관리를 훨씬 더 쉽게 만들어줍니다.

이 단계에서 알아야 할 중요한 점은 MongoDB에 추가하는 모든 데이터는 임시적이라는 것입니다. 컨테이너를 중지하거나 삭제한 뒤 다시 실행하면, 이전에 저장한 데이터가 모두 사라진 것을 확인할 수 있습니다. 이는 컨테이너 내부의 데이터가 기본적으로 영구 보존되지 않기 때문입니다.
걱정하지 마세요. 이것은 정상적인 동작입니다. 튜토리얼의 이후 단계에서 Docker Volumes 개념을 다루며 어떻게 데이터를 영구적으로 저장할 수 있는지 배울 예정입니다.
일단은 컨테이너를 재시작할 때마다 MongoDB가 초기 상태로 시작된다는 사실만 기억하시면 됩니다.
전체 예제(Dockerfile과 docker-compose 파일을 포함한 예시)는 이 GitHub 링크에서 확인하실 수 있습니다.
나만의 Docker 이미지 빌드하는 방법
이제 Node.js 애플리케이션을 로컬에서 테스트했고, MongoDB와 Mongo Express와도 완벽히 연동되어 작동하는 것을 확인했습니다. 다음 단계는 이제 배포를 위한 준비입니다.
로컬 환경에서 직접 애플리케이션을 실행하는 것은 개발 단계에서는 괜찮지만, 배포 환경이나 다른 서버로 옮기고자 할 때는 실용적이지 않습니다. Docker 이미지를 생성하여, 애플리케이션 코드, 의존성, 설정, 실행 환경을 하나의 이동 가능한 단위로 패키징할 수 있습니다. 이 이미지는 Docker가 설치된 어디서든 동일하게 실행될 수 있으며, 개발–테스트–운영 환경 간에 일관된 동작 보장이 가능합니다.
간단히 말해, Docker 이미지를 빌드한다는 것은 애플리케이션을 컨테이너화하고 배포 준비 상태로 만든다는 뜻입니다.
이번에는 우리가 만든 Todo 앱을 컨테이너화하기 위해 Dockerfile이 필요합니다.
Dockerfile은 Docker에게 이 애플리케이션으로 이미지를 어떻게 빌드할지를 알려주는 청사진(blueprint)입니다. 이 설정은 기본 환경을 정의하고, 애플리케이션 코드를 복사하며, 필요한 의존성을 설치하고, 애플리케이션이 어떻게 시작될지를 지정합니다. 이 청사진을 통해 Docker는 어떤 머신에서든 동일하게 동작하는 일관된 이미지를 생성할 수 있으며, 우리의 Node.js 애플리케이션을 완전히 이식 가능하고 배포 준비가 완료된 상태로 만들어 줍니다.
우리의 Dockerfile에서는 대문자 D를 사용하는 점에 주의하세요. 이는 표준 명명 규칙입니다. 이 파일은 Node.js 프로젝트의 루트 디렉토리에 위치시켜야 합니다. 단순한 프로젝트의 경우, server.js 또는 index.js 같은 메인 파일과 package.json이 루트에 함께 위치합니다. Docker는 이 파일을 청사진으로 사용하여 애플리케이션의 컨테이너 이미지를 빌드합니다.
만약 메인 앱 파일이 하위 폴더에 있다면 괜찮습니다. 단, Dockerfile 내 COPY 및 CMD 명령에서 정확한 경로를 지정해야 합니다. 중요한 점은 Dockerfile이 프로젝트 루트에 있어야 Docker가 여러분의 앱 빌드 기준점을 인식할 수 있다는 것입니다.
우리의 Dockerfile 내용은 다음과 같습니다:
# Use full Node 18 (Debian-based)
FROM node:18
# Set environment variables
ENV MONGO_DB_USERNAME=admin \
MONGO_DB_PASSWORD=password
# Set working directory
WORKDIR /home/app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Expose port
EXPOSE 3000
# Start the app
CMD ["node", "server.js"]
여기서 어떤 일이 일어나고 있는지 살펴보겠습니다:
FROM node:13-alpine은 컨테이너의 기본 이미지로서 Node.js가 이미 설치되어 있으며 매우 가볍기 때문에 이미지를 작게 유지할 수 있습니다.ENV MONGO_DB_USERNAME=admin \ MONGO_DB_PASSWORD=password는 컨테이너 내부에서 환경 변수를 설정하여 Node.js 애플리케이션이 MongoDB에 연결할 수 있게 합니다.WORKDIR /home/app은 컨테이너 내부에서의 작업 디렉터리를 지정합니다. 이후 실행되는COPY,RUN등의 명령은 이 폴더 기준으로 수행됩니다.COPY . .은 로컬 프로젝트의 모든 파일을 컨테이너의 작업 디렉토리로 복사합니다. 여기에는server.js,package.json을 포함하여 애플리케이션을 실행하는 데 필요한 모든 파일이 포함됩니다.RUN npm install은package.json에 나열된 의존성을 컨테이너 안에 설치합니다.EXPOSE 3000은 컨테이너가 3000 포트에서 수신 대기한다는 것을 Docker에 알려주며, 이는 우리 Node.js 애플리케이션이 실행되는 포트입니다.CMD ["node", "server.js"]은 컨테이너가 시작될 때 실행되는 명령어를 정의하여 Node.js 서버를 실행합니다.
이 Dockerfile을 프로젝트 루트에 배치함으로써 Docker는 애플리케이션 파일과 의존성을 정확히 어디서 찾아야 하는지 알게 됩니다. 이미지를 빌드할 때 이식 가능한 컨테이너에 모두 패키징하여 Docker가 설치된 어디서든 실행할 수 있게 하며, 배포를 간단하고 일관되도록 해줍니다.

이제 Dockerfile이 준비되었으니, 다음 단계는 Node.js 애플리케이션용 Docker 이미지를 빌드하는 것입니다.
이미지를 빌드하려면 터미널을 열고, 프로젝트 루트 디렉토리(Dockerfile이 있는 곳)에 있는지 확인한 뒤 다음 명령어를 실행하세요:
docker build -t todo-app:1.0 .
todo-app→ 생성할 이미지의 이름:1.0→ 버전 태그 (예:1.0,v1,latest등 자유롭게 지정 가능).→ Docker에게 현재 폴더(프로젝트 루트)를 빌드 컨텍스트로 사용하라고 알려줍니다.
다음 명령어를 실행한 후:
docker build -t todo-app:1.0 .
Docker는 Dockerfile을 읽고, Node.js 애플리케이션과 모든 의존성을 패키징하여 Docker 이미지를 생성합니다. 이미지가 생성되었는지 확인하려면 다음 명령어를 실행하세요:
docker images
다음과 같은 출력이 나타날 것입니다:
REPOSITORY TAG IMAGE ID CREATED SIZE
todo-app 1.0 d85dd4ed97f9 45 seconds ago 147MB
mongo latest 1d659cebf5e9 2 weeks ago 894MB
mongo-express latest 1133e12468c7 20 months ago 182MB
이 출력은 todo-app 이미지가 MongoDB와 Mongo Express 이미지와 함께 성공적으로 생성되었음을 보여줍니다.
Node.js 앱 컨테이너 실행
이제 이미지가 생성되었으니 다음 단계는 이 이미지를 사용해 컨테이너를 실행하는 것입니다. 컨테이너는 기본적으로 이미지의 실행 중인 인스턴스입니다. 이를 위해 다음 명령어를 실행하세요:
docker run todo-app:1.0
이 명령어는 다음을 수행합니다:
docker run은 이미지에서 새 컨테이너를 시작합니다.todo-app:1.0은 Docker에게 (방금 빌드한) 이미지를 사용하라고 알려줍니다.
이 명령어가 실행되면 Node.js 애플리케이션이 컨테이너 내부에서 로컬 환경과 분리된 상태로 실행됩니다. 브라우저에서 http://localhost:3000을 열어 Todo 앱이 로컬에서와 동일하게 작동하는 것을 확인할 수 있습니다.
실행 중인 모든 컨테이너를 보려면 다음을 사용하세요:
docker ps
다음과 같은 출력을 볼 수 있습니다:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d85dd4ed97f9 todo-app:1.0 "node server.js" 10s ago Up 10s 0.0.0.0:3000->3000/tcp awesome_todo
이것은 컨테이너가 실행 중임을 확인해 줍니다. 필요시 중지하려면:
docker stop <container-id>
오류 해결
여기서 문제가 발생합니다: docker run todo-app:1.0을 실행하면 다음과 같은 오류가 나타납니다:
Server → http://localhost:3000
MongoDB connection error: MongoServerSelectionError: getaddrinfo ENOTFOUND mongodb
at Topology.selectServer (/home/app/node_modules/mongodb/lib/sdam/topology.js:346:38)
...
[cause
특히 todo 리스트 생성 등의 작업을 시도할 때 발생합니다.
getaddrinfo ENOTFOUND mongodb 오류는 Node.js 컨테이너가 MongoDB를 찾을 수 없음을 알려줍니다. MongoDB가 다른 컨테이너에서 실행 중이더라도 앱 컨테이너는 격리되어 있어 이를 찾을 수 없습니다.
왜 이런 일이 발생하는가:
server.js에서 MongoDB에 다음과 같이 연결하는 것을 기억하세요:
const mongoUrl = "mongodb://admin:password@localhost:27017";
문제는 localhost에 있습니다. 로컬 머신에서 앱을 실행할 때는 localhost가 완벽하게 작동하지만, Docker 컨테이너 내부에서는 localhost가 해당 컨테이너 자신만 가리킵니다.
비유하자면:
- 로컬 실행: 앱과 MongoDB가 같은 방에 있는 두 사람,
localhost작동 - Docker 실행: 각 컨테이너가 별도의 방,
localhost는 그 방만 가리킴
해결책
MongoDB 연결 URL을 localhost 대신 Docker 서비스 이름으로 변경해야 합니다. server.js 파일을 업데이트하세요:
const mongoUrl = "mongodb://admin:password@localhost:27017";
다음으로 변경하세요:
const mongoUrl = "mongodb://admin:password@mongodb:27017";
완전한 업데이트된 server.js는 다음과 같습니다:
const express = require("express");
const multer = require("multer");
const path = require("path");
const fs = require("fs");
const { MongoClient, ObjectId } = require("mongodb");
const app = express();
const PORT = 3000;
// Host = localhost → talks to the MongoDB container via the exposed port
// Port = 27017 → default MongoDB port
// User / Pass → admin / password (the credentials you gave the container)
const mongoUrl = "mongodb://admin:password@mongodb:27017";
const dbName = "todos";
let db;
MongoClient.connect(mongoUrl)
.then((client) => {
db = client.db(dbName);
console.log("Connected to MongoDB →", dbName);
})
.catch((err) => console.error("MongoDB connection error:", err));
const uploadDir = path.join(__dirname, "uploads");
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadDir),
filename: (req, file, cb) => {
const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);
cb(null, "photo-" + unique + path.extname(file.originalname));
},
});
const upload = multer({ storage });
app.use(express.static(__dirname));
app.use("/uploads", express.static(uploadDir));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get("/todos", async (req, res) => {
const todos = await db.collection("todos").find().toArray();
res.json(todos);
});
app.post("/todos", upload.single("photo"), async (req, res) => {
const text = req.body.text?.trim();
if (!text) return res.status(400).json({ error: "Text required" });
const todo = {
text,
image: req.file ? `/uploads/${req.file.filename}` : null,
createdAt: new Date(),
};
const result = await db.collection("todos").insertOne(todo);
todo._id = result.insertedId;
res.json(todo);
});
// Start server
app.listen(PORT, () => {
console.log(`Server → http://localhost:${PORT}`);
});
왜 mongodb가 작동하는가
호스트명 mongodb는 docker-compose.yml에서 정의한 서비스 이름과 일치합니다:
services:
mongodb: # ← 다른 컨테이너들이 사용하는 호스트명
image: mongo
container_name: mongo
...
같은 Docker Compose 네트워크에서 컨테이너들이 실행될 때, Docker는 내부 DNS를 제공하여 서비스 이름을 올바른 컨테이너 IP 주소로 해석합니다. 따라서 앱이 mongodb:27017에 연결하려고 하면 Docker가 자동으로 MongoDB 컨테이너로 라우팅합니다.
Docker 이미지 재빌드
코드를 업데이트했으니 이 변경사항을 포함하여 Docker 이미지를 재빌드해야 합니다:
docker build -t todo-app:1.0 .
``
You should see output confirming the build completed successfully:
``
[+] Building 8.1s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 443B
...
=> => naming to docker.io/library/todo-app:1.0
Docker Compose에 앱 추가
이제 docker-compose.yml 파일을 업데이트하여 todo-app 서비스를 포함하세요:
version: "3.8"
services:
mongodb:
image: mongo
container_name: mongo
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
mongo-express:
image: mongo-express
container_name: mongo-express
ports:
- "8081:8081"
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: admin
ME_CONFIG_MONGODB_ADMINPASSWORD: password
ME_CONFIG_MONGODB_SERVER: mongodb
depends_on:
- mongodb
todo-app:
image: todo-app:1.0
container_name: todo-app
ports:
- "3000:3000"
depends_on:
- mongodb
todo-app 서비스는 다음을 포함합니다:
image: todo-app:1.0- 방금 재빌드한 Docker 이미지 사용container_name: todo-app- 컨테이너에 친근한 이름 부여ports: "3000:3000"- 3000 포트로 앱 노출depends_on: mongodb- MongoDB가 앱보다 먼저 시작되도록 보장
모든 서비스 시작
먼저 실행 중인 컨테이너들을 중지하세요:
docker compose down
로컬 시스템에서 3000 포트가 사용 중이라면 이를 중지하세요 (3000 포트 해제).
이전에 로컬에서 서버를 실행했지만, 이제 Docker 이미지를 빌드했으므로 앱은 컨테이너 내부에서 실행되며 로컬 머신 환경에 더 이상 의존하지 않습니다.
node server.js
Server → http://localhost:3000
이제 해당 터미널에서 Ctrl + C로 중지하세요.
그 다음 모든 것을 함께 시작하세요:
docker compose up -d
``
You should see:
``
[+] Running 4/4
✔ Network docker_tut_default Created
✔ Container mongo Started
✔ Container mongo-express Started
✔ Container todo-app Started
모든 것이 작동하는지 확인
모든 컨테이너가 실행 중인지 확인하세요:
docker ps
``
예상 출력:
``
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a1b2c3d4e5f6 todo-app:1.0 "node server.js" 30 seconds ago Up 28 seconds 0.0.0.0:3000->3000/tcp todo-app
3d7c797fde1d mongo-express "/sbin/tini -- /dock…" 30 seconds ago Up 29 seconds 0.0.0.0:8081->8081/tcp mongo-express
4511ade73c38 mongo "docker-entrypoint.s…" 30 seconds ago Up 29 seconds 0.0.0.0:27017->27017/tcp mongo
``
## 애플리케이션 테스트
이제 모든 것이 작동하는지 확인해보겠습니다:
### 1. Todo 앱 접속
브라우저를 열고 다음으로 이동하세요:
``
http://localhost:3000
``
### 2. Todo 항목 생성
기능을 테스트하기 위해 몇 개의 todo 항목을 추가하세요. 이미지 업로드도 시도해보세요!
### 3. Mongo Express에서 확인
Mongo Express를 열어보세요:
``
http://localhost:8081
todos 데이터베이스, 그 다음 todos 컬렉션으로 이동하세요. 방금 생성한 모든 todo와 완전한 데이터가 표시됩니다.
무엇이 변경되었고 왜 작동하는가
수정 전:
- 연결 문자열이
localhost:27017사용 ❌ - 컨테이너가 자기 자신에서 MongoDB 찾음
ENOTFOUND오류로 연결 실패
수정 후:
- 연결 문자열이
mongodb:27017사용 ✅ - Docker 내부 DNS가
mongodb를 MongoDB 컨테이너로 해석 - 연결 성공 및 데이터 정상 흐름
이는 Docker 네트워킹의 핵심 교훈입니다: 컨테이너들은 localhost가 아닌 서비스 이름으로 통신합니다. Docker Compose는 모든 서비스가 이름으로 서로를 찾을 수 있는 네트워크를 자동 생성합니다.
컨테이너 관리 방법
실행 중인 컨테이너를 관리하는 방법을 간단히 알아보겠습니다. 일반적으로 다음 명령어들을 사용합니다:
모든 서비스 중지:
docker compose down
앱 로그 보기:
docker compose logs todo-app
실시간 로그 보기:
docker compose logs -f todo-app
코드 변경 후 재빌드:
docker build -t todo-app:1.0 .
docker compose up -d --force-recreate todo-app
이제 애플리케이션이 완전히 컨테이너화되어 프로덕션 준비가 완료되었습니다. 세 서비스가 완벽하게 함께 작동하며, docker-compose.yml 파일과 빌드된 이미지만 있으면 Docker가 지원되는 어디서든 이 전체 스택을 배포할 수 있습니다.
전체 업데이트된 코드는 여기에서 확인하세요.
프라이빗 Docker 레지스트리 생성 방법
이제 커스텀 Docker 이미지를(로컬 머신 대신에) 프라이빗 컨테이너 레지스트리에 저장하고 싶습니다. 이는 세 가지 주요 장점을 제공합니다:
- 1. 제어된 접근 - 명시적으로 권한을 부여한 사람이나 서버만 이미지를 가져오거나 업로드할 수 있습니다. 코드와 의존성이 비공개로 안전하게 유지됩니다.
- 2. 안정적인 배포 - 올바른 AWS 자격 증명을 가진 누구나(또는 어떤 서버도) 전 세계 어디서든 정확히 동일한 이미지를 가져올 수 있어 "내 머신에서는 작동하는데..." 문제가 사라집니다.
- 3. 버전 관리 및 라이프사이클 관리 - 여러 태그 버전(1.0, 2.0, latest 등)을 유지하고 필요시 쉽게 롤백할 수 있습니다.
첫 번째 단계는 프라이빗 Docker 레지스트리(컨테이너 레지스트리)를 생성하는 것입니다. 여기서는 AWS Elastic Container Registry (ECR)를 사용하겠습니다. Amazon ECR은 컨테이너 이미지와 아티팩트를 어디에서나 안전하게 저장·관리·공유·배포할 수 있게 해주는 완전관리형 컨테이너 레지스트리 서비스입니다.

홈페이지에 접속한 후 Create 버튼을 클릭하세요. 레포지토리 이름을 이미지와 동일하게 todo-app으로 지정한 후 Create를 클릭하여 설정을 완료합니다.

추가 옵션은 신경 쓰지 않아도 됩니다 - 이것은 AWS 튜토리얼이 아닙니다.
참고: AWS ECR에서는 각 이미지가 고유한 레포지토리를 가지며, 해당 이미지의 다양한 태그 버전을 저장합니다.

이제 이미지를 프라이빗 레포지토리에 푸시하려면 두 가지를 해야 합니다. 먼저 프라이빗 레포에 로그인해야 합니다. AWS가 푸시를 허용하기 전에 본인을 인증해야 하기 때문입니다. 즉, 로컬 이미지를 레포에 푸시할 때 "이 레지스트리에 접근 권한이 있습니다. 여기 제 자격 증명입니다."라고 말하는 셈입니다.
우리의 경우 AWS ECR을 사용하므로 사용자 이름과 비밀번호를 수동으로 입력하는 대신 AWS를 통해 인증합니다.
1단계: AWS 액세스 키 가져오기
AWS 콘솔에서 액세스 키를 찾으려면 다음 단계를 따르세요:
1. https://console.aws.amazon.com 에 접속해 AWS 콘솔에 로그인합니다.
2. 오른쪽 상단의 계정 이름을 클릭한 뒤 “보안 자격 증명(Security Credentials)”으로 이동합니다.
3. 아래로 스크롤해 "Access keys" 섹션을 찾습니다.
4. 액세스 키가 없다면:
- "Create access key"를 클릭합니다.
- "Command Line Interface (CLI)"를 선택합니다.
- 확인 체크박스를 선택하고 Next를 클릭합니다.
- 설명을 입력(선택 사항)한 뒤 "Create access key"를 클릭합니다.
5. 중요: 액세스 키 ID(예: AKIAIOSFODNN7EXAMPLE처럼 보임)와 시크릿 액세스 키(예: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY처럼 보임)를 둘 다 복사해 바로 저장하세요. 시크릿 키는 한 번만 표시되므로 잃어버리면 새 키 쌍을 만들어야 합니다.
또는, 다른 사람이 당신의 AWS 계정을 관리한다면 관리자에게 다음 사항을 요청해야 합니다:
- ECR 권한이 부여된 IAM 사용자
- 해당 사용자의 액세스 키 ID와 시크릿 액세스 키
2단계: AWS CLI 설치 여부 확인
다음 명령어로 확인할 수 있습니다:
aws --version
3단계: AWS CLI에 자격 증명 설정
다음 명령어를 실행합니다:
aws configure
``
프롬프트에 다음 네 가지를 입력해야 합니다:
``
AWS Access Key ID [None]: <여기에 Access Key ID를 붙여넣으세요>
AWS Secret Access Key [None]: <여기에 Secret Access Key를 붙여넉으세요>
Default region name [None]: eu-north-1 또는 원하는 리전
Default output format [None]: json
프롬프트가 뜨면 키를 그대로 붙여넣고, 리전은 eu-north-1 또는 원하는 리전을 입력하고, 출력 형식은 json을 입력하거나 Enter를 눌러도 됩니다.
4단계: AWS 설정 테스트
모든 설정이 올바른지 확인하려면:
aws sts get-caller-identity
정상이라면 AWS 계정 정보가 출력됩니다.
5단계: ECR(Docker 레지스트리)에 로그인
이제 ECR에 로그인합니다:
aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin 244836489456.dkr.ecr.eu-north-1.amazonaws.com
"Login Succeeded"가 출력되어야 합니다.
Docker 이미지 명명 방식 이해하기
모든 Docker 이미지는 Docker에게 해당 이미지를 어디서 찾거나 저장할지를 알려주는 이름을 가지고 있습니다.
예를 들어 아래 명령을 실행하면:
docker pull mongo:4.2
실제로는 다음에서 이미지를 가져옵니다:
docker.io/library/mongo:4.2
여기에서 어떤 일이 일어나는지 살펴보겠습니다:
[docker.io](http://docker.io/)→ 레지스트리(이 경우 Docker Hub)library→ 공식 이미지에 대한 기본 네임스페이스mongo→ 레포지토리 이름4.2→ 이미지 태그
`todo-app:1.0 같은 로컬 이미지를 빌드하면, 그 이미지는 여러분의 머신에만 존재합니다. 전체 레지스트리 경로를 포함하지 않으면 Docker는 그것을 어디에 푸시해야 할지 모릅니다.
AWS ECR을 사용할 때는 이미지 이름에 ECR 레지스트리 URL을 포함해야 합니다. 예를 들어:
docker tag todo-app:1.0 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
그 다음 이렇게 푸시합니다:
docker push 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
이 전체 경로가 없으면 Docker는 어떤 원격 레포지토리를 가리키는지 알 수 없습니다. 그래서 todo-app:1.0만으로는 동작하지 않습니다.
6단계: 이미지 빌드, 태그, 푸시

로컬 이미지를 전체 ECR 경로로 태그하세요.
docker tag todo-app:1.0 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
그리고 푸시:
docker push 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
주의: 이미지 태그를 지정하고 푸시할 때 주의하세요. 모든 ECR 저장소 URL은 특정 AWS 계정과 리전에 연결되어 있습니다.
예를 들어, 튜토리얼에서 사용하는 것은:
244836489456.dkr.ecr.eu-north-1.amazonaws.com
하지만 여러분의 ECR URL은 사용하는 AWS 계정과 선택한 리전(예: us-east-1, ap-south-1 등)에 따라 서로 다르게 됩니다.
그래서 docker tag, docker push를 실행하기 전에, 레지스트리 URL, 리전을 반드시 여러분의 값으로 바꿔야 합니다.
그렇지 않으면 “tag does not exist”, “repository not found” 같은 오류가 납니다.
요약하자면, 당황하지 말고 리전을 다시 한 번 확인한 뒤, 이미지를 푸시하기 전에 AWS 콘솔에 표시된 ECR URL이 정확한지 항상 확인하세요.
6단계를 성공적으로 실행했다면 터미널에 다음과 비슷한 출력이 보일 것입니다.
stephenjohnson@Oghenekparobo docker_tut % docker tag todo-app:1.0 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
stephenjohnson@Oghenekparobo docker_tut % docker push 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
The push refers to repository [244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app]
4f94b5cbe8ab: Pushed
85ba7bf54231: Pushed
4ea46a43fa07: Pushed
dee30873f229: Pushed
e78159dbd370: Pushed
a358a725b813: Pushed
cd8a6003174c: Pushed
abb63e49e652: Pushed
6cc65bdde70e: Pushed
41a4e3939504: Pushed
3520c50ae60e: Pushed
75ba6634710f: Pushed
1.0: digest: sha256:51f07267936fc94d9b677db8a760801e6c5fd4764f4bb2bd7b4dd150c756a39b size: 2842이 출력은 이미지가 프라이빗 AWS ECR 레포지토리에 성공적으로 푸시되었음을 의미합니다.
이제 AWS Management Console에서 ECR로 이동하면, 태그 1.0이 붙은 todo-app 이미지를 목록에서 확인할 수 있습니다.
이 시점부터 이미지는 AWS ECR에 안전하게 저장되어 있으며, 레포지토리에 접근할 수 있는 어느 곳에서든 가져오거나 배포할 준비가 된 상태입니다.

과제: 앱의 새 버전 만들기 및 푸시하기
이제 첫 번째 이미지(todo-app:1.0)를 AWS ECR에 성공적으로 푸시했으니, 실제 환경에서 개발자가 앱을 수정하고 새 버전을 배포하는 워크플로를 한 번 시뮬레이션해보겠습니다.
이제 Node.js 앱에 작은 변경을 주고, 이미지를 다시 빌드한 뒤, 업데이트된 버전을 todo-app:2.0으로 푸시할 것입니다.
이미지 배포하기
이제 Docker Compose를 사용해 이미지를 배포해보겠습니다.
지금까지는 로컬 이미지를 사용해 앱을 실행했습니다:
image: todo-app:1.0
하지만 이제 이미지를 AWS ECR에 올렸으므로, Docker가 이미지를 어디에서 가져와야 하는지 알 수 있도록 해당 줄을 전체 ECR 이미지 URI로 교체해야 합니다.
로컬 이미지:
image: todo-app:1.0
프라이빗 레포지토리 이미지(ECR):
image: 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
Docker는 “todo-app:1.0”이 어디에 저장되어 있는지 마법처럼 추측해주지 않습니다. 전체 레지스트리 URL을 포함하지 않으면, Docker는 그것이 AWS가 아니라 로컬 머신에 있는 이미지라고 가정합니다.
다음은 ECR에서 앱 이미지를 가져오도록 정리되고, 수정되고, 올바르게 포맷된 docker-compose 파일입니다:
version: "3.8"
services:
my-app:
image: 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
container_name: my-app
ports:
- "3000:3000"
depends_on:
- mongodb
mongodb:
image: mongo
container_name: mongo
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
mongo-express:
image: mongo-express
container_name: mongo-express
ports:
- "8081:8081"
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: admin
ME_CONFIG_MONGODB_ADMINPASSWORD: password
ME_CONFIG_MONGODB_SERVER: mongodb
depends_on:
- mongodb
왜 서비스 이름을 “todo-app”이 아니라 “my-app”으로 했을까?
여기서는 다음 둘을 헷갈리지 않게 하려고 이름을 바꿨습니다:
- 로컬의 “todo-app:1.0”
- ECR에 있는 “todo-app:1.0”
이렇게 하면 좀 더 구분이 명확해지지만, 원한다면 다시 원래 이름으로 돌려도 됩니다.
왜 ECR에는 전체 이미지 URL을 써야 할까?
mongo, mongo-express 같은 다른 컨테이너는 다음처럼 동작합니다:
image: mongo
image: mongo-express
그 이유는 Docker가 이 이미지들이 Docker Hub에 있다는 것을 알고 있기 때문입니다.
하지만 AWS ECR 같은 프라이빗 레포지토리의 경우, 전체 경로를 주지 않으면 Docker는 “todo-app”이 어디에 있는지 전혀 알 수 없습니다:
AWS_ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/repository_name:tag
이 정보는 Docker에게 다음을 알려 줍니다:
- 어떤 AWS 계정인지
- 어떤 리전인지
- 어떤 레포지토리인지
- 어떤 버전(tag)인지
이 URL이 없으면 Docker는 이미지를 가져올 수 없습니다.
프라이빗 ECR 레포지토리에서 가져오고 싶을 때마다(그리고 Docker Compose를 사용할 때도 마찬가지로) 반드시 로그인 상태여야 합니다.
다음 명령을 실행하세요:
aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin 244836489456.dkr.ecr.eu-north-1.amazonaws.com
로그인되어 있지 않으면 Docker Compose는 보통 다음과 같은 오류를 냅니다:
❌ pull access denied
❌ repository does not exist
❌ no basic auth credentials
Docker Compose로 앱 배포하기
배포하기 전에, 포트 충돌이나 남은 컨테이너 때문에 문제가 생기지 않도록 기존 컨테이너를 중지하고 제거하는 것이 좋습니다:
# 이 프로젝트에서 실행 중인 모든 컨테이너 중지
docker-compose down --remove-orphans
#선택 사항: 아무 것도 실행 중이 아닌지 확인
docker ps이렇게 하면 3000 포트를 포함한 매핑된 포트가 비워져, 새 컨테이너를 시작할 때 발생할 수 있는 오류를 방지할 수 있습니다.
환경을 깨끗하게 정리했다면, 이제 스택을 배포합니다:
docker-compose up -d
Docker Compose는 다음 작업을 수행합니다:
- AWS ECR에 접속 → 인증한 뒤 프라이빗 레포지토리에서
todo-app:1.0이미지를 가져옴 - MongoDB 시작 → 설정한 자격 증명으로 DB 컨테이너 실행
- Mongo Express 시작 → 웹 기반 MongoDB 관리 인터페이스 실행
- Node.js 앱 시작 → MongoDB에 연결된
my-app컨테이너 실행
실행 중인 컨테이너를 확인하려면:
docker ps
다음과 같은 컨테이너들이 보여야 합니다:
- mongo
- mongo-express
- my-app
만약 my-app이 시작에 실패했다면, 보통 3000 포트가 이미 사용 중인 경우입니다. 사용 중인 프로세스를 중지하여 포트를 비우세요:
lsof -i :3000
kill -9 <PID> # if a process is using it그 후 다시 실행하세요:
docker-compose up -d
앱에 접속하려면:
- Node.js 앱: http://localhost:3000/
- Mongo Express: http://localhost:8081/
이 워크플로를 통해 깨끗한 상태에서 시작할 수 있고, 포트 충돌이나 컨테이너 충돌 같은 흔한 문제를 피할 수 있습니다.
프라이빗 Docker 이미지 공유하기
Node.js 앱 이미지를 AWS ECR에 푸시했다면, 이제 이미지는 프라이빗 레포지토리에 안전하게 저장된 상태입니다. 그렇다면 다른 개발자, 팀원, 또는 서버가 같은 이미지를 실행해야 할 때는 어떻게 할까요? 프라이빗이기 때문에 mongo나 nginx 같은 공개 이미지처럼 자동으로 가져올 수 없고, 반드시 인증된 접근이 필요합니다.
다음은 (다른 사람들이) 이미지를 가져와 사용하는 방법입니다.
1. IAM 권한 부여
협업자는 ECR 권한이 있는 AWS IAM 사용자 또는 역할이 필요합니다. 최소한 다음 작업을 허용하는 정책이 있어야 합니다:
ecr:GetAuthorizationTokenecr:BatchCheckLayerAvailabilityecr:GetDownloadUrlForLayerecr:BatchGetImage
이를 위해 전용 IAM 사용자를 만든 다음, 해당 사용자에게 Access Key ID와 Secret Access Key를 제공할 수 있습니다.
2. AWS CLI 설치 및 설정
협업자는 AWS CLI가 설치되어 있어야 합니다. 그 다음 자신의 자격 증명으로 CLI를 설정합니다:
aws configure
이때 다음 값을 입력합니다:
- Access Key ID
- Secret Access Key
- 기본 리전(해당 ECR 레포지토리가 있는 리전, 예:
eu-north-1) - 기본 출력 형식(보통
json)
3. Docker를 ECR에 인증시키기
이미지를 pull하기 전에, Docker는 AWS 자격 증명을 사용해 먼저 인증을 해야 합니다:
aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin 244836489456.dkr.ecr.eu-north-1.amazonaws.com
성공하면 Docker가 다음과 같이 응답합니다:
Login Succeeded
4. 이미지 Pull
이제 협업자는 AWS 계정, 리전, 레포지토리 이름, 태그를 모두 포함한 전체 ECR URI로 이미지를 가져올 수 있습니다:
docker pull 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
5. 컨테이너 실행
이미지를 pull한 후에는 로컬에서 다음과 같이 컨테이너를 실행할 수 있습니다:
docker run -p 3000:3000 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
또는 Docker Compose 파일에 포함시켜 image: 필드를 전체 ECR URI로 교체할 수 있습니다.
- mongo 같은 공개 이미지는 Docker Hub가 공개 레지스트리이기 때문에 이런 과정이 필요 없지만, 프라이빗 ECR 이미지는 반드시 명시적인 인증이 필요합니다.
- 프라이빗 레포지토리에서 pull을 할 때는 항상 유효한 로그인 상태가 필요합니다. Docker는 자격 증명을 추측할 수 없습니다.
- 전체 이미지 URI를 사용하는 것은 Docker가 이미지를 어디에서 가져와야 하는지 정확히 알 수 있게 해 줍니다.
이 설정을 통해 팀은 로컬 머신, 스테이징 서버, 운영 환경 어디서든 애플리케이션을 공유, 배포, 실행할 수 있으며, 동시에 저장소를 비공개로 안전하게 유지할 수 있습니다.
Docker 볼륨
MongoDB 같은 컨테이너를 실행할 때, 컨테이너 내부에서 생성된 데이터는 모두 일시적입니다. 컨테이너가 중지되거나 삭제되면 그 안의 데이터도 모두 사라집니다. 테스트 용도로는 괜찮지만, 프로덕션 환경에는 적합하지 않습니다.
이를 해결하기 위해 Docker는 볼륨을 제공합니다. 볼륨을 사용하면 컨테이너가 데이터를 컨테이너 외부(호스트 머신이나 Docker가 관리하는 스토리지)에 저장하여, 컨테이너가 재시작되거나 재빌드되거나 삭제되더라도 데이터가 유지되도록 할 수 있습니다.
Docker 볼륨의 동작 방식
Docker 볼륨은 컨테이너를 위한 “지속되는 폴더”라고 생각하면 됩니다:
- 볼륨에 기록된 데이터는 컨테이너가 삭제돼도 안전하게 남아 있습니다.
- 컨테이너는 이 볼륨에 자유롭게 읽기/쓰기를 할 수 있습니다.
- 볼륨은 데이터베이스, 로그, 파일 업로드, 그 외 애플리케이션에서 필요한 모든 영속 데이터에 필수적입니다.
Docker 볼륨의 종류
Docker에는 세 가지 주요 볼륨 유형이 있습니다:
1. 네임드 볼륨(Named Volumes)
네임드 볼륨은 사용자가 이름을 부여한 볼륨으로, Docker가 완전히 관리합니다. 보통 프로덕션 데이터베이스나 여러 컨테이너가 공유해야 하는 영속 데이터에 사용합니다.
예시입니다:
volumes:
mongo-data:
서비스에서의 모습입니다:
volumes:
- mongo-data:/data/db
2. 바인드 마운트(Bind Mounts)
바인드 마운트는 호스트 머신의 폴더를 컨테이너 내부 경로에 직접 연결합니다. 개발 환경에서 코드, 로그, 업로드 파일 등을 실시간으로 동기화할 때 자주 사용합니다.
예시입니다:
volumes:
- ./uploads:/usr/src/app/uploads
3. 익명 볼륨(Anonymous Volumes)
이름이 없는 볼륨입니다. Docker가 자동으로 임의의 이름을 붙입니다. 테스트용 임시 데이터처럼, 짧게 쓰고 버릴 데이터에 사용할 수 있지만, 프로덕션에서는 흔히 쓰이지 않습니다.
예시입니다:
volumes:
- /data/tmp
볼륨을 사용하는 Docker Compose 예제
다음은 Node.js + MongoDB + Mongo Express 스택에서 가장 흔한 볼륨 유형을 사용하는 전체 docker-compose.yml 예제입니다:
version: "3.8"
services:
my-app:
image: 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
container_name: my-app
ports:
- "3000:3000"
depends_on:
- mongodb
volumes:
- ./uploads:/usr/src/app/uploads # 파일 업로드용 bind mount
mongodb:
image: mongo
container_name: mongo
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
volumes:
- mongo-data:/data/db # 데이터베이스 영속 스토리지 네임드 볼륨
mongo-express:
image: mongo-express
container_name: mongo-express
ports:
- "8081:8081"
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: admin
ME_CONFIG_MONGODB_ADMINPASSWORD: password
ME_CONFIG_MONGODB_SERVER: mongodb
depends_on:
- mongodb
volumes:
mongo-data: # 네임드 볼륨 정의
이 코드가 동작하는 방식:
- MongoDB 볼륨(
mongo-data): 네임드 볼륨으로, 컨테이너 내부의/data/db아래에 있는 모든 DB 파일을 저장합니다. 컨테이너가 재시작·삭제·재빌드되어도 데이터는 유지됩니다. - Node.js 업로드(
./uploads): 바인드 마운트로, 호스트의uploads폴더를 컨테이너의/usr/src/app/uploads에 매핑합니다. 업로드된 파일은 즉시 호스트에서도 확인할 수 있습니다. - 익명 볼륨: 이 예제에는 포함되어 있지 않은데, 이름 없이 정의된 볼륨은 Docker가 자동으로 생성하며, 보통 프로덕션보다는 임시 용도로만 사용합니다.
시각적 개념(단순화):
Host Machine(호스트 머신)
├─ /project/uploads ← 컨테이너와 동기화되는 bind mount
├─ Docker Volumes
│ └─ mongo-data ← MongoDB 영속 데이터를 위한 네임드 볼륨
Containers(컨테이너)
├─ my-app
│ └─ /usr/src/app/uploads ← 호스트 uploads 폴더를 봄
├─ mongodb
│ └─ /data/db ← 네임드 볼륨 mongo-data 사용
├─ mongo-express
핵심 정리 (Takeaways)
- 중요한 데이터에는 항상 볼륨을 사용하세요.
- 프로덕션 환경의 데이터베이스에는 네임드 볼륨이 가장 적합합니다.
- 바인드 마운트는 개발용과 실시간 동기화에 가장 잘 어울립니다.
- 익명 볼륨은 테스트를 제외하고는 거의 필요하지 않습니다.
- 볼륨은 컨테이너의 라이프사이클과 데이터의 라이프사이클을 분리해 주며, 이는 Docker 모범 사례의 핵심 개념입니다.
애플리케이션 시작하기
Docker Compose에 볼륨 설정까지 완료했다면, 다음 단계는 애플리케이션을 시작하고 볼륨이 제대로 동작하는지 확인하는 것입니다. 아래는 간단한 단계별 가이드입니다.
1. 컨테이너 시작
다음 명령을 실행하세요:
docker-compose up -d
-d 플래그는 컨테이너를 백그라운드(detached 모드)에서 실행한다는 의미입니다.
Docker는 다음을 수행합니다:
- (로그인되어 있다면) AWS ECR에서 앱 이미지를 pull합니다.
- 네임드 볼륨과 함께 MongoDB를 시작합니다.
- Mongo Express를 시작합니다.
- Node.js 앱을 시작합니다.
2. 실행 중인 컨테이너 확인
모든 것이 제대로 시작되었는지 확인하려면:
docker ps
다음과 비슷한 출력이 보여야 합니다:
CONTAINER ID IMAGE STATUS PORTS
2a2e120cc912 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0 Up 5s 0.0.0.0:3000->3000/tcp
f4d5a1ab1234 mongo Up 5s 0.0.0.0:27017->27017/tcp
c3d5b2bc2345 mongo-express Up 5s 0.0.0.0:8081->8081/tcp3. 볼륨 확인
Docker 볼륨 목록을 보려면:
docker volume ls
여기서 예를 들어 mongo-data 같은 네임드 볼륨을 확인할 수 있어야 합니다.
볼륨을 자세히 보려면:
docker volume inspect docker_tut_mongo-data
그러면 Docker가 호스트에서 MongoDB 데이터를 어디에 저장하는지 보여주는데, 예를 들어 다음과 같이 표시됩니다:
[
{
"Name": "mongo-data",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/mongo-data/_data",
"Labels": {},
"Scope": "local"
}
]MongoDB 컨테이너 내부의 /data/db에 저장되는 모든 것은 실제로 호스트의 이 경로에 저장됩니다.
4. 데이터 영속성 테스트
1. MongoDB 또는 앱에 접속해서 데이터를 몇 개 추가해 보세요.
2. 그 다음 컨테이너를 중지 및 삭제합니다:
docker-compose down3. 이후 다시 앱을 시작합니다:
docker-compose up -d4. 데이터를 다시 확인해 보세요.
- MongoDB가 네임드 볼륨을 사용하고 있기 때문에 데이터는 그대로 남아 있습니다.
- 이로써 볼륨이 영속적으로 동작한다는 것이 증명됩니다.
5. 선택: Node.js 업로드(바인드 마운트) 확인
- 앱을 통해 파일을 업로드했다면, 프로젝트 폴더의
./uploads를 확인해 보세요. - 바인드 마운트는 호스트와 컨테이너 디렉터리를 동기화하므로, 업로드한 파일이 호스트 머신의
./uploads폴더에도 생성되어 있어야 합니다.
결론
잘하셨습니다! 이 포괄적인 Docker 튜토리얼의 끝에 도달했습니다. 여러분은 컨테이너와 이미지의 기본을 풀어내는 것부터 네트워킹, Docker Compose, 볼륨, 심지어 비공개 AWS ECR 저장소 배포까지, 완전히 컨테이너화된(프로덕션할 준비가 되고 확장 가능한) Node.js 애플리케이션 스택을 구축했습니다.
이것들은 실제 시나리오에서 애플리케이션을 개발하고, 협업하고, 배포하는 방식을 바꿔놓을 실전 스킬들입니다.
끈질기게 따라와 주셔서 감사합니다. Docker는 처음엔 압도적으로 느껴질 수 있습니다 – 그 긴 명령어들, 네트워킹 특이점들, 영속 데이터 문제들은 결코 사소하지 않죠.
하지만 여기까지 왔다는 것은? 가파른 학습 곡선을 정복하고 개발 여정에서 새로운 경지에 도달했다는 뜻입니다.
이제 "내 컴퓨터에서는 잘 작동하는데"라는 골칫거리를 없애고, CI/CD 파이프라인을 간소화하며, 백엔드나 풀스택 전문가로 한 단계 업그레이드할 준비가 되었습니다.
계속 실험해보세요: todo-app을 수정하거나, Dockerfile에서 멀티스테이지 빌드(도커 다중 단계 빌드 - 단일 Dockerfile 내에서 여러 개의 빌드 단계를 정의하여 최종 이미지 크기를 최적화하고 빌드 효율성을 높이는 기술)를 시도하거나, 다음으로 Kubernetes 같은 오케스트레이션 도구를 탐구해보세요. Docker 생태계는 방대하지만, 이 기초를 갖췄으니 더 깊이 파고들 준비가 되었습니다. 문제가 생기거나 질문이 있으면 Docker Hub, Stack Overflow, GitHub 커뮤니티를 활용하세요.
최종 코드는 여기에서 확인할 수 있습니다.