
Docker Compose: 로컬 개발 환경을 한 방에 세팅하기
새 팀원이 올 때마다 '로컬 세팅 문서' 보내주는 게 지쳤다. docker compose up 하나로 DB, Redis, 앱 서버를 한 번에 띄우는 방법.

새 팀원이 올 때마다 '로컬 세팅 문서' 보내주는 게 지쳤다. docker compose up 하나로 DB, Redis, 앱 서버를 한 번에 띄우는 방법.
서버를 끄지 않고 배포하는 법. 롤링, 카나리, 블루-그린의 차이점과 실제 구축 전략. DB 마이그레이션의 난제(팽창-수축 패턴)와 AWS CodeDeploy 활용법까지 심층 분석합니다.

새벽엔 낭비하고 점심엔 터지는 서버 문제 해결기. '택시 배차'와 '피자 배달' 비유로 알아보는 오토 스케일링과 서버리스의 차이, 그리고 Spot Instance를 활용한 비용 절감 꿀팁.

내 서버가 해킹당하지 않는 이유. 포트와 IP를 검사하는 '패킷 필터링'부터 AWS Security Group까지, 방화벽의 진화 과정.

왜 넷플릭스는 멀쩡한 서버를 랜덤하게 꺼버릴까요? 시스템의 약점을 찾기 위해 고의로 장애를 주입하는 카오스 엔지니어링의 철학과 실천 방법(GameDay)을 소개합니다.

새 팀원이 들어왔다. 반가운 마음에 온보딩 문서를 건넸다. PostgreSQL 설치, Node.js 버전 맞추기, Redis 깔기, 환경변수 설정... 25단계쯤 됐다. 두 시간 뒤, Slack 메시지가 왔다.
"PostgreSQL 설치가 안 되는데요. M1이라서 그런가요?"
또 한 시간 뒤.
"Node.js 버전은 맞는데 왜 npm install이 실패하죠?"
그날 오후는 그렇게 날아갔다. 나는 그때 이해했다. 로컬 환경 세팅이라는 건, 마치 레시피 없이 요리를 가르치는 것과 같다는 걸. 재료(OS)도 다르고, 도구(칩셋)도 다르고, 불 세기(시스템 설정)도 다른데 같은 결과를 기대하는 건 무리였다. 더 정확히 말하면, "내 컴퓨터에선 됩니다"는 말이 세상에서 가장 쓸모없는 말이라는 걸 그날 뼈저리게 정리해본다.
Docker Compose를 만난 건 그 이후였다. 그리고 이제는 새 팀원에게 딱 한 줄만 보낸다.
docker compose up
처음 Docker를 접했을 때 든 생각은 "아, 가상머신 같은 건가?"였다. 틀렸다. VM은 집을 통째로 들고 이사하는 거고, 컨테이너는 필요한 방만 조립해서 가는 거였다.
VM은 운영체제를 통째로 복사한다. 메모리 2GB, 디스크 20GB가 기본이다. 부팅도 느리다. 반면 컨테이너는 호스트 OS의 커널을 공유하면서, 앱 실행에 필요한 것만 격리한다. 가볍고, 빠르고, 일관되다.
하지만 Docker만으로는 부족했다. PostgreSQL 컨테이너를 띄우고, Redis 컨테이너를 띄우고, 앱 컨테이너를 띄우고... 각각 docker run 명령어를 외워야 했다. 포트는? 볼륨은? 네트워크는? 매번 타이핑하다가 오타가 나면 디버깅 지옥이었다.
그때 Docker Compose가 나타났다. 마치 레고 조립 설명서처럼, YAML 파일 하나에 "어떤 컨테이너를, 어떤 설정으로, 어떤 순서로 띄울지"를 선언하면 끝이었다. 더 이상 긴 명령어를 외울 필요가 없었다.
핵심은 이거였다. "한 번 작성하면, 누구나 같은 환경을 띄울 수 있다."
docker-compose.yml 파일은 크게 세 영역으로 나뉜다.
version: '3.8'
services:
# 여기에 컨테이너들을 정의
volumes:
# 데이터를 저장할 볼륨
networks:
# 컨테이너 간 통신 네트워크
Services는 실행할 컨테이너들이다. 각 서비스는 독립적인 프로세스로 돌아가지만, 같은 네트워크에 묶여 서로 통신할 수 있다. 마치 한 사무실에 있는 팀들처럼.
Volumes는 컨테이너가 죽어도 살아남는 저장소다. 컨테이너는 휘발성이다. 삭제하면 내부 데이터도 사라진다. 하지만 볼륨에 저장한 데이터는 호스트 머신에 남아서, 컨테이너를 재시작해도 그대로 유지된다.
Networks는 컨테이너들이 서로를 찾는 방법이다. 같은 네트워크에 있으면 서비스 이름으로 통신할 수 있다. localhost:5432 대신 postgres:5432처럼.
구체적인 예제를 보자. 전형적인 웹 앱 스택이다.
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: dev-postgres
environment:
POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: dev-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
app:
build:
context: .
dockerfile: Dockerfile.dev
container_name: dev-app
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
DATABASE_URL: postgresql://devuser:devpass@postgres:5432/myapp
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
command: npm run dev
volumes:
postgres-data:
redis-data:
networks:
default:
driver: bridge
이 파일 하나로 세 개의 컨테이너가 오케스트라처럼 동작한다. 포인트를 뜯어보자.
volumes:
- .:/app
- /app/node_modules
첫 번째 줄은 현재 디렉토리를 컨테이너의 /app에 마운트한다. 코드를 수정하면 즉시 컨테이너 안에도 반영된다. 핫 리로드가 작동하는 이유다.
두 번째 줄은 node_modules를 별도 볼륨으로 분리한다. 왜? 호스트의 node_modules와 컨테이너의 node_modules가 다를 수 있기 때문이다. (M1 Mac vs Linux 바이너리 차이) 이렇게 하면 컨테이너는 자기만의 node_modules를 유지한다.
이건 마치 공유 작업실에 개인 사물함을 두는 것과 같다. 작업 공간은 공유하지만, 개인 도구는 따로 보관한다.
하드코딩된 비밀번호가 보이는가? 실제로는 절대 안 된다. .env 파일로 분리하자.
# .env
POSTGRES_USER=devuser
POSTGRES_PASSWORD=devpass
POSTGRES_DB=myapp
그리고 docker-compose.yml에서 참조한다.
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
더 간단하게는 env_file을 쓸 수도 있다.
env_file:
- .env
환경변수는 열쇠다. 코드에 열쇠를 박아두면 문을 바꿀 때마다 코드를 고쳐야 한다. 분리하면 환경(개발/스테이징/프로덕션)마다 다른 열쇠를 쓸 수 있다.
depends_on:
postgres:
condition: service_healthy
앱이 실행되려면 DB가 먼저 준비돼야 한다. depends_on은 이 순서를 보장한다. 하지만 컨테이너가 "시작됨"과 "준비됨"은 다르다.
PostgreSQL 컨테이너는 1초 만에 뜨지만, 실제로 연결을 받을 준비가 되려면 3~4초가 더 걸린다. 이때 앱이 바로 연결을 시도하면 "connection refused" 에러가 난다.
Healthcheck가 이걸 해결한다. PostgreSQL이 pg_isready 테스트를 통과할 때까지 기다렸다가, 그제서야 앱을 시작한다.
이건 식당에서 테이블이 준비될 때까지 기다리는 것과 같다. 문을 열었다고(started) 바로 앉을 수 있는 게 아니다. 테이블 세팅이 끝나야(healthy) 앉을 수 있다.
Healthcheck가 없는 서비스도 있다. Redis처럼 간단한 경우, 또는 외부 API처럼 제어할 수 없는 경우. 이럴 땐 wait-for-it 스크립트를 쓴다.
# wait-for-it.sh
#!/bin/sh
until nc -z postgres 5432; do
echo "Waiting for postgres..."
sleep 1
done
echo "PostgreSQL is ready!"
exec "$@"
그리고 Dockerfile에서 entrypoint로 지정한다.
COPY wait-for-it.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/wait-for-it.sh
ENTRYPOINT ["wait-for-it.sh"]
CMD ["npm", "run", "dev"]
이제 앱은 PostgreSQL이 정말로 포트를 열 때까지 기다린다. 폴링 방식이라 우아하지는 않지만, 확실하다. 이 패턴을 처음 접했을 때는 "이게 정말 필요해?"라고 생각했다. 그런데 헬스체크 없이 컨테이너가 연결 거부 에러로 죽는 걸 세 번쯤 경험하고 나서 와닿았다. 견고함은 항상 조금 더 복잡한 설정에서 나온다는 걸.
개발 환경에서는 핫 리로드, 디버깅 포트, 소스 마운트가 필요하다. 프로덕션에서는? 불필요하고, 심지어 위험하다.
그래서 파일을 분리한다.
# docker-compose.yml (기본, 공통)
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
# docker-compose.dev.yml (개발 전용)
services:
app:
volumes:
- .:/app
command: npm run dev
# docker-compose.prod.yml (프로덕션 전용)
services:
app:
command: npm start
실행할 때 조합한다.
# 개발
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
# 프로덕션
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
이건 레고 베이스 플레이트에 다른 블록을 조합하는 것과 같다. 기본 구조는 같지만, 용도에 따라 다르게 확장한다.
예전엔 docker-compose(하이픈)였다. 파이썬으로 만든 별도 도구였다. 지금은 docker compose(공백)다. Docker CLI에 통합됐고, Go로 다시 작성됐다. 더 빠르고, 더 안정적이다.
주요 차이:
docker-compose up → docker compose up2023년 7월, Docker는 v1 지원을 종료했다. 이제 v2가 표준이다.
문제가 생겼다. 컨테이너가 죽거나, 연결이 안 되거나, 예상대로 작동하지 않는다. 어떻게 디버깅할까?
Logs: 가장 먼저 볼 것.
docker compose logs app
docker compose logs -f postgres # 실시간 tail
Exec: 컨테이너 안에 들어가서 직접 확인.
docker compose exec app sh
docker compose exec postgres psql -U devuser -d myapp
Inspect: 컨테이너의 내부 설정 확인.
docker compose exec app env # 환경변수 확인
docker inspect dev-postgres # 전체 설정 JSON
이 세 가지는 탐정의 돋보기, 발자국 추적, 지문 채취와 같다. 로그로 흔적을 보고, exec으로 현장에 들어가고, inspect로 증거를 수집한다.
Docker Compose가 만능은 아니다. 오버엔지니어링일 때도 있다.
쓰지 말아야 할 때:결국 도구는 문제에 맞춰야 한다. 못 하나 박는데 전동 드릴은 과하다. 하지만 책장을 조립한다면 필수다.
Docker Compose를 쓰고 나서, 온보딩 시간이 2시간에서 5분으로 줄었다. 새 팀원은 이제 README 한 줄만 보면 된다.
docker compose up
로컬 환경이 일관되니 "내 컴퓨터에선 되는데" 같은 말도 사라졌다. 버그를 재현하기도 쉬워졌다. 같은 환경을 몇 초 만에 복제할 수 있으니까. 그리고 솔직히, 이게 와닿았다. 기술은 반복되는 마찰을 제거하기 위해 존재한다. 매번 환경 세팅으로 하루를 낭비하는 건 그냥 고통이다. 한 번의 YAML 작성이 수십 번의 고통을 없애준다는 걸 직접 경험하고 나서야 이해했다.
핵심 교훈 세 가지:Docker Compose는 마법이 아니다. 그저 반복 작업을 자동화하고, 실수를 줄이고, 협업을 쉽게 만드는 도구일 뿐이다. 하지만 그게 전부다. 개발은 본질에 집중해야 한다. 환경 세팅에 시간을 쓰는 건 낭비다.
이제 당신도 새 팀원에게 한 줄만 보내면 된다.
docker compose up
그리고 커피나 마시러 가면 된다.