
왜 지금 services 블록 구성을 알아야 할까요?
새로운 프로젝트를 시작하거나 서버 환경을 구축할 때, 매번 긴 docker run 명령어를 터미널에 복사하고 붙여넣는 과정이 얼마나 번거로운지 잘 알고 있어요. 네트워크를 연결하고, 볼륨을 마운트하고, 환경 변수를 일일이 설정하다 보면 어느 순간 명령어 하나를 빼먹어 전체 시스템이 꼬여버리는 경험을 해보셨을 거예요. 특히 컨테이너의 개수가 늘어나면 어떤 설정이 어디에 적용되어 있는지 파악하는 것조차 힘들어지죠.
이런 혼란을 해결해 주는 구세주가 바로 도커 컴포즈(Docker Compose)예요. 그중에서도 가장 핵심이 되는 것이 바로 컴포즈 services 정리를 통해 이해해야 하는 services 블록이에요. 이 블록은 우리가 실행하고자 하는 각각의 컨테이너가 어떤 이미지를 사용하고, 어떤 포트를 열며, 다른 컨테이너와 어떻게 통신할지를 정의하는 설계도와 같아요.
단순히 설정을 나열하는 것을 넘어, 서비스 간의 의존성을 관리하고 효율적인 자원 배분을 결정하는 단계가 바로 이 services 블록을 얼마나 정교하게 짜느냐에 달려 있어요. 이 글을 끝까지 읽고 나면, 복잡한 컨테이너 환경을 코드 한 줄로 관리할 수 있는 능력을 갖추게 될 거예요.
- services 블록의 근본적인 개념과 구성 요소
- docker-compose.yml 작성 시 반드시 알아야 할 핵심 옵션
- 실무에서 즉시 활용 가능한 설정 예시와 상세 해설
- 컨테이너 운영 시 흔히 마주치는 오류와 해결 방법
컨테이너 운영을 위한 사전 지식과 체크리스트
본격적으로 파일을 작성하기 전에, 우리가 다룰 용어들의 개념을 명확히 짚고 넘어가야 해요. 용어가 헷갈리면 설정 옵션을 적용할 때 의도치 않은 결과가 발생할 수 있거든요. 도커 컴포즈 환경에서 services는 단순히 컨테이너 하나를 의미하는 것이 아니라, 특정 역할을 수행하는 애플리케이션의 단위라고 이해하는 것이 훨씬 정확해요.
반드시 알아야 할 핵심 용어
가장 먼저 이미지(Image)와 컨테이너(Container)의 차이를 구분해야 해요. 이미지는 실행 가능한 프로그램이 담긴 붕어빵 틀이고, 컨테이너는 그 틀에서 찍어낸 실제 붕어빵이에요. services 블록은 바로 이 붕어빵을 어떻게 만들고 어떤 양념을 넣을지를 결정하는 곳이죠.
또한, 네트워크(Network)는 서비스 간의 통로를, 볼륨(Volume)은 데이터를 저장하는 창고를 의미해요. 이 요소들이 services 블록 안에서 어떻게 유기적으로 연결되는지를 파악하는 것이 컴포즈 services 개념정리의 핵심이에요. 단순히 명령어를 외우는 것이 아니라, 각 설정이 컨테이너의 생명주기에 어떤 영향을 미치는지 이해해야 해요.
설정 방식 선택을 위한 기준
서비스를 구성할 때 어떤 방식을 사용할지 결정하는 것도 중요한 작업이에요. 아래 표를 통해 상황에 맞는 적절한 설정 기준을 확인해 보세요.
| 구분 항목 | 이미지 사용 (image) | 직접 빌드 (build) |
|---|---|---|
| 주요 용도 | 이미 완성된 솔루션 사용 (DB, Redis 등) | 자체 개발한 애플리케이션 배포 |
| 장점 | 설정이 매우 간편하고 안정적임 | 소스 코드 변경 사항 즉시 반영 가능 |
| 단점 | 커스터마이징에 한계가 있음 | 빌드 시간이 소요됨 |
| 권장 상황 | 검증된 오픈소스 소프트웨어 활용 시 | 로컬 개발 환경 구축 시 |
준비가 되셨나요? 이제 이 개념들을 바탕으로 실제 파일을 어떻게 작성하는지 구체적인 단계별 실행법으로 들어가 볼게요.
도커 컴포즈 services 블록 완벽 구성법
이제 실전이에요. docker-compose.yml 파일의 핵심인 services 영역을 단계별로 파헤쳐 볼게요. 이 과정은 단순히 옵션을 적는 것이 아니라, 서비스의 명세를 정의하는 과정이에요.
STEP 1. 서비스 이름 정의와 기본 구조 잡기
가장 먼저 해야 할 일은 각 서비스에 이름을 붙이는 것이에요. 이 이름은 나중에 컨테이너끼리 서로를 찾을 때 사용하는 호스트 이름(Hostname)이 되기 때문에 매우 중요해요. 예를 들어, 데이터베이스 서비스 이름을 db라고 지었다면, 웹 애플리케이션 컨테이너는 네트워크를 통해 db:5432와 같은 방식으로 데이터베이스에 접속할 수 있어요.
구조는 항상 최상위에 services: 키를 두고, 그 아래에 각 서비스의 이름을 들여쓰기하여 작성해요. 들여쓰기는 YAML 문법의 핵심이므로 반드시 스페이스(Space)를 사용하고 탭(Tab)은 피해야 한다는 점을 잊지 마세요.
STEP 2. 이미지 지정과 빌드 컨텍스트 설정하기
서비스가 어떤 환경에서 돌아갈지를 결정하는 단계예요. 두 가지 방식이 있어요. 첫째는 Docker Hub에 이미 올라와 있는 이미지를 그대로 가져오는 image 옵션이고, 둘째는 내 컴퓨터에 있는 소스 코드를 바탕으로 이미지를 만드는 build 옵션이에요.
만약 직접 만든 앱을 실행한다면 아래와 같이 설정할 수 있어요.
build: . 처럼 경로를 지정하면 현재 디렉토리에 있는 Dockerfile을 찾아 이미지를 생성해요. 개발 중에는 이 방식이 훨씬 효율적이에요.이미 검증된 데이터베이스를 사용한다면 image: postgres:15-alpine처럼 특정 버전을 명시하는 것이 좋습니다. 버전을 명시하지 않고 최신(latest) 버전을 쓰면, 나중에 갑작스러운 업데이트로 인해 서비스가 깨질 수 있기 때문이에요.
STEP 3. 포트 포워딩과 볼륨 마운트 설정하기
컨테이너는 외부 세계와 격리되어 있어요. 그래서 우리가 서비스를 사용하려면 통로를 열어줘야 하는데, 이것이 바로 ports 옵션이에요. "8080:80"이라고 적으면, 내 컴퓨터의 8080 포트로 들어온 요청이 컨테이너 내부의 80 포트로 전달돼요.
데이터의 영속성도 챙겨야 해요. 컨테이너는 삭제되면 내부 데이터도 모두 사라지거든요. 그래서 volumes를 통해 컨테이너 내부의 특정 폴더를 내 컴퓨터의 폴더와 연결해야 해요. 데이터베이스 데이터가 들어있는 폴더는 반드시 볼륨으로 설정해야 컨테이너를 재시작해도 데이터가 유지됩니다.
STEP 4. 환경 변수와 의존성 관리하기
보안이 중요한 비밀번호나 API 키는 설정 파일에 직접 적지 말고 environment나 env_file 옵션을 사용하세요. 또한, 웹 서버가 실행되기 전에 반드시 데이터베이스가 먼저 켜져 있어야 한다면 depends_on 옵션을 사용하여 실행 순서를 제어할 수 있어요. 하지만 주의할 점은 depends_on이 컨테이너의 ‘실행’만을 보장할 뿐, 데이터베이스 내부의 서비스가 ‘준비 완료’될 때까지 기다려주지는 않는다는 점이에요.
STEP 5. 종합 실전 예시 시나리오
지금까지 배운 내용을 모두 합친 예시 코드를 살펴볼게요. 이 코드를 그대로 복사해서 테스트 환경에서 실행해 보며 구조를 익혀보세요.
services:
web-app:
build: .
ports:
- "3000:3000"
environment:
- DB_HOST=db
- DB_PASSWORD=mysecretpassword
depends_on:
- db
networks:
- app-network
db:
image: postgres:15
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=mysecretpassword
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
db-data:
위 예시는 웹 애플리케이션과 데이터베이스가 하나의 네트워크 안에서 통신하며, 데이터베이스의 데이터는 독립된 볼륨에 안전하게 저장되는 구조를 보여주고 있어요. 각 설정이 왜 필요한지 다시 한번 머릿속으로 그려보세요.
자주 하는 실수와 해결법
실무에서 많은 개발자가 저지르는 실수들을 모아봤어요. 비슷한 상황을 겪고 있다면 아래 내용을 확인해 보세요.
❌ 실수: 포트 충돌 발생
왜 발생하는가: 이미 내 컴퓨터에서 사용 중인 포트를 ports에 설정했을 때 발생해요.
✅ 해결법: netstat -ano 명령어로 사용 중인 포트를 확인하거나, 호스트 포트 번호를 변경하세요.
❌ 실수: 데이터베이스 연결 실패
왜 발생하는가: depends_on만 믿고 DB가 완전히 구동되기 전에 웹 앱이 접속을 시도하기 때문이에요.
✅ 해결법: 애플리케이션 코드 내에 재시도(Retry) 로직을 넣거나, wait-for-it.sh 같은 스크립트를 활용하세요.
❌ 실수: 파일 권한 문제
왜 발생하는가: 호스트의 폴더를 volumes로 연결했을 때, 컨테이너 내부 사용자가 해당 폴더에 쓸 권한이 없는 경우예요.
✅ 해결법: 호스트 폴더의 권한을 조정하거나, Named Volume을 사용하여 도커가 직접 관리하도록 하세요.
❌ 실수: 환경 변수 오타
왜 발생하는가: .env 파일의 변수명과 docker-compose.yml의 변수명이 일치하지 않을 때 발생해요.
✅ 해결법: 변수명을 대문자로 통일하고, 설정 후 docker compose config 명령어로 문법을 검증하세요.
❌ 실수: 네트워크 격리 문제
왜 발생하는가: 서로 다른 네트워크에 속한 서비스끼리는 이름을 통해 통신할 수 없어요.
✅ 해결법: 통신이 필요한 서비스들은 동일한 networks 항목에 포함시켜 주세요.
자주 묻는 질문
Q. docker-compose와 docker compose의 차이가 무엇인가요?
과거에는 별도의 도구로 설치해야 했던 docker-compose(하이픈 포함)와 달리, 최신 Docker Desktop이나 Docker Engine에서는 도커의 플러그인 형태로 포함된 docker compose(공백) 사용을 권장하고 있어요. 기능은 거의 동일하지만 최신 기능을 쓰려면 공백 형식을 권장해요.
Q. services 블록 안에 수십 개의 서비스를 넣어도 괜찮을까요?
너무 많은 서비스를 하나의 파일에 넣으면 관리가 매우 힘들어져요. 서비스의 성격에 따라 여러 개의 파일로 분리하고 -f 옵션으로 병합하여 사용하는 것이 유지보수에 유리해요.
Q. 이미지 버전을 항상 최신으로 쓰는 게 좋지 않나요?
운영 환경(Production)에서는 절대 금물이에요! 최신 이미지는 때때로 예기치 못한 버그를 포함할 수 있으므로, 반드시 특정 버전(Tag)을 명시하여 환경의 안정성을 확보해야 해요.
Q. 볼륨을 사용하면 용량이 너무 많이 차지하지 않나요?
데이터가 계속 쌓이면 용량이 늘어나는 것은 당연해요. 주기적으로 사용하지 않는 볼륨을 정리하려면 docker volume prune 명령어를 사용해 보세요.