Membawa Symfony ke production dengan Docker: Dockerfile multi-stage, docker-compose untuk stack app + db + redis, menjalankan migrasi dan cache warmup saat deploy, serta pipeline GitHub Actions untuk testing, build image, dan deploy staging ke production.

Setelah worker mode di episode 22, episode 23 menjawab pertanyaan operasional terbesar: bagaimana aplikasi ini sampai ke server dengan konsisten? Jawabannya: container image + pipeline. Image Docker menjamin lingkungan identik dari laptop sampai production; CI/CD mengotomasi pengujian dan deployment.
Mengapa ini penting? Karena "jalan di laptop saya" adalah lawan utama keandalan. Dengan image yang di-build sekali dan di-deploy ke mana pun, kalian menghilangkan seluruh kelas masalah konfigurasi — dan migrasi & cache warmup yang benar saat deploy mencegah downtime dan error post-deploy.
Satu file image dengan beberapa stage: composer (dependensi), build (kode + optimasi), dan runtime (hasil akhir yang ramping):
FROM composer:2 AS composer
FROM php:8.4-cli AS build
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN apt-get update && apt-get install -y unzip git libpq-dev \
&& docker-php-ext-install pdo_pgsql intl opcache
WORKDIR /app
COPY composer.json composer.lock symfony.lock ./
RUN composer install --no-dev --optimize-autoloader --prefer-dist \
--no-interaction --no-progress
COPY . .
RUN php bin/console cache:clear --env=prod --no-debug \
&& php bin/console cache:warmup --env=prod --no-debug
FROM dunglas/frankenphp:8.4 AS runtime
COPY --from=build /app /app
WORKDIR /app
ENV APP_ENV=prod APP_DEBUG=0
EXPOSE 80Mengapa multi-stage? Stage composer & build menyimpan toolchain pengembangan; stage runtime hanya berisi hasil jadi — image lebih kecil, permukaan serangan lebih sempit. Perhatikan cache warmup di stage build: container runtime langsung berjalan dengan cache container yang hangat.
docker-compose.yml merangkai aplikasi + database + Redis:
services:
app:
build: .
ports:
- "8080:80"
env_file: .env.prod
depends_on:
- db
- redis
db:
image: postgres:17
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
db_data:Env var secret diambil dari .env.prod (tidak di-commit) — seperti pola episode 16. Redis digunakan untuk cache/session/rate limiter shared (episode 14/20/24).
Migrasi tidak boleh dieksekusi sembarangan. Pola yang benar saat deploy:
docker compose run --rm app php bin/console doctrine:migrations:migrate \
--env=prod --no-interaction --allow-no-migrationAturan penting:
--no-interaction — dalam pipeline, tidak boleh menunggu input manual.Setelah deploy, container baru harus hangat:
docker compose run --rm app php bin/console cache:clear --env=prod --no-debug
docker compose run --rm app php bin/console cache:warmup --env=prod --no-debugBiasakan memanggil warmup, bukan hanya clear — warmup membangun semua cache (routes, container, twig) di muka sehingga request pertama tidak menderita cold start. Pada image yang sudah di-warmup di stage build (Dockerfile di atas), langkah ini opsional — tetapi tetap berguna untuk memvalidasi container sebelum traffic masuk.
CI/CD lengkap punya tiga tahap: test → build → deploy.
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: test
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: composer
- run: composer install --no-interaction
- run: php bin/phpunit
- run: composer audit
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build & push image
uses: docker/build-push-action@v5
with:
push: true
tags: registry.example.com/myapp:${{ github.sha }}
secrets: |
"APP_SECRET=${{ secrets.APP_SECRET }}"
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy ke staging→production
run: |
# contoh: SSH ke server, tarik image, jalankan migrate + up
ssh deploy@server "cd /srv/myapp && docker compose pull \
&& docker compose run --rm app php bin/console doctrine:migrations:migrate \
--env=prod --no-interaction \
&& docker compose up -d"Struktur yang perlu diperhatikan:
| Tahap | Tanggung jawab |
|---|---|
test | PHPUnit + composer audit (episode 17/18) |
build | Build image + push ke registry (tag = SHA commit) |
deploy | Tarik image baru, migrate, restart container |
Secrets (episode 16) tersimpan di GitHub Secrets — tidak pernah di file workflow. composer audit di pipeline menjamin dependensi rentan tertangkap sebelum sampai production.
| Strategi | Downtime | Kompleksitas | Cocok untuk |
|---|---|---|---|
| Recreate (up -d) | Beberapa detik | Rendah | Aplikasi kecil |
| Rolling update | Minim | Sedang | Orkestrasi (Swarm/K8s) |
| Blue/Green | Nol | Tinggi | Enterprise, SLA ketat |
Apapun strateginya, urutannya konsisten: test → build → migrate → warmup → switch traffic.
Warning
Jangan pernah meletakkan migrasi "di dalam" startup container (CMD/entrypoint). Beberapa instance bisa mengeksekusi migrasi bersamaan → race condition & skema korup. Jalankan migrasi sebagai langkah deploy eksplisit (seperti contoh di atas) atau gunakan lock berbasis DB.
--secret/CI secrets, bukan ENV di Dockerfile.Pada episode 23 ini, kalian telah mempackaging dan mendeploy aplikasi.
Inti yang harus dibawa pulang:
.env.prod.--no-interaction, sebelum traffic baru.composer audit → build image → migrate → deploy.Di episode 24 selanjutnya kita menskalakan: Scaling & High Availability — stateless app, Redis sessions & cache, database read replicas, load balancing horizontal, dan load testing untuk tuning. Sampai jumpa di episode 24!