한 파일로 로컬 개발 도구 상자를 만드는 가장 쉬운 방법
제가 아는 모든 개발자는 똑같은 과정을 겪어왔습니다. 새로운 프로젝트를 시작하고 데이터베이스가 필요해집니다. 그래서 PostgreSQL이나 MySQL을 다운로드하고 설정을 마친 후, 5분 만에 설정한 비밀번호를 잊어버립니다. 몇 달이 지나면, 또 다른 프로젝트를 시작하게 되고 그 프로젝트에는 다른 버전의 데이터베이스가 필요하거나, 어쩌면 Redis가 필요할 수도 있습니다. 그러면 갑자기 호스트 머신은 한 번 설치하고 잊어버린 서비스들로 가득한 유령 도시가 됩니다.
이 문제를 해결하는 방법은 많습니다(솔직히 너무 많지만), 하지만 개인 로컬 개발 환경에서는 모든 것을 하나의 Docker Compose 파일에 넣는 것이 가장 간단하고 유지 관리하기 쉬운 방법입니다.
로컬 개발 도구 상자의 구조
대부분의 개발 환경이 필요한 서비스
웹 애플리케이션 작업을 시작할 때 거의 항상 필요한 몇 가지 서비스가 있습니다. 대부분의 개발자들은 정확한 요구사항이 프로젝트마다 다르더라도 비슷한 도구 세트를 사용합니다. 데이터를 저장하기 위해 데이터베이스가 필요하며, 보통 PostgreSQL을 사용합니다. 캐싱이나 세션 관리를 위해 Redis와 같은 키-값 저장소가 자주 필요합니다. 백그라운드 작업이나 이벤트 기반 아키텍처와 관련된 작업을 할 경우, RabbitMQ와 같은 메시지 브로커가 필요하고, 개발 중 이메일 기능을 테스트하려면, 실제로 이메일을 보내는 대신 웹 인터페이스에서 발송된 이메일을 보여주는 MailHog 같은 것이 필요합니다.
우리는 이 모든 서비스를 함께 실행하는 Docker Compose 파일을 만들 것입니다. 이 파일은 당신의 휴대용 개발 도구 상자가 될 것입니다. 프로젝트마다 이 파일을 복사하고, 버전을 조정하며, 매번 일관된 환경을 가질 수 있습니다.
먼저 프로젝트를 위한 새 디렉토리를 만들고 그 디렉토리 안에 docker-compose.yml이라는 파일을 만드세요. 이것이 표준 이름이며, docker compose 명령어가 기본적으로 찾는 이름입니다.
1. 데이터베이스부터 시작하기
PostgreSQL부터 시작합시다. 다음은 PostgreSQL의 기본 서비스 정의입니다:
services:
postgres:
image: postgres:18.2
container_name: local_postgres
ports:
- "5432:5432"
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: myapp_dev
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
이 작은 블록에는 많은 일이 일어나고 있으므로 각 줄을 설명하겠습니다. services 블록은 실행하려는 모든 컨테이너를 나열하는 곳입니다. 각 서비스에는 이름이 있으며, 이 경우에는 postgres입니다.
이미지 라인은 Docker에게 Docker Hub에서 어떤 이미지를 가져올지 알려줍니다. 나는 이를 postgres:18.2로 고정했습니다. 태그 없이 그냥 postgres를 사용하면 최신 태그를 가져오게 되는데, 이는 PostgreSQL의 새로운 주요 버전이 출시될 때 환경이 예기치 않게 변경될 수 있다는 것을 의미합니다(종종 문제가 발생합니다).
container_name은 선택 사항이지만 유용합니다. 이는 컨테이너에 고정된 이름을 설정합니다. 이것이 없으면 Docker는 postgres_1234abcd와 같은 임의의 이름을 생성합니다. 예측 가능한 이름을 가지면 특정 컨테이너에 대해 docker 명령을 실행해야 할 때 더 쉽게 처리할 수 있습니다.
포트 라인은 호스트 머신의 포트를 컨테이너 내부의 포트에 매핑합니다. 형식은 "호스트:컨테이너"입니다. PostgreSQL은 기본적으로 컨테이너 내부의 5432 포트를 청취합니다. 이를 호스트의 동일한 포트에 매핑하면, 마치 PostgreSQL을 네이티브로 설치한 것처럼 노트북에서 실행되는 도구를 통해 localhost:5432를 사용하여 데이터베이스에 연결할 수 있습니다.
환경 섹션은 컨테이너 안에서 환경 변수를 설정합니다. PostgreSQL 이미지에서는 기본 사용자, 비밀번호 및 데이터베이스를 구성하기 위해 이러한 특정 변수를 찾습니다. 여기서는 데모를 위해 간단한 값을 사용했습니다.
볼륨 섹션은 매우 중요합니다. 왜냐하면 컨테이너는 기본적으로 임시적이기 때문입니다. 만약 컨테이너를 삭제하면 데이터도 함께 삭제됩니다(영구 삭제, 휴지통이나 재활용 bin 없음). postgres_data: /var/lib/postgresql/data 라인은 postgres_data라는 이름의 볼륨을 생성하고 이를 PostgreSQL이 데이터 파일을 저장하는 컨테이너 내부 경로에 마운트합니다. 이는 데이터베이스가 컨테이너 재시작이나 심지어 컨테이너 삭제 후에도 살아남는다는 것을 의미합니다. 우리는 파일 하단에서 볼륨을 정의할 것입니다.
마지막으로, restart: unless-stopped는 컨테이너가 충돌할 경우 Docker가 자동으로 재시작하도록 하지만, 사용자가 수동으로 중지하면 재시작하지 않도록 합니다.
2. Redis 추가
다음으로 Redis를 추가해 보겠습니다. Redis는 PostgreSQL에 비해 간단합니다. 복잡한 사용자 시스템이 없고 설정을 위해 환경 변수가 필요하지 않습니다.
redis:
image: redis:8.6.1
container_name: local_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
restart: unless-stopped
3. RabbitMQ 통합
RabbitMQ는 관리 UI가 있기 때문에 좀 더 복잡합니다. 우리는 브라우저에서 그 UI에 접근하기를 원합니다.
rabbitmq:
image: rabbitmq:4.2-management-alpine
container_name: local_rabbitmq
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
volumes:
- rabbitmq_data:/var/lib/rabbitmq
restart: unless-stopped
현재 두 개의 포트가 매핑되어 있습니다. 포트 5672는 애플리케이션이 메시지를 보내고 받기 위해 RabbitMQ에 연결할 때 사용할 주요 AMQP 포트입니다. 포트 15672는 웹 관리 인터페이스를 위한 HTTP 포트입니다. 브라우저를 열고 http://localhost:15672에 접속하여 기본 계정인 guest/guest로 로그인하면 큐, 익스체인지 및 메시지를 확인할 수 있습니다.
4. MailHog로 이메일 잡기
MailHog는 가짜 SMTP 서버를 실행하고 그로 보내진 모든 메시지를 캡처하는 도구입니다. 캡처된 이메일을 볼 수 있는 웹 인터페이스를 제공합니다. 실제 사용자를 실수로 스팸 처리하지 않고 회원가입 흐름, 비밀번호 재설정, 알림 이메일을 테스트하는 데 매우 유용합니다.
mailhog:
image: minidocks/mailhog
container_name: local_mailhog
ports:
- "1025:1025"
- "8025:8025"
restart: unless-stopped
당신의 docker-compose.yml 파일 하단에서 사용한 이름 있는 볼륨을 선언해야 합니다:
volumes:
postgres_data:
redis_data:
rabbitmq_data:
이것은 Docker에게 이러한 볼륨을 생성하고 관리하도록 지시합니다. Docker는 실제 데이터를 호스트 파일 시스템의 어딘가에 저장하지만, 사용자는 그 위치를 알 필요가 없습니다.
5. 모든 것을 합치기
# Local Dev Toolbox
services:
postgres:
image: postgres:18.2
container_name: local_postgres
ports:
- "5432:5432"
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: myapp_dev
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:8.6.1
container_name: local_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
restart: unless-stopped
rabbitmq:
image: rabbitmq:4.2-management-alpine
container_name: local_rabbitmq
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
volumes:
- rabbitmq_data:/var/lib/rabbitmq
restart: unless-stopped
mailhog:
image: minidocks/mailhog
container_name: local_mailhog
ports:
- "1025:1025"
- "8025:8025"
restart: unless-stopped
volumes:
postgres_data:
redis_data:
rabbitmq_data:
그 파일을 프로젝트 디렉토리에 저장하세요.
툴박스 실행
1. docker-compose.yml 파일이 있는 디렉토리에서 터미널을 엽니다. 모든 것을 시작하려면 다음을 실행하세요:
docker compose up -d -d 플래그는 컨테이너를 분리 모드(detached mode)로 실행합니다. 즉, 백그라운드에서 실행되며 터미널 프롬프트를 다시 사용할 수 있습니다.
2. 어떤 것이 실행 중인지 확인하려면 다음을 사용하세요:
docker compose ps이것은 각 컨테이너의 상태, 매핑된 포트, 실행된 시간을 보여줍니다.
3. 하루 일과를 마치면, 서비스를 종료할 수 있지만 삭제하지는 마세요:
docker compose stop
이 명령은 컨테이너를 중지하지만 컨테이너와 그 데이터 볼륨은 그대로 유지합니다. 나중에 docker compose start 또는 docker compose up으로 다시 시작할 수 있습니다.
4. 컨테이너를 완전히 제거하되 데이터 볼륨은 유지하려면 다음을 사용하세요:
docker compose down
5. 볼륨과 그 안의 모든 데이터를 포함하여 모든 것을 제거하려면 -v 플래그를 추가하세요:
docker compose down -v
Docker Compose의 대안
시스템에 서비스를 직접 설치하기
Compose 파일이 로컬 개발 도구 상자를 만드는 유일한 방법일까요? 물론 그렇지 않습니다. 수많은 리눅스 배포판이 있는 것처럼 개발 스택을 만드는 방법도 많습니다.
가장 전통적인 접근 방식은 모든 것을 운영 체제에 직접 설치하는 것입니다. 패키지 관리자를 통해 PostgreSQL을 설치하고, 서비스를 활성화한 후 진행하면 됩니다. 더 넓은 의미에서는, Debian 기반 시스템에 build-essential과 같은 것을 설치하는 것조차도 이미 개발 스택의 시작입니다. 소프트웨어가 로컬에서 컴파일되고 실행될 수 있도록 컴파일러, 헤더, 핵심 도구들을 기본 운영 체제에 가져오는 것입니다.
이 방법은 간단한 경우에는 잘 작동하지만, 프로젝트가 갈라지기 시작하면 문제가 생깁니다. 한 애플리케이션이 PostgreSQL 18을 필요로 하고 다른 애플리케이션이 PostgreSQL 16에 의존한다면, 서로 다른 포트에서 여러 인스턴스를 관리하거나 다른 버전을 재설치해야 합니다. 이를 기본 시스템 서비스로 관리하는 것은 번거로워집니다. 나중에 제거할 때는 기대만큼 깔끔하지 않은 경우가 많고, 설정 파일은 시스템 전체에 흩어져 남는 경향이 있습니다.
asdf와 같은 버전 관리자
asdf와 같은 도구는 언어 런타임을 관리하는 데 훌륭하며 서로 다른 버전의 Python, Node.js 또는 Ruby 사이를 쉽게 전환할 수 있게 해줍니다. 애플리케이션 코드에서는 이것이 실제 문제를 해결해 줍니다.
그러나 데이터베이스, 메시지 브로커, 메일 서버는 그 모델에 깔끔하게 들어맞지 않습니다. 이들은 심으로 교체하는 경량 런타임 바이너리가 아닙니다. 이들은 지속적인 데이터 디렉토리, 백그라운드 프로세스, 시스템 자원을 필요로 합니다. 수동으로 이를 컴파일할 수도 있지만, 그러면 빠르고 재현 가능한 설정을 가진다는 목표가 무산됩니다.
Ansible 및 Taskfiles와 같은 자동화 도구
또 다른 옵션은 자동화 프레임워크와 작업 실행기를 사용하는 것입니다. Ansible과 같은 도구는 서버 프로비저닝에 매우 강력합니다. 마찬가지로 Task와 같은 작업 실행기도 복잡한 명령 시퀀스를 조율할 수 있게 해줍니다.
하지만 지역 개발 환경에서는 PostgreSQL, Redis, 및 RabbitMQ가 설치되고 적절히 구성되도록 보장하는 플레이북을 작성하는 것이 문제에 비해 과도하게 느껴질 수 있습니다.
Docker를 직접 실행 사용
Compose의 가장 가까운 기술적 대안은 순수한 docker run 명령을 사용하는 것입니다. 단일 서비스의 경우, 이것은 깔끔하고 효율적일 수 있습니다. 한 명령어로 컨테이너를 시작하고, 포트를 매핑하며, 작업이 완료됩니다.
서비스가 추가될수록 복잡성이 빠르게 증가합니다. 포트 매핑, 영구 데이터를 위한 명명된 볼륨, 자격 증명을 위한 환경 변수, 재시작 정책 및 사용자 지정 네트워크를 모두 수동으로 지정해야 합니다. 서비스가 서로 통신해야 할 때, 모든 플래그를 기억하는 것은 오류가 발생하기 쉽습니다. 솔직히 말해서, 아무도 그 긴 명령어들을 기억할 수 없습니다!
세 개나 네 개의 컨테이너를 다루고 나면, 많은 개발자들이 긴 명령어를 문서에서 복사하거나 셸 스크립트에 감싸는 방식으로 작업하게 됩니다(저도 그랬습니다). 그 단계에서는 스크립트가 손으로 쓴 것 같고 읽기 어려운 docker-compose.yml 파일 버전처럼 보이게 됩니다.
Compose가 여전히 실용적인 이유
Docker Compose는 길고 명령적인 docker run 호출을 구조적이고 선언적인 구성으로 변환합니다. 모든 서비스는 단일 파일에 정의되며 컨테이너 간의 네트워킹은 자동으로 생성됩니다. Compose의 경우에도 네트워킹은 복잡해질 수 있지만, 그것은 다른 날의 이야기입니다. 대부분의 경우, 여러분은 그것을 다루지 않게 될 것이기 때문입니다.
Compose의 진정한 장점은 단순한 독점성이 아니라 그것이 제공하는 명확성과 재현성에 있습니다. 대부분의 로컬 개발 스택에서는 Compose 파일이 실험하는 모든 프로젝트마다 기본 운영 체제를 영구적으로 변경하지 않으면서 단순성과 제어 사이에서 실용적인 균형을 제공합니다.
컨테이너화된 개발 환경이 시간을 절약하는 이유
Docker Compose 파일을 사용하여 로컬 개발 박스를 만드는 것은 네이티브 서비스를 설치, 구성 및 문제 해결하는 데 필요한 시간을 절약해 줍니다. 프로젝트가 완료되면 프로젝트 디렉터리를 삭제하고, 컨테이너를 중지하면 시스템에는 남는 흔적이 없습니다. 저는 Compose가 모든 경우에 객관적으로 최고 옵션이라고 주장하는 것은 아닙니다. 하지만 설정의 용이성과 시간 효율성 측면에서는 로컬 환경을 정리하기 시작하는 개발자에게는 경쟁하기 어렵습니다. 경험이 쌓이고 요구 사항이 점점 전문화됨에 따라 선호도는 진화하는 경향이 있습니다.
시간이 지나면서 제 작업 흐름도 변화했습니다. 저는 특정 프로젝트에서 asdf와 Podman Pods와 같은 도구로 점차 이동해 왔는데, 특히 데몬이 필요 없고 호스트 시스템과 더 밀접하게 통합된 경험을 선호하는 경우에 그렇습니다. 그러나 핵심 아이디어는 그대로입니다: 관련 서비스들을 그룹화하고, 기본 운영 체제와 분리하며, 전체 스택을 재현 가능하게 만드는 것입니다.






