Docker 실행 환경과 개발 도구 통합: GitHub, Python, 그리고 실전 워크플로우 | 가나투데이

Docker 실행 환경과 개발 도구 통합: GitHub, Python, 그리고 실전 워크플로우

Docker가 실행되는 환경 이해하기

Docker를 제대로 활용하려면 먼저 Docker가 어떤 환경에서 실행되는지 이해해야 합니다.

Docker의 아키텍처

Docker는 크게 세 가지 구성 요소로 이루어져 있습니다:

1. Docker Client (명령줄 도구)

  • 우리가 docker run, docker build 같은 명령어를 입력하는 인터페이스
  • 실제 작업은 Docker Daemon에게 요청합니다

2. Docker Daemon (dockerd)

  • 백그라운드에서 실행되며 실제로 컨테이너를 관리하는 서버
  • 이미지 빌드, 컨테이너 실행, 네트워크 관리 등을 담당

3. Docker Registry (Docker Hub)

  • 이미지를 저장하고 공유하는 저장소
  • GitHub가 코드 저장소라면, Docker Hub는 이미지 저장소

운영체제별 실행 환경

Linux에서의 Docker

Linux는 Docker의 본고장입니다. Docker는 Linux 커널의 기능을 직접 사용하므로 가장 효율적으로 실행됩니다.

# Docker가 사용하는 Linux 커널 기능 확인
docker info | grep "Kernel Version"
docker info | grep "Operating System"

# 시스템 리소스 확인
docker system df
free -h  # 메모리 확인
df -h    # 디스크 확인

Linux의 장점:

  • 네이티브 성능 (가상화 오버헤드 없음)
  • 서버 환경에 최적화
  • 리소스 효율성 극대화

Windows에서의 Docker

Windows는 두 가지 방식으로 Docker를 실행합니다:

Windows 10/11 Pro 이상: WSL 2 (Windows Subsystem for Linux 2)

# WSL 2 상태 확인
wsl --list --verbose

# Docker Desktop 설정 확인
docker context ls
docker version

Docker Desktop은 WSL 2 내부에서 경량 Linux VM을 실행하여 Docker를 구동합니다.

Windows Home: Hyper-V 기반

  • 가상화 기술을 사용하여 Linux 환경 제공
  • 약간의 성능 오버헤드 존재

macOS에서의 Docker

macOS는 Linux 커널을 사용하지 않으므로, Docker Desktop이 경량 가상 머신을 실행합니다.

# Docker 리소스 할당 확인
docker info | grep "CPUs"
docker info | grep "Total Memory"

# macOS에서 사용 중인 하이퍼바이저 확인
docker context ls

Docker Desktop 설정에서 CPU, 메모리, 디스크 할당량을 조정할 수 있습니다.

Docker 실행 환경 최적화

# Docker 데몬 설정 확인
docker info

# 리소스 사용량 실시간 모니터링
docker stats

# 특정 컨테이너에 리소스 제한 설정
docker run -d \
  --memory="512m" \
  --cpus="1.5" \
  --name limited-container \
  nginx

GitHub와 Docker 통합: 현대적 개발 워크플로우

GitHub와 Docker를 함께 사용하면 코드와 실행 환경을 동시에 버전 관리할 수 있습니다.

프로젝트 구조 설계

my-project/
├── .github/
│   └── workflows/
│       └── docker-build.yml    # GitHub Actions
├── src/
│   └── app.py
├── tests/
│   └── test_app.py
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .gitignore
├── requirements.txt
└── README.md

.gitignore와 .dockerignore 설정

.gitignore (Git이 무시할 파일):

# Python
__pycache__/
*.py[cod]
*.so
.Python
env/
venv/
*.egg-info/

# Docker
.dockerignore

# IDE
.vscode/
.idea/

# 환경 변수
.env
.env.local

# 빌드 결과
dist/
build/

.dockerignore (Docker 이미지에 포함하지 않을 파일):

.git
.gitignore
.github
README.md
.env
.env.local
docker-compose.yml
*.md
tests/
.pytest_cache/
__pycache__/
*.pyc
.vscode/

GitHub Actions로 Docker 이미지 자동 빌드

프로젝트 루트에 .github/workflows/docker-build.yml 생성:

name: Docker Build and Push

on:
  push:
    branches: [ main, develop ]
    tags:
      - 'v*'
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - name: 코드 체크아웃
      uses: actions/checkout@v3
    
    - name: Docker 메타데이터 설정
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: |
          yourusername/myapp
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=semver,pattern={{version}}
          type=semver,pattern={{major}}.{{minor}}
    
    - name: Docker Hub 로그인
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
    
    - name: Docker 이미지 빌드 및 푸시
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
    
    - name: 테스트 실행
      run: |
        docker run --rm yourusername/myapp:latest pytest

GitHub Secrets 설정:

  1. GitHub 저장소 → Settings → Secrets and variables → Actions
  2. DOCKER_USERNAMEDOCKER_PASSWORD 추가

GitHub Container Registry 사용

Docker Hub 대신 GitHub의 자체 컨테이너 레지스트리를 사용할 수도 있습니다:

- name: GitHub Container Registry 로그인
  uses: docker/login-action@v2
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

- name: 이미지 빌드 및 푸시
  uses: docker/build-push-action@v4
  with:
    context: .
    push: true
    tags: ghcr.io/${{ github.repository }}:latest

Python + Docker: 실전 개발 환경 구축

Python 프로젝트에서 Docker를 활용하는 실전 예제를 살펴보겠습니다.

1. 기본 Flask API 프로젝트

프로젝트 구조:

flask-api/
├── app/
│   ├── __init__.py
│   ├── main.py
│   └── models.py
├── tests/
│   └── test_api.py
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── .env.example

app/main.py:

from flask import Flask, jsonify, request
import os
from datetime import datetime

app = Flask(__name__)

# 환경 변수에서 설정 읽기
app.config['ENV'] = os.getenv('FLASK_ENV', 'production')
app.config['DEBUG'] = os.getenv('FLASK_DEBUG', 'False') == 'True'

@app.route('/')
def home():
    return jsonify({
        'message': 'Flask API with Docker',
        'environment': app.config['ENV'],
        'timestamp': datetime.now().isoformat()
    })

@app.route('/api/data', methods=['GET', 'POST'])
def handle_data():
    if request.method == 'POST':
        data = request.get_json()
        return jsonify({
            'status': 'success',
            'received': data
        }), 201
    
    return jsonify({
        'data': [
            {'id': 1, 'name': 'Item 1'},
            {'id': 2, 'name': 'Item 2'}
        ]
    })

@app.route('/health')
def health_check():
    return jsonify({'status': 'healthy'}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

requirements.txt:

Flask==3.0.0
python-dotenv==1.0.0
gunicorn==21.2.0
pytest==7.4.3
requests==2.31.0

Dockerfile (멀티 스테이지 빌드):

# 베이스 이미지
FROM python:3.11-slim as base

# 환경 변수 설정
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# 작업 디렉토리
WORKDIR /app

# 의존성 빌드 스테이지
FROM base as builder

# 의존성 설치
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# 최종 실행 스테이지
FROM base as final

# 빌더에서 설치한 패키지 복사
COPY --from=builder /root/.local /root/.local

# PATH 업데이트
ENV PATH=/root/.local/bin:$PATH

# 애플리케이션 코드 복사
COPY ./app /app

# 포트 노출
EXPOSE 5000

# 헬스체크 추가
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import requests; requests.get('http://localhost:5000/health')"

# 운영 환경용 Gunicorn 사용
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--timeout", "60", "main:app"]

docker-compose.yml (개발 환경):

version: '3.8'

services:
  web:
    build: 
      context: .
      target: base  # 개발 시에는 base 스테이지만 사용
    command: python main.py  # 개발 모드
    volumes:
      - ./app:/app  # 코드 변경 시 자동 반영
    ports:
      - "5000:5000"
    environment:
      - FLASK_ENV=development
      - FLASK_DEBUG=True
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
    depends_on:
      - db
      - redis
    networks:
      - app-network

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - app-network

  adminer:
    image: adminer
    ports:
      - "8080:8080"
    networks:
      - app-network

volumes:
  postgres-data:
  redis-data:

networks:
  app-network:
    driver: bridge

docker-compose.prod.yml (운영 환경):

version: '3.8'

services:
  web:
    build:
      context: .
      target: final  # 최종 운영 이미지
    command: gunicorn --bind 0.0.0.0:5000 --workers 4 main:app
    restart: always
    environment:
      - FLASK_ENV=production
      - FLASK_DEBUG=False
    # 볼륨 마운트 제거 (운영 환경)

2. 실행 명령어 모음

# 개발 환경 시작
docker compose up -d

# 로그 확인 (실시간)
docker compose logs -f web

# 특정 서비스만 재시작
docker compose restart web

# 컨테이너 내부에서 명령 실행
docker compose exec web python
docker compose exec web flask shell
docker compose exec db psql -U postgres -d myapp

# 테스트 실행
docker compose exec web pytest

# 데이터베이스 마이그레이션
docker compose exec web flask db upgrade

# 운영 환경 실행
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# 모든 서비스 중지 및 삭제
docker compose down

# 볼륨까지 삭제 (주의!)
docker compose down -v

3. Python 데이터 과학 프로젝트 with Docker

Dockerfile (Jupyter + Pandas + Scikit-learn):

FROM python:3.11-slim

# 시스템 패키지 설치
RUN apt-get update && apt-get install -y \
    build-essential \
    curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /workspace

# Python 패키지 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Jupyter 설정
RUN jupyter notebook --generate-config && \
    echo "c.NotebookApp.ip = '0.0.0.0'" >> ~/.jupyter/jupyter_notebook_config.py && \
    echo "c.NotebookApp.allow_root = True" >> ~/.jupyter/jupyter_notebook_config.py && \
    echo "c.NotebookApp.token = ''" >> ~/.jupyter/jupyter_notebook_config.py

EXPOSE 8888

CMD ["jupyter", "notebook", "--no-browser", "--port=8888"]

requirements.txt:

jupyter==1.0.0
pandas==2.1.3
numpy==1.26.2
matplotlib==3.8.2
seaborn==0.13.0
scikit-learn==1.3.2
plotly==5.18.0

docker-compose.yml:

version: '3.8'

services:
  jupyter:
    build: .
    ports:
      - "8888:8888"
    volumes:
      - ./notebooks:/workspace/notebooks
      - ./data:/workspace/data
      - ./models:/workspace/models
    environment:
      - JUPYTER_ENABLE_LAB=yes
# 실행
docker compose up -d

# 브라우저에서 http://localhost:8888 접속

4. Django 프로젝트 with Docker

Dockerfile:

FROM python:3.11-slim

ENV PYTHONUNBUFFERED=1

WORKDIR /app

# 시스템 의존성
RUN apt-get update && apt-get install -y \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

# Python 의존성
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# 정적 파일 수집을 위한 디렉토리
RUN mkdir -p /app/staticfiles

# 엔트리포인트 스크립트
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]

entrypoint.sh:

#!/bin/bash

echo "Waiting for postgres..."
while ! nc -z db 5432; do
  sleep 0.1
done
echo "PostgreSQL started"

# 마이그레이션 실행
python manage.py migrate --noinput

# 정적 파일 수집
python manage.py collectstatic --noinput

exec "$@"

docker-compose.yml:

version: '3.8'

services:
  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    environment:
      - DEBUG=1
      - SECRET_KEY=dev-secret-key-change-in-production
      - DATABASE_URL=postgresql://postgres:password@db:5432/django_db
      - ALLOWED_HOSTS=localhost,127.0.0.1
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=django_db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password

  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./staticfiles:/staticfiles
    ports:
      - "80:80"
    depends_on:
      - web

volumes:
  postgres_data:

GitHub + Docker + Python 완전 통합 워크플로우

실전 시나리오: CI/CD 파이프라인 구축

1. 로컬 개발

# 저장소 클론
git clone https://github.com/username/my-python-app.git
cd my-python-app

# 개발 환경 시작
docker compose up -d

# 코드 수정 후 테스트
docker compose exec web pytest

# Git 커밋
git add .
git commit -m "Add new feature"
git push origin feature/new-feature

2. Pull Request 생성 시 자동 테스트

.github/workflows/test.yml:

name: Test

on:
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Docker Compose로 테스트 환경 구성
      run: |
        docker compose -f docker-compose.yml -f docker-compose.test.yml up -d
        docker compose exec -T web pytest --cov=app --cov-report=xml
    
    - name: 코드 커버리지 업로드
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.xml
    
    - name: 정리
      run: docker compose down -v

3. Main 브랜치 머지 시 자동 배포

.github/workflows/deploy.yml:

name: Deploy

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Docker 이미지 빌드
      run: docker build -t myapp:${{ github.sha }} .
    
    - name: Docker Hub에 푸시
      run: |
        echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
        docker tag myapp:${{ github.sha }} username/myapp:latest
        docker push username/myapp:latest
    
    - name: 서버에 배포 (SSH)
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /opt/myapp
          docker compose pull
          docker compose up -d
          docker system prune -f

개발 생산성을 높이는 Docker 팁

1. Makefile로 명령어 단순화

Makefile:

.PHONY: build up down logs shell test migrate

build:
	docker compose build

up:
	docker compose up -d

down:
	docker compose down

logs:
	docker compose logs -f

shell:
	docker compose exec web python manage.py shell

bash:
	docker compose exec web bash

test:
	docker compose exec web pytest

migrate:
	docker compose exec web python manage.py migrate

collectstatic:
	docker compose exec web python manage.py collectstatic --noinput

createsuperuser:
	docker compose exec web python manage.py createsuperuser

사용법:

make build    # 이미지 빌드
make up       # 서비스 시작
make logs     # 로그 확인
make test     # 테스트 실행
make shell    # Django shell 접속

2. 개발용 VS Code DevContainer 설정

.devcontainer/devcontainer.json:

{
  "name": "Python Development",
  "dockerComposeFile": "../docker-compose.yml",
  "service": "web",
  "workspaceFolder": "/app",
  "extensions": [
    "ms-python.python",
    "ms-python.vscode-pylance",
    "ms-azuretools.vscode-docker"
  ],
  "settings": {
    "python.defaultInterpreterPath": "/usr/local/bin/python",
    "python.linting.enabled": true,
    "python.linting.pylintEnabled": true,
    "python.formatting.provider": "black"
  }
}

VS Code에서 "Reopen in Container"를 선택하면 컨테이너 내부에서 직접 개발할 수 있습니다.

3. 핫 리로드 설정

Python 코드가 변경되면 자동으로 재시작되도록 설정:

services:
  web:
    build: .
    command: >
      sh -c "pip install watchdog &&
             watchmedo auto-restart --directory=./ --pattern=*.py --recursive -- 
             python main.py"
    volumes:
      - ./app:/app

문제 해결 가이드

문제 1: 컨테이너가 데이터베이스에 연결되지 않음

증상:

sqlalchemy.exc.OperationalError: could not connect to server

해결:

services:
  web:
    depends_on:
      db:
        condition: service_healthy  # 데이터베이스가 준비될 때까지 대기
  
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

문제 2: 코드 변경이 반영되지 않음

원인: 볼륨 마운트 문제

해결:

# 컨테이너 재시작
docker compose restart web

# 이미지 재빌드 (캐시 무시)
docker compose build --no-cache web
docker compose up -d

문제 3: 포트 충돌

증상:

Error: port is already allocated

해결:

# 다른 포트 사용
docker compose up -d --scale web=0
docker run -p 5001:5000 myapp

# 또는 docker-compose.yml에서 포트 변경
ports:
  - "5001:5000"

결론

Docker와 GitHub, Python을 함께 사용하면 다음과 같은 이점을 얻을 수 있습니다:

  1. 재현 가능한 개발 환경: 팀원 모두가 동일한 환경에서 작업
  2. 자동화된 CI/CD: 코드 푸시만으로 테스트와 배포 자동화
  3. 격리된 서비스 관리: 여러 프로젝트를 동시에 실행해도 충돌 없음
  4. 빠른 온보딩: 신규 개발자가 docker compose up 한 번으로 시작 가능
  5. 운영 환경과의 일관성: 개발 환경이 곧 운영 환경

이제 Docker는 선택이 아닌 필수입니다. 위의 예제들을 기반으로 여러분의 프로젝트에 맞게 커스터마이징하여 사용해보시기 바랍니다.

#가나 투데이 #ganatoday

그린아프로