Belajar Flask - Docker & CI/CD
Episode 23 of 27

Belajar Flask - Docker & CI/CD

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.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

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.

Kenapa Docker

  • Reproducible: image berisi Python, dependensi, dan aplikasi — hasilnya sama di laptop, server, maupun CI.
  • Isolasi: aplikasi terpisah dari host (dependensi, library sistem, versi).
  • Immutable: setiap deploy memakai image baru — tidak ada "update manual yang lupa".

Multi-Stage Dockerfile

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:

Dockerfile
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.

Healthcheck: Biarkan Infrastruktur Memeriksa

Docker (dan orchestrator) perlu tahu kapan aplikasi "sehat". Tambahkan endpoint dan healthcheck:

PythonEndpoint healthz
@main_bp.get("/healthz")
def healthz() -> tuple[dict, int]:
    db.session.execute(db.text("SELECT 1"))
    return api_ok({"status": "ok"})
Dockerfile - tambah healthcheck
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 1

Healthcheck 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.

Docker Compose: Merangkai Layanan

docker-compose.yml
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 dengan GitHub Actions

CI/CD (Continuous Integration / Continuous Deployment): setiap push ke main, pipeline otomatis menjalankan test, build image, lalu deploy. Workflow sederhana:

.github/workflows/ci.yml
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 }}:latest

Alur 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.

Deploy Otomatis dari Image

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.

Common Pitfalls Docker & CI/CD

  • Image non-deterministik: pip install tanpa lockfile — freeze requirements (episode 0) atau gunakan uv.lock/poetry.lock.
  • Healthcheck hanya cek proses: tanpa cek database, DB mati tidak terdeteksi.
  • Secret di image: ENV SECRET_KEY=... di Dockerfile ikut dibakar ke image — selalu dari environment di compose/deploy.
  • Migrations tidak jalan sebelum deploy: skema dan kode tidak sinkron.

Penutup

Pada episode 23 ini, kalian telah mengotomatiskan seluruh alur dari kode ke produksi.

Inti yang harus dibawa pulang:

  • Multi-stage Dockerfile: builder untuk dependensi, runtime ramping untuk produksi.
  • Healthcheck harus memeriksa yang penting (DB), bukan sekadar proses hidup.
  • docker-compose merangkai app + db dengan depends_on health dan secret dari env.
  • Jalankan 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!

Belajar Flask - Docker & CI/CD | Belajar Flask