Mengemas aplikasi Flask ke Docker dengan multi-stage image dan healthcheck, merangkai layanan lewat docker-compose, lalu membangun pipeline CI/CD GitHub Actions yang menjalankan test dan mendeploy secara otomatis.

Mendeploy manual (episode 22) rentan terhadap "bekerja di mesin saya": environment berbeda di tiap server, dependensi melenceng, proses tidak konsisten. Docker mengemas aplikasi beserta seluruh environment-nya menjadi image yang sama di mana pun dijalankan. CI/CD kemudian mengotomatiskan build, test, dan deploy setiap kali kode berubah. Episode 23 menggabungkan keduanya: image Flask yang efisien (multi-stage), orchestration dengan docker-compose, healthcheck, dan pipeline GitHub Actions.
Multi-stage build memisahkan tahap build (dengan compiler/tool lengkap) dari tahap runtime (kecil dan ramping). Hasilnya image produksi yang jauh lebih kecil dan aman:
FROM python:3.13-slim AS builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.13-slim AS runtime
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "app:create_app()"]Tahap builder membuat virtual environment dan menginstall dependensi; tahap runtime hanya menyalin venv yang sudah jadi dan kode aplikasi. Image runtime tidak membawa pip, compiler, atau cache — kecil, cepat, dan permukaan serangannya lebih sempit.
Docker (dan orchestrator) perlu tahu kapan aplikasi "sehat". Tambahkan endpoint dan healthcheck:
@main_bp.get("/healthz")
def healthz() -> tuple[dict, int]:
db.session.execute(db.text("SELECT 1"))
return api_ok({"status": "ok"})HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz')" || exit 1Healthcheck memeriksa endpoint tiap 30 detik; tiga kegagalan beruntun menandai container unhealthy. Healthcheck yang baik memeriksa database (bukan sekadar proses hidup) — jadi SELECT 1 di atas penting: aplikasi yang "hidup" tapi tidak bisa akses DB tetap dianggap tidak sehat.
services:
app:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://flask:flask@db:5432/flask
SECRET_KEY: ${SECRET_KEY}
FLASK_ENV: production
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz')"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
db:
image: postgres:16
environment:
POSTGRES_USER: flask
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: flask
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U flask"]
interval: 5s
timeout: 5s
retries: 10
volumes:
pgdata:depends_on.condition: service_healthy membuat aplikasi menunggu database benar-benar siap (bukan sekadar container jalan) sebelum start. SECRET_KEY dan DB_PASSWORD diambil dari environment (${...}) — jangan di-commit. Jalankan dengan docker compose up -d --build kemudian docker compose ps.
CI/CD (Continuous Integration / Continuous Deployment): setiap push ke main, pipeline otomatis menjalankan test, build image, lalu deploy. Workflow sederhana:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install -r requirements.txt -r requirements-dev.txt
- run: pytest --cov=app
build-and-push:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:latestAlur pipeline: (1) test — checkout kode, install dependensi, jalankan pytest. Jika test gagal, pipeline berhenti — kode buruk tidak pernah lanjut. (2) build-and-push — hanya di main, build image dan push ke container registry (ghcr.io). Inilah aturan emas CI: test adalah gate — kode yang gagal test tidak akan pernah sampai ke produksi.
Setelah image di registry, deploy ke server tinggal docker compose pull + docker compose up -d. Untuk deploy yang lebih rapi, tambahkan langkah di pipeline: SSH ke server dan jalankan dua perintah di atas — atau pasang agent deploy (Watchtower untuk auto-update sederhana). Prinsipnya sama: image teruji adalah satu-satunya artefak yang dijalankan.
Note
Sekaligus jalankan migrations sebelum aplikasi baru melayani trafik: di pipeline deploy, tambahkan step docker compose run --rm app flask --app run db upgrade (episode 11) SEBELUM docker compose up -d. Ini mencegah error "no such column" saat kode baru memakai skema baru.
pip install tanpa lockfile — freeze requirements (episode 0) atau gunakan uv.lock/poetry.lock.ENV SECRET_KEY=... di Dockerfile ikut dibakar ke image — selalu dari environment di compose/deploy.Pada episode 23 ini, kalian telah mengotomatiskan seluruh alur dari kode ke produksi.
Inti yang harus dibawa pulang:
depends_on health dan secret dari env.db upgrade sebelum aplikasi baru menerima trafik.Di episode 24 selanjutnya, kita mengukur dan mengecilkan bottleneck: scaling & performance — tipe worker Gunicorn (sync/gevent), connection pooling, caching, load testing, dan optimasi berbasis data. Sampai jumpa di episode 24!