
Docker Compose로 개발 환경을 완벽하게 재현 가능하게 만드는 실전 테크닉
요약
Docker Compose를 활용하여 로컬(ARM64)과 운영(x86_64) 환경 간의 아키텍처 차이로 발생하는 문제를 해결하는 실전 테크닉을 다룹니다. 무조건적인 플랫폼 강제 대신, 개발 생산성을 유지하면서 운영 환경의 일관성을 확보하는 설계 방식을 제안합니다.
핵심 포인트
- Apple Silicon과 x86_64 간의 아키텍처 불일치 문제 해결
- platform 설정 강제 시 발생하는 개발 속도 저하 경고
- 로컬은 네이티브 아키텍처를 사용하고 운영 이미지는 CI/CD에서 빌드 권장
- docker-compose.override.yml을 활용한 환경별 설정 분리
「로컬에서는 돌아갔으니까 괜찮습니다!」
그렇게 자신만만하게 말을 남기고 금요일 저녁에 릴리스를 완료한 뒤, 의기양양하게 이자카야로 향했다. 하지만 새벽 2시, 맥주로 취기가 오른 머리를 때려 깨운 것은 가차 없이 울려 퍼지는 PagerDuty의 알람이었다. 식은땀으로 파자마를 적시며, 떨리는 손으로 롤백(Rollback) 버튼을 연타한다――.
그, 심장이 짓눌리는 듯한 절망을 당신은 맛보지 않았으면 한다.
「Docker를 사용하고 있으니까, 어디서든 똑같이 돌아갈 거야」
그런 달콤한 환상을 품고 있던 시기가 나에게도 있었다. 하지만 Docker는 마법의 은탄환(Silver Bullet)이 아니다. 본질을 이해하지 못하고 막 다루면, 로컬(특히 Apple Silicon Mac)과 운영 환경(Linux/x86_64)의 격차에 발목을 잡혀 너무나도 쉽게 시스템은 침몰한다.
이 기사에서는 내가 실제 프로젝트나 개인 개발에서 피를 흘리며 겪었던 **「Docker 환경 차이의 치명적인 함정」**과, 그것을 힘으로 해결하는 것이 아니라 우아하게 해결하기 위해 도달한 Docker Compose의 실전 테크닉을 공유한다.
M1/M2/M3 Mac(ARM64)의 보급에 따라, 개발 환경과 운영 환경(일반적으로 Intel/AMD의 x86_64)의 「CPU 아키텍처의 괴리」가 심각한 문제로 부상했다.
로컬 Mac에서 완벽하게 동작하던 컨테이너를 의기양양하게 운영 Linux 서버에 배포한 직후, 컨테이너가 재시작을 반복하며 로그에 다음과 같은 한 줄이 새겨진다.
standard_init_linux.go:228: exec user process caused: exec format error
원인은 로컬 환경(ARM64)용으로 빌드된 이미지를 다른 아키텍처(x86_64)인 운영 환경에서 실행하려고 했기 때문이다.
인터넷의 기술 기사를 검색하면 자주 이런 「해결책」이 적혀 있다.
「
docker-compose.yml에 platform: linux/amd64를 쓰면 해결됩니다!"
확실히, 이렇게 하면 동작한다. Apple Silicon Mac 위에서 x86_64 시뮬레이션(Rosetta 2 / QEMU)이 실행되어 운영 환경과 동일한 아키텍처로 동작한다.
하지만, 이것은 함정이다.
이 설정을 강제하면 로컬에서의 빌드나 실행 속도가 극단적으로 느려진다. 패키지 설치(pip install이나 npm install)에 통상적인 수 배에서 열 배 가까운 시간이 걸리며, 개발 시의 핫 리로드(Hot Reload)조차 더뎌지게 된다. 개발 생산성을 희생하면서 환경을 맞추는 것은 본말전도다.
올바른 접근 방식은 **「개발 환경에서는 로컬의 네이티브 아키텍처(ARM64)로 돌리고, 운영용 이미지는 CI/CD 파이프라인에서 운영용(amd64)으로 빌드한다」**는 설계로 하는 것이다.
정말로 로컬에서 운영과 동일한 아키텍처를 검증하고 싶은 경우에만, 오버라이드 파일(docker-compose.override.yml)이나 환경 변수를 사용하여 제어한다.
version: '3.8'
services:
web:
...
운영 환경이나, 로컬에서 운영과 동일한 거동을 엄격하게 테스트하고 싶은 경우에는 설정 파일을 겹쳐서(머지(Merge)하여) 사용한다.
# docker-compose.prod.yml
services:
web:
...
실행할 때는 다음과 같이 파일을 여러 개 지정하여 띄운다.
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
이를 통해 평소 개발(DX)의 쾌적함을 유지하면서도 운영 환경과의 일관성을 담보할 수 있다.
다음으로 개발자를 괴롭히는 것이 호스트(로컬 PC)와 컨테이너 간의 **「파일 퍼미션(Permission) 문제」**다.
- 컨테이너 내부에서 생성된 로그나 캐시 파일이 호스트 측(Mac/Linux)에서 편집·삭제할 수 없다.
- 반대로, 호스트 측에서 편집한 소스 코드를 컨테이너 내부의 비 루트(non-root) 사용자(
node나python프로세스)가 읽지 못해Permission Denied로 크래시(Crash)가 발생한다.
여기서 「번거로우니까」라는 이유로 운영 서버의 디렉터리 권한을 chmod -R 777로 변경하는 악마의 유혹에 넘어가서는 안 된다. 그것은 보안 감사에서 즉시 레드카드를 받게 될, 취약점투성이인 서버를 만드는 원인이 된다.
Mac의 Docker Desktop은 가상 머신(VM)을 거치기 때문에 바인드 마운트 (Bind Mount) 시의 UID를 어느 정도 자동으로 매핑해 준다. 하지만, Linux 데스크톱 환경이나 CI/CD용 셀프 호스트 러너 (Self-hosted Runner), 그리고 프로덕션 Linux 서버 환경에서는 이 문제가 직접적으로 문제를 일으킨다.
컨테이너 내부의 기본 사용자(통상 root, UID: 0)가 작성한 파일은 호스트 측에서 root 소유로 보이기 때문에, 일반 사용자(UID: 1000 등)인 당신이 접근할 수 없게 된다.
이 문제는 컨테이너 실행 시 호스트의 UID/GID를 동적으로 주입하고, 컨테이너 내부의 프로세스를 해당 권한으로 실행함으로써 완전히 해결할 수 있다.
먼저, 프로젝트 루트에 다음과 같은 .env 파일을 생성한다 (또는 실행 스크립트에서 자동으로 생성한다).
# .env
UID=1000
GID=1000
Tips: Linux 환경이라면 .bashrc 등에 export UID=$(id -u) 및 export GID=$(id -g)를 설정해 두면, Docker Compose가 자동으로 셸의 환경 변수를 읽어온다.
다음으로, docker-compose.yml에서 user 명령을 동적으로 매핑한다.
services:
web:
build: .
...
Dockerfile 측에서도 특정 UID로 동작할 수 있도록 사용자를 사전에 정의해 두는 것이 보안상의 베스트 프랙티스 (Best Practice)이다.
FROM python:3.11-slim
# 애플리케이션을 실행할 비특권 사용자를 생성
RUN groupadd -g 1000 appgroup && \
...
이 접근 방식의 묘미는 **"컨테이너 내부에 불필요한 root 권한을 남기지 않으면서, 개발 시와 프로덕션 시 모두 보안 기준을 충족한 상태로 권한 오류 (Permission Error)를 근절할 수 있다"**는 점에 있다.
Docker Compose에서 가장 빈번하게 발생하는 트러블 중 하나는 **"컨테이너의 실행 순서"**에 관한 오해다.
"depends_on에 db를 작성했는데도, 앱이 데이터베이스 연결 오류로 인해 실행에 실패한다"
이런 경험이 있지 않은가?
# 잘못된 예
services:
web:
...
Docker Compose에서 depends_on의 기본 동작은 **"의존 대상 컨테이너의 프로세스가 시작되었는가"**만을 확인한다. PostgreSQL이나 MySQL 같은 데이터베이스는 프로세스가 시작된 후, 내부 초기화 처리(데이터 디렉터리 생성 및 마이그레이션 준비)를 마치고 연결을 수락할 수 있게 되기까지 수 초에서 수십 초의 타임 랙 (Time Lag)이 발생한다.
이 "준비 중"인 틈에 애플리케이션(Web)이 연결을 시도하면, 당연하게도 커넥션 오류 (Connection Error)로 즉시 종료된다.
가장 깔끔한 해결책은 데이터베이스 측에 "자신이 정상적으로 가동 중인지"를 판별하는 healthcheck를 정의하고, 애플리케이션 측에서 해당 헬스 체크 (Health Check)를 통과할 때까지 실행을 대기시키는 것이다.
services:
web:
build: .
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기