개인 프로젝트에 Docker 쓰는 이유 — 봇 배포 환경 통일하기
봇을 서버에 처음 올린 날, 저는 세 시간을 날렸습니다. 로컬에서 멀쩡히 돌던 코드가 서버에서는 ModuleNotFoundError로 죽었거든요. 파이썬 버전이 달랐고, 패키지 버전이 달랐고, 시간대까지 달랐습니다. "내 PC에선 됐는데" 문제입니다. Docker는 이 문제를 정면으로 해결합니다.
Docker가 정확히 뭘 해주나
Docker는 프로그램과 그게 필요로 하는 실행 환경(OS 라이브러리·파이썬 버전·패키지)을 통째로 하나의 이미지로 묶어주는 도구입니다. 그 이미지를 실행한 것이 컨테이너고요.
비유하자면 이미지는 냉동 도시락입니다. 재료를 현지에서 조달하는 게 아니라, 완성된 한 끼를 통째로 얼려서 보냅니다. 도착지 부엌에 뭐가 있든 상관없이 데우기만 하면 같은 맛이 납니다. 로컬에서 만든 이미지와 서버에서 도는 이미지가 비트 단위로 동일하기 때문에, 환경 차이라는 변수 자체가 사라집니다.
개인 프로젝트에 오버킬 아니냐고요? 저는 반대로 생각합니다. 혼자 하는 프로젝트일수록 환경 설정 기록을 남길 사람이 나 하나뿐입니다. Dockerfile이 그 기록입니다.
봇용 Dockerfile 최소 구성
파이썬 자동매매 봇을 기준으로 잡아봅니다. 파일은 프로젝트 루트에 Dockerfile이라는 이름으로 둡니다.
# 1) 베이스 이미지 — 태그를 정확히 고정한다
FROM python:3.12-slim
# 2) 타임존 (아래에서 다시 설명)
ENV TZ=Asia/Seoul
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 3) 로그가 버퍼에 갇히지 않게
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# 4) 의존성 먼저 — 캐시를 살리는 핵심
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 5) 그 다음 소스 코드
COPY . .
CMD ["python", "-u", "bot.py"]
4번과 5번의 순서가 중요합니다. Docker는 명령 한 줄마다 결과를 캐시하는데, 바뀌지 않는 것을 위에 두면 재빌드가 빨라집니다. 코드 한 줄만 고쳤을 때 pip install을 다시 돌리지 않게 되죠. 이 순서 하나로 빌드가 2분에서 5초가 됩니다.
FROM python:3.12-slim에서 태그를 반드시 명시하세요. python:latest로 두면 어느 날 파이썬이 올라가면서 봇이 조용히 깨집니다. 환경을 고정하려고 Docker를 쓰는데 태그를 흘려두면 앞뒤가 안 맞습니다.
설정과 로그는 컨테이너 밖으로
초보가 가장 많이 하는 실수는 API 키가 든 설정 파일을 이미지에 굽는 것입니다. 이미지는 복사되고 공유되고 레지스트리에 올라갑니다. 키를 구우면 키를 배포하는 셈입니다.
규칙은 단순합니다. 변하는 것은 전부 볼륨(bind mount)으로 밖에서 주입합니다.
- 설정 파일 — 호스트의
config.ini를 읽기 전용으로 마운트 - 로그 — 컨테이너 안에 쌓으면 재시작 시 날아갑니다. 호스트 폴더로 빼세요
- DB·체결 기록 — 마찬가지로 볼륨
그리고 .dockerignore를 꼭 만드세요. COPY . .가 실수로 키 파일을 쓸어담는 걸 막아줍니다.
.git
__pycache__/
*.log
config.ini
.env
data/
docker compose로 굳히기
실행 옵션을 매번 손으로 치면 언젠가 하나 빠뜨립니다. compose.yaml 한 장에 못박아 둡니다.
services:
trading-bot:
build: .
container_name: trading-bot
restart: unless-stopped
environment:
- TZ=Asia/Seoul
- API_KEY=${API_KEY} # .env에서 주입, 파일에 직접 쓰지 않는다
- API_SECRET=${API_SECRET}
volumes:
- ./config.ini:/app/config.ini:ro # ro = 읽기 전용
- ./logs:/app/logs
logging:
driver: "json-file"
options:
max-size: "10m" # 로그 무한 증식 방지
max-file: "3"
이제 docker compose up -d 한 줄로 뜨고, docker compose logs -f로 실시간 로그를 봅니다. 새 서버로 옮길 때는 이 파일과 설정만 들고 가면 끝입니다.
restart: unless-stopped는 실전에서 체감이 큽니다. 봇이 예외로 죽거나 서버가 재부팅돼도 Docker가 알아서 다시 띄웁니다. 다만 이건 안전망이지 해결책이 아닙니다 — 죽는 원인은 따로 잡아야 합니다. 무한 재시작 루프에 빠지지 않게 로그를 꼭 확인하세요.
반드시 밟는 함정 두 개
1. 타임존. 대부분의 리눅스 이미지는 UTC로 돕니다. 한국 시각 기준으로 "오전 9시 장 시작"을 판단하는 코드가 컨테이너 안에서는 9시간 어긋납니다. 위 Dockerfile의 TZ 설정을 빠뜨리지 마세요. 더 안전한 방법은 애초에 코드에서 시간대를 명시적으로 다루는 것입니다.
2. 로그 버퍼링. 파이썬은 출력이 파이프로 갈 때 버퍼에 모았다가 한 번에 내보냅니다. 그래서 docker logs를 봐도 한참 아무것도 안 나옵니다. 봇이 멈춘 줄 알고 놀라죠. PYTHONUNBUFFERED=1 또는 python -u로 끄면 해결됩니다.
안 쓰는 게 나은 경우
정직하게 말하면, Docker가 항상 정답은 아닙니다.
- GUI가 필요한 봇 — 화면을 띄우는 프로그램은 컨테이너에서 번거롭습니다
- 윈도우 전용 API를 쓰는 경우 — 예를 들어 COM 기반 증권사 API는 리눅스 컨테이너로 못 옮깁니다
- 하루짜리 실험 스크립트 — 그냥
python으로 돌리세요
기준은 이겁니다. "이걸 다른 컴퓨터에서 다시 돌릴 일이 있는가?" 있다면 Docker를 쓸 값어치가 있습니다.
정리
- Docker는 실행 환경을 통째로 고정해 "내 PC에선 됐는데"를 없앤다
- 베이스 이미지 태그를 정확히 고정,
latest금지 requirements.txt를 소스보다 먼저 COPY해서 빌드 캐시를 살린다- API 키·설정·로그는 이미지에 굽지 말고 볼륨/환경변수로 주입,
.dockerignore필수 compose.yaml로 실행 옵션 못박기 +restart: unless-stopped로 자동 복구- 타임존은 UTC가 기본, 파이썬 로그는 버퍼링됨 — 둘 다 명시적으로 끈다
처음 Dockerfile을 쓰는 데 30분이 걸립니다. 하지만 그 30분이, 서버를 옮길 때마다 반복되던 세 시간을 영구히 없애줍니다. 봇을 오래 굴릴 생각이라면 초반에 투자할 값어치가 충분합니다.
댓글
댓글 쓰기