
도커 컴포즈, 왜 수동 명령어를 버려야 할까요
새벽에 갑자기 서버가 다운되어 급하게 터미널을 열었어요. 평소처럼 docker run 명령어를 하나씩 입력하며 서비스를 복구하려고 하죠. 그런데 아차 하는 순간, 예전에 설정해둔 네트워크 이름이 기억나지 않거나 데이터 볼륨 경로를 잘못 입력했어요. 결국 서비스는 다시 뜨지 않고, 로그에는 알 수 없는 네트워크 오류만 가득해요. 이런 경험, 1인 개발자라면 한 번쯤은 겪어보셨을 거예요.
단순한 컨테이너 하나를 띄우는 건 어렵지 않아요. 하지만 데이터베이스, 웹 서버, 캐시 서버가 얽혀 있는 실제 서비스를 운영하다 보면 이야기가 달라져요. 어떤 순서로 실행해야 하는지, 환경 변수는 무엇인지, 네트워크는 어떻게 연결했는지 일일이 기억하는 건 불가능에 가깝죠. 이 과정에서 발생하는 사소한 실수가 운영 서버 전체를 마비시키는 결과를 초래해요.
이런 혼란을 막기 위해 docker compose yml 실전 구축이 꼭 필요해요. 설정을 코드 형태로 남겨두면, 명령어 한 줄로 복잡한 환경을 그대로 재현할 수 있어요. 서버를 이전하거나 새로운 개발 환경을 만들 때도 더 이상 명령어를 외울 필요가 없죠. 모든 설정이 하나의 파일에 기록되어 있으니까요.
오늘 이 글을 통해 여러분은 단순한 문법 공부를 넘어, 실제 운영 환경에서 바로 꺼내 쓸 수 있는 설계 능력을 갖추게 돼요. 복잡한 설정값을 눈으로 익히는 것이 아니라, 왜 이 설정을 써야 하는지 원리를 이해하게 될 거예요.
이 글에서 다루는 핵심 내용
- 실무에서 바로 사용하는 디렉터리 구조 설계법
- 서비스 간 의존성과 네트워크를 연결하는 핵심 문법
- 운영 서버 배포 전 반드시 확인해야 할 점검 리스트
- 자주 발생하는 설정 오류와 즉각적인 해결 방법
실전 구축 전 반드시 갖춰야 할 준비물
본격적으로 파일을 작성하기 전에, 현재 여러분의 작업 환경이 준비되었는지 확인해야 해요. 준비가 안 된 상태에서 문법부터 공부하면 결국 실전에서 막히기 마련이에요. 특히 서버 운영은 환경의 일관성이 생명이기에, 도구의 버전을 맞추는 작업이 우선이에요.
가장 먼저 Docker Engine이 설치되어 있어야 해요. 그리고 최근에는 도커 컴포즈가 도커의 플러그인 형태로 포함되는 추세예요. 터미널에서 docker compose version을 입력했을 때 버전 정보가 정상적으로 나온다면 준비는 끝난 셈이에요.
Docker Compose는 여러 개의 컨테이너를 정의하고 실행하기 위한 도구예요. YAML(YAML Ain’t Markup Language) 형식을 사용하여 설정 파일을 작성하며, 들여쓰기 하나에도 민감하게 반응하니 주의가 필요해요.
또한, 운영 서버에 적용할 예정이라면 리눅스 환경에서의 파일 권한 개념을 미리 익혀두는 게 좋아요. 컨테이너 내부의 사용자와 호스트 OS의 사용자가 다를 경우, 볼륨 마운트 과정에서 권한 거부(Permission Denied) 오류가 빈번하게 발생하기 때문이에요.
도구 선택 및 환경 비교
어떤 방식으로 컨테이너를 관리할지 고민 중이라면, 아래 표를 참고해서 결정해 보세요. 단순 테스트용인지, 실제 서비스를 운영할 목적 인지에 따라 선택 기준이 달라져요.
| 구분 | Docker CLI (수동) | Docker Compose (자동) | 추천 상황 |
|---|---|---|---|
| 설정 관리 | 명령어 기반 (기록 필요) | YAML 파일 기반 (문서화) | 복합 서비스 운영 시 |
| 재현성 | 낮음 (실수 가능성 높음) | 매우 높음 (동일 환경 보장) | 팀 협업 및 배포 시 |
| 네트워크 설정 | 매번 수동 생성 및 연결 | 파일 내 정의로 자동 구성 | 서비스 간 통신 필요 시 |
| 학습 곡선 | 매우 낮음 | 보통 (YAML 문법 숙지) | 중장기 운영 목적 시 |
결론적으로, 단발성으로 컨테이너 하나만 테스트하고 말 것이라면 CLI 방식이 빠를 수 있어요. 하지만 컨테이너 운영의 안정성을 생각한다면 무조건 컴포즈를 선택하는 것이 옳아요. 설정 파일 자체가 하나의 문서가 되어 나중에 봐도 무엇을 어떻게 돌렸는지 명확히 알 수 있기 때문이에요.
단계별 docker-compose.yml 구축 실전
이제 본격적으로 파일을 작성해 볼 시간이에요. 단순히 코드만 복사해서 붙여넣는 것이 아니라, 실제 서비스가 돌아가는 논리적인 구조를 따라가며 구축해야 해요. 우리는 웹 서버, 애플리케이션, 데이터베이스가 연결된 전형적인 3계층 구조를 목표로 삼을 거예요.
STEP 1. 프로젝트 디렉터리 구조 설계하기
파일을 작성하기 전, 프로젝트 폴더를 먼저 정리해야 해요. 모든 파일을 한 곳에 때려 넣으면 나중에 설정 파일과 데이터 파일이 뒤섞여 관리가 불가능해져요. 추천하는 구조는 다음과 같아요.
project-root/: 전체 프로젝트 최상위 폴더├── docker-compose.yml: 컴포즈 설정 파일├── .env: 보안이 필요한 환경 변수 저장 파일├── nginx/: 웹 서버 설정 및 소스 코드├── app/: 애플리케이션 소스 코드└── data/: 데이터베이스 데이터가 저장될 볼륨 공간
이렇게 구조를 나누어 놓으면, 나중에 특정 서비스의 설정만 바꾸고 싶을 때 해당 폴더만 건드리면 되므로 매우 안전해요.
STEP 2. 서비스 정의와 기본 문법 작성하기
이제 docker-compose.yml 파일을 생성하고 기본 뼈대를 잡아볼게요. YAML은 들여쓰기가 생명이에요. 탭(Tab) 대신 반드시 스페이스(Space) 2칸을 사용하는 습관을 들이세요.
가장 먼저 어떤 이미지를 사용할지, 어떤 포트를 열어줄지를 결정해야 해요. 아래 예시는 아주 기초적인 웹 서버 설정이에요.
services:
web-server:
image: nginx:latest
ports:
- "80:80"
restart: always
여기서 ports 부분의 “80:80″은 호스트 포트:컨테이너 포트를 의미해요. 만약 내 컴퓨터의 80번 포트를 이미 쓰고 있다면 “8080:80″처럼 앞에 숫자를 바꿔주면 돼요. restart: always 설정은 컨테이너가 예기치 않게 종료되었을 때 도커가 자동으로 다시 살려주도록 만드는 운영 필수 옵션이에요.
STEP 3. 데이터 영속성을 위한 볼륨 설정하기
컨테이너는 삭제되면 그 안의 데이터도 모두 사라져요. 데이터베이스의 테이블이나 사용자가 업로드한 이미지가 사라지면 큰일 나겠죠? 이를 방지하기 위해 볼륨(Volume) 설정을 반드시 해야 해요. 볼륨에는 두 가지 방식이 있어요. 도커가 관리하는 이름 있는 볼륨(Named Volume)과 내 컴퓨터의 특정 폴더를 직접 연결하는 바인드 마운트(Bind Mount)예요.
데이터베이스처럼 안정적인 관리가 필요한 곳에는 이름 있는 볼륨을, 웹 소스 코드처럼 수시로 수정해야 하는 곳에는 바인드 마운트를 사용하는 것이 유리해요.
services:
db:
image: postgres:15
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
이렇게 작성하면 컨테이너를 껐다 켜거나 심지어 삭제해도, db_data라는 이름의 공간에 데이터가 안전하게 보관돼요.
STEP 4. 서비스 간 의존관계와 네트워크 구성
실제 서비스는 여러 컨테이너가 서로 대화를 나누며 작동해요. 웹 서버는 DB에 데이터를 요청해야 하고, 앱은 캐시 서버에 값을 저장해야 하죠. 이때 컨테이너끼리 서로를 찾을 수 있게 해주는 것이 네트워크(Network)예요. 컴포즈는 기본적으로 모든 서비스를 하나의 가상 네트워크에 묶어주지만, 보안을 위해 네트워크를 분리하는 것이 더 좋아요.
또한, depends_on 옵션을 통해 실행 순서를 제어할 수 있어요. DB가 먼저 뜨고 나서 앱이 떠야 오류가 안 나겠죠? 하지만 주의할 점이 있어요. depends_on은 컨테이너가 ‘시작’되는 순서만 보장할 뿐, DB 내부의 소프트웨어가 ‘준비’될 때까지 기다려주지는 않아요. 그래서 헬스체크(Healthcheck) 설정을 함께 쓰는 것이 실전 구축의 핵심이에요.
STEP 5. [종합 예제] 실전 3계층 서비스 구축 시나리오
이제 앞서 배운 모든 내용을 종합해서, 실제 운영 환경에 적용할 수 있는 수준의 완성된 파일을 만들어 볼게요. 이 파일 하나면 Nginx, Node.js 앱, PostgreSQL DB가 유기적으로 돌아가는 환경이 구축돼요.
version: '3.8'
services:
# 1. 데이터베이스 서비스
database:
image: postgres:15-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp_db
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d myapp_db"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend_net
# 2. 애플리케이션 서비스
app:
build: ./app
environment:
DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@database:5432/myapp_db
depends_on:
database:
condition: service_healthy
networks:
- backend_net
- frontend_net
# 3. 웹 서버 서비스 (Reverse Proxy)
proxy:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
depends_on:
- app
networks:
- frontend_net
networks:
frontend_net:
backend_net:
volumes:
db_data:
이 시나리오를 분석해 볼까요?
1. 보안 강화: DB는 backend_net에만 연결되어 외부 노출을 차단했어요.
2. 환경 변수 분리: 데이터베이스 비밀번호를 파일에 직접 쓰지 않고 ${DB_PASSWORD} 형태로 처리했어요. 이는 .env 파일을 통해 관리하면 훨씬 안전해요.
3. 안정적인 부팅: 앱은 DB가 단순히 켜지는 게 아니라, 헬스체크를 통해 pg_isready 명령이 성공할 때까지 기다린 후 시작돼요. 이 방식이 바로 운영 서버에서 서비스 장애를 막는 프로의 기술이에요.
자주 하는 실수와 해결법 및 FAQ
실전 구축을 하다 보면 이론과 실제 사이의 간극 때문에 당황스러운 순간이 반드시 찾아와요. 가장 흔하게 마주치는 문제들을 정리했으니, 문제가 생기면 바로 확인해 보세요.
자주 하는 실수와 해결법
- ❌ YAML 들여쓰기 오류 → 왜 발생하는가: 스페이스와 탭을 혼용하거나, 들여쓰기 칸 수가 맞지 않을 때 발생해요. → ✅ 해결법: 에디터(VS Code 등)에서 YAML 포맷터를 반드시 사용하고, 항상 스페이스 2칸을 유지하세요.
- ❌ 포트 충돌 오류 → 왜 발생하는가: 호스트 OS에서 이미 해당 포트를 사용 중일 때 발생해요. → ✅ 해결법:
netstat -tunlp명령어로 포트 사용 현황을 확인하고, 컴포즈 파일의 포트 번호를 변경하세요. - ❌ 데이터 권한 거부(Permission Denied) → 왜 발생하는가: 호스트의 폴더 권한과 컨테이너 내부 사용자의 UID/GID가 다르기 때문이에요. → ✅ 해결법: 볼륨으로 연결할 폴더의 권한을
chmod 777로 임시 테스트해 보거나, Dockerfile에서 사용자 권한을 맞춰주세요. - ❌ 환경 변수 미적용 → 왜 발생하는가:
.env파일의 경로가 틀렸거나 변수명이 오타가 났을 때 발생해요. → ✅ 해결법: 컴포즈 실행 경로에.env가 있는지 확인하고,docker compose config명령어로 적용된 값을 미리 점검하세요. - ❌ 서비스 연결 실패 (Connection Refused) → 왜 발생하는가: 컨테이너끼리 다른 네트워크에 있거나, 서비스 이름이 틀렸을 때 발생해요. → ✅ 해결법: 모든 서비스가 동일한 network 아래 있는지 확인하고, IP 주소 대신 서비스 이름을 호스트 이름으로 사용하세요.
자주 묻는 질문
Q. 컨테이너 로그는 어떻게 확인하나요?
특정 서비스의 로그만 보고 싶다면 docker compose logs -f [서비스이름]을 입력하세요. -f 옵션을 붙이면 실시간으로 올라오는 로그를 볼 수 있어요.
Q. 설정 파일을 수정했는데 적용이 안 돼요. 어떻게 하죠?
단순히 docker compose up만 하면 변경 사항이 반영되지 않을 때가 있어요. docker compose up -d --build 명령어를 사용해 이미지를 다시 빌드하고 컨테이너를 재생성해야 해요.
Q. 특정 서비스 하나만 끄고 싶을 때는 어떻게 하나요?
docker compose stop [서비스이름]을 사용하면 해당 서비스만 멈춰요. 다시 켜고 싶다면 docker compose start [서비스이름]을 입력하면 돼요.
Q. 이미지 버전을 항상 최신으로 유지하려면 어떻게 하나요?
태그를 latest로 쓰면 편하지만, 운영 서버에서는 위험해요. 업데이트 시 예기치 못한 오류가 날 수 있기 때문이죠. 가급적 postgres:15.3처럼 구체적인 버전을 명시하는 것을 권장해요.
Q. .env 파일은 Git에 올려도 되나요?
절대 안 돼요! 비밀번호나 API 키가 들어있는 .env 파일은 반드시 .gitignore에 추가해서 보안을 유지해야 해요. 대신 .env.example 파일을 만들어 어떤 변수가 필요한지만 알려주는 방식을 사용하세요.
안정적인 컨테이너 운영을 위한 마지막 한 걸음
지금까지 docker compose yml 실전 구축의 전 과정을 살펴보았어요. 처음에는 복잡해 보일 수 있지만, 구조를 이해하고 나면 이만큼 든든한 도구도 없다는 걸 느끼게 될 거예요. 설정 파일 하나가 여러분의 소중한 서버와 데이터를 지켜주는 방패가 되어줄 거예요.
- 프로젝트별로 독립된 디렉터리 구조를 설계하세요.
- YAML 문법은 반드시 스페이스 2칸 들여쓰기를 준수하세요.
- 데이터 유실 방지를 위해 볼륨(Volume) 설정을 잊지 마세요.
- 서비스 간 의존성은 healthcheck와 함께 관리해야 안전해요.
- 민감한 정보는 .env 파일로 분리하고 Git에는 올리지 마세요.
이제 여러분이 할 일은 명확해요. 우선 지금 운영 중인 서비스를 메모장에라도 적어보세요. 어떤 컨테이너가 필요한지, 어떤 포트를 쓰는지 리스트업하는 것부터 시작이에요. 그다음, 오늘 배운 예제를 바탕으로 로컬 환경에서 먼저 테스트를 진행해 보세요.
이번 주에는 실제 운영 서버로 옮기기 전, docker compose config 명령어를 통해 작성한 파일에 문법적 오류가 없는지 검증하는 습관을 들여보시길 바라요. 작은 습관이 큰 사고를 막아준답니다.
구축 절차를 꼼꼼하게 문서로 남겨 두면, 나중에 서버를 이전하거나 새로운 인프라를 구성할 때 훨씬 쉬워질 거예요. 여러분의 성공적인 컨테이너 운영을 응원할게요!
관련해서 더 깊은 내용이 궁금하다면 아래 글을 참고해 보세요.
도커 컴포즈 기본 개념 완벽 정리 — 개념부터 실무 활용까지 한눈에 보는 가이드