# Symfony dan Docker di 2026: Lingkungan Pengembangan dan Deployment Produksi > Alur kerja Symfony Docker lengkap untuk 2026: lingkungan pengembangan FrankenPHP, image produksi multi-stage, mode worker, secret terenkripsi, optimasi image, dan deploy tanpa downtime dengan migrasi. - Published: 2026-06-25 - Updated: 2026-07-06 - Author: SharpSkill - Tags: symfony, docker, frankenphp, php, deployment, devops - Reading time: 10 min --- Tutorial Symfony Docker ini membangun alur kerja kontainer yang lengkap: sebuah lingkungan pengembangan lokal yang dapat direproduksi dan sebuah image produksi yang diperkuat, keduanya berpusat pada FrankenPHP dan Symfony 7.4 LTS. Menjalankan Symfony di dalam kontainer menghilangkan celah environment-drift klasik antara laptop dan server, sekaligus mengubah setiap deployment menjadi pengiriman satu artefak yang tidak dapat diubah, bukan menjalankan rangkaian perintah server manual yang rapuh. > **Stack Symfony Docker 2026** > > Template resmi Symfony Docker kini menyertakan [FrankenPHP](https://frankenphp.dev/) sebagai runtime-nya, menggantikan pasangan tradisional Nginx dan PHP-FPM. Satu binary melayani HTTP/2 dan HTTP/3, serta menjalankan Symfony dalam mode worker persisten dengan mempertahankan kernel tetap ter-boot di antara request alih-alih membangunnya ulang pada setiap panggilan. ## Lingkungan Pengembangan Docker Compose untuk Symfony Setup pengembangan yang baik memberikan setiap kontributor versi PHP yang sama, ekstensi yang sama, dan database yang sama tanpa menyentuh mesin host. Docker Compose mendeskripsikan stack tersebut secara deklaratif. Contoh di bawah memadukan kontainer FrankenPHP yang dibangun dari `Dockerfile` lokal dengan layanan PostgreSQL 17, yang saling terhubung pada jaringan Compose bawaan. ```yaml # compose.yaml services: php: build: context: . target: frankenphp_dev ports: - "80:80" - "443:443" volumes: - ./:/app # bind mount: edits on the host appear instantly - caddy_data:/data # persist TLS certificates across restarts environment: DATABASE_URL: "postgresql://app:app@database:5432/app?serverVersion=17" depends_on: - database database: image: postgres:17-alpine environment: POSTGRES_DB: app POSTGRES_USER: app POSTGRES_PASSWORD: app volumes: - db_data:/var/lib/postgresql/data volumes: caddy_data: db_data: ``` Bind mount pada layanan `php` memetakan root proyek ke dalam kontainer, sehingga perubahan file berlaku tanpa perlu membangun ulang image. FrankenPHP menghasilkan sertifikat TLS lokal saat boot pertama, itulah alasan port 443 diekspos dan `caddy_data` dijadikan named volume: sertifikat tersebut tetap bertahan setelah `docker compose down`. Petunjuk `serverVersion=17` pada DSN membuat Doctrine dapat melewati query deteksi versi pada setiap koneksi. Setelah stack berjalan, setiap perintah Symfony dieksekusi di dalam kontainer `php`, bukan di host, sehingga versi dan ekstensi PHP yang benar terjamin. Membuat database, menjalankan migrasi, atau membuka shell semuanya mengikuti pola yang sama melalui `docker compose exec`. > **Menjalankan Perintah Console di dalam Kontainer** > > Awali panggilan Symfony dan Composer dengan `docker compose exec php` agar dieksekusi terhadap runtime PHP kontainer, misalnya `docker compose exec php bin/console make:entity` atau `docker compose exec php composer require symfony/uid`. Host tidak pernah perlu memasang PHP. ## Dockerfile Multi-Stage untuk Produksi Symfony Sebuah [Dockerfile multi-stage](https://docs.docker.com/build/building/multi-stage/) berbagi base yang sama lalu memisah menjadi target pengembangan dan target produksi. Stage produksi hanya memasang dependensi runtime, membuang paket dev Composer, dan menghangatkan cache saat build sehingga kontainer yang berjalan langsung siap seketika. ```dockerfile # Dockerfile FROM dunglas/frankenphp:1-php8.4 AS base WORKDIR /app # Install the PHP extensions Symfony relies on RUN install-php-extensions \ intl \ opcache \ pdo_pgsql \ zip COPY --from=composer/composer:2-bin /composer /usr/bin/composer # --- Development target --- FROM base AS frankenphp_dev ENV APP_ENV=dev RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" # --- Production target --- FROM base AS frankenphp_prod ENV APP_ENV=prod ENV FRANKENPHP_CONFIG="worker ./public/index.php" ENV APP_RUNTIME="Runtime\FrankenPhpSymfony\Runtime" RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" # Layer caching: copy manifests first, install, then copy the source COPY composer.json composer.lock symfony.lock ./ RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist COPY . . RUN composer dump-autoload --no-dev --classmap-authoritative \ && composer dump-env prod \ && bin/console cache:warmup ``` Menyalin `composer.json` dan file lock sebelum sisa kode sumber adalah optimasi kuncinya: Docker meng-cache layer instalasi dependensi dan hanya menjalankannya ulang ketika manifest berubah, bukan pada setiap perubahan kode. Flag `--classmap-authoritative` memberi tahu Composer bahwa classmap sudah lengkap, sehingga autoloader tidak pernah jatuh ke pencarian filesystem saat runtime. Menghangatkan cache selama build berarti request produksi pertama akan mengenai kontainer yang telah terkompilasi penuh. ## Mode Worker FrankenPHP dan Symfony Runtime PHP-FPM tradisional mem-boot kernel Symfony, menangani satu request, lalu membuang semuanya. Mode worker mempertahankan kernel dan container dependency-injection tetap hidup di antara request, dan di situlah sebagian besar penghematan latensi berasal. Paket `runtime/frankenphp-symfony` membuat mekanisme ini bekerja melalui komponen Symfony Runtime dan bootstrap `public/index.php` standar. ```php // public/index.php use App\Kernel; require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; return function (array $context) { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); }; ``` Baris `FRANKENPHP_CONFIG="worker ./public/index.php"` yang diatur di dalam Dockerfile mengaktifkan loop ini. Karena kontainer digunakan kembali, layanan apa pun yang menyimpan state khusus request harus mereset dirinya di antara request. Symfony menangani layanan framework secara otomatis, tetapi layanan stateful buatan sendiri sebaiknya mengimplementasikan `Symfony\Contracts\Service\ResetInterface` dan diberi tag `kernel.reset` agar state yang terakumulasi tidak bocor ke request berikutnya. Ini adalah disiplin yang sama seperti yang dituntut oleh [Messenger worker](/blog/symfony/symfony-messenger-queues-workers-async-architecture) yang berjalan lama, dan imbalannya adalah throughput request beberapa kali lebih tinggi daripada PHP-FPM pada perangkat keras yang sama. Untuk pengembangan lokal, menjalankan FrankenPHP dengan flag `--watch` akan me-restart worker secara otomatis ketika file berubah, sehingga mode worker tidak mengganggu loop edit-refresh. ## FrankenPHP Versus PHP-FPM pada Symfony Pilihan antara FrankenPHP dan stack klasik Nginx plus PHP-FPM membentuk baik Dockerfile maupun perilaku runtime. Tabel di bawah merangkum perbedaan praktisnya untuk setup produksi Symfony. | Aspek | Mode worker FrankenPHP | Nginx + PHP-FPM | |--------|------------------------|-----------------| | Kontainer | Satu | Dua (web server + PHP) | | Boot kernel | Sekali per worker | Sekali per request | | HTTP/3 | Bawaan | Perlu konfigurasi tambahan | | TLS | Otomatis (Caddy) | Setup sertifikat manual | | Penanganan state | Harus reset di antara request | Terisolasi per request | | Biaya cold-start | Dibayar sekali saat boot | Dibayar pada setiap request | Mode worker unggul dalam throughput karena mengamortisasi langkah boot kernel dan kompilasi container yang mahal ke seluruh ribuan request. PHP-FPM tetap relevan ketika sebuah aplikasi bergantung pada global atau library pihak ketiga yang mengasumsikan proses baru per request, karena hal-hal tersebut akan rusak di bawah worker yang digunakan kembali. > **Kebocoran State pada Mode Worker** > > Sebuah layanan yang meng-cache data request di dalam properti privat akan menyajikan data satu request ke request berikutnya kecuali ia mereset diri. Audit singleton, event subscriber, dan apa pun yang menyimpan token keamanan atau request saat ini sebelum beralih ke mode worker di produksi. ## Mengelola Environment Variable dan Secret di Produksi Symfony membaca konfigurasi dari environment variable, dan ada dua cara yang solid untuk menyuplainya di produksi. Cara pertama adalah menyuntikkan variabel biasa melalui orchestrator atau file Compose. Cara kedua adalah vault secret terenkripsi milik Symfony, yang menyimpan nilai sensitif di dalam repository sebagai ciphertext dan mendekripsinya saat runtime dengan satu kunci privat. ```bash # Generate the keypair; commit the public key, keep the private key out of the image php bin/console secrets:generate-keys # Encrypt a value into config/secrets/prod/ php bin/console secrets:set DATABASE_URL # In production, expose only the decryption key to the container export SYMFONY_DECRYPTION_SECRET="$(cat config/secrets/prod/prod.decrypt.private.php)" ``` Langkah `composer dump-env prod` di dalam Dockerfile mengkompilasi file `.env` menjadi satu `.env.local.php` yang teroptimasi, yang menghilangkan biaya runtime untuk mem-parsing file dotenv saat boot. Variabel apa pun yang diatur di environment kontainer sebenarnya tetap menimpa nilai default terkompilasi, sehingga secret yang disediakan orchestrator selalu menang. [Panduan deployment Symfony](https://symfony.com/doc/current/deployment.html) mendokumentasikan urutan presedensi lengkapnya, tetapi aturan praktisnya sederhana: jangan pernah memanggang `APP_SECRET` atau kredensial database ke dalam image, dan suntikkan keduanya saat kontainer dimulai. ## Mengoptimalkan Image Produksi Symfony Dua pengaturan OPcache menyumbang sebagian besar kesenjangan performa produksi: menonaktifkan validasi timestamp dan mengaktifkan preloading. Karena image kontainer bersifat tidak dapat diubah, kode sumber tidak pernah berubah saat runtime, sehingga OPcache tidak seharusnya pernah men-stat file untuk memeriksa perubahan. Preloading melangkah lebih jauh dengan memuat kelas Symfony dan aplikasi ke dalam shared memory sekali saat server dimulai. ```ini ; docker/php/prod.ini opcache.enable=1 opcache.preload=/app/var/cache/prod/App_KernelProdContainer.preload.php opcache.preload_user=root opcache.memory_consumption=256 opcache.max_accelerated_files=20000 opcache.validate_timestamps=0 ; immutable image: never re-check the filesystem realpath_cache_size=4096K realpath_cache_ttl=600 ``` Symfony menghasilkan file `preload.php` yang tercantum di atas selama `cache:warmup`, sehingga file tersebut sudah ada di dalam image setelah build. [Preloading OPcache](https://www.php.net/manual/en/opcache.preloading.php) menautkan kelas-kelas ini saat startup dan melewati langkah kompilasi pada request pertama yang membutuhkannya. Menetapkan `validate_timestamps=0` hanya aman untuk deployment yang tidak dapat diubah di mana rilis baru berarti image baru; pada host yang dapat diubah hal ini akan menyajikan kode usang. Menetapkan `max_accelerated_files` di atas jumlah file sebenarnya menjaga seluruh framework tetap ter-cache tanpa eviction. ## Mengurangi Ukuran Image Symfony dengan .dockerignore File `.dockerignore` yang hilang secara diam-diam menggembungkan build. Tanpanya, langkah `COPY . .` akan mengirim direktori `vendor` lokal, `var/cache` dari pengembangan, output build node, dan riwayat `.git` langsung ke dalam image dan konteks build. Mengecualikan semuanya akan mengecilkan image, mempercepat unggahan konteks build ke daemon, dan mencegah artefak pengembangan menimpa dependensi produksi yang baru saja terpasang. ```gitignore # .dockerignore /.git/ /vendor/ /node_modules/ /var/ /.env.local /.env.*.local /tests/ /docker/ compose*.yaml Dockerfile ``` Mengabaikan `/vendor/` adalah yang paling penting: stage produksi menjalankan `composer install --no-dev`, sehingga mengirim direktori vendor host yang bercita rasa pengembangan akan menggagalkan langkah tersebut sepenuhnya. Mengecualikan `/var/` menjaga cache dan log pengembangan tetap keluar dari image, membiarkan `cache:warmup` saat build menghasilkan cache produksi yang bersih. Dikombinasikan dengan build multi-stage dan ekstensi berbasis Alpine, image Symfony yang ramping biasanya berukuran jauh di bawah 150 MB. ## Men-deploy Kontainer Symfony dan Menjalankan Migrasi File Compose produksi mereferensikan image yang telah dibangun sebelumnya dari registry alih-alih membangunnya secara lokal, dan ia mendefinisikan health check agar orchestrator hanya mengarahkan trafik ke kontainer yang merespons. Secret tiba sebagai environment variable, tidak pernah sebagai layer image. ```yaml # compose.prod.yaml services: php: image: registry.example.com/app:${TAG} environment: APP_ENV: prod APP_SECRET: ${APP_SECRET} DATABASE_URL: ${DATABASE_URL} healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost/health"] interval: 10s timeout: 3s retries: 3 restart: unless-stopped ``` Health check tersebut men-curl route `/health`, jadi aplikasi harus mengeksposnya. Sebuah controller minimal yang mengembalikan status 200 tanpa menyentuh database menjaga check tetap cepat dan menghindari penandaan kontainer sebagai tidak sehat selama gangguan database sesaat. Ketika kesiapan database penting, probe kedua yang lebih dalam dapat menjalankan query ringan, tetapi endpoint liveness tetap sederhana. ```php // src/Controller/HealthController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; final class HealthController extends AbstractController { #[Route('/health', name: 'health', methods: ['GET'])] public function __invoke(): JsonResponse { return new JsonResponse(['status' => 'ok']); } } ``` Migrasi database sebaiknya dijalankan sebagai langkah terpisah sebelum kontainer baru menerima trafik, bukan di dalam boot aplikasi. Menjalankannya di dalam kontainer sekali pakai menjamin skema selalu terkini tepat satu kali per rilis, bahkan ketika beberapa replika aplikasi dimulai secara paralel. ```bash # deploy.sh set -euo pipefail docker compose -f compose.prod.yaml pull # Apply migrations once, in an isolated container docker compose -f compose.prod.yaml run --rm php \ bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration # Start replicas and wait for the health check to pass docker compose -f compose.prod.yaml up -d --wait ``` Flag `--wait` memblokir hingga setiap layanan melaporkan sehat, memberi skrip deploy sinyal keberhasilan yang nyata alih-alih start fire-and-forget. Memadukan ini dengan rolling update pada orchestrator seperti Kubernetes atau Docker Swarm menghasilkan rilis tanpa downtime: kontainer lama terus melayani hingga yang baru lolos health check-nya. Untuk gambaran lebih luas tentang versi framework yang menjadi target alur kerja ini, [ikhtisar platform Symfony](/technologies/symfony) dan [panduan fitur Symfony 8 dan PHP 8.4](/blog/symfony/symfony-8-new-features-php-84-lazy-objects) membahas apa yang berubah pada lini rilis saat ini. ## Kesimpulan - Gunakan Dockerfile multi-stage agar image pengembangan dan produksi berbagi satu base sementara target produksi dikirim tanpa dependensi dev Composer - Salin `composer.json` dan file lock sebelum kode sumber untuk menjaga layer instalasi dependensi tetap ter-cache lintas perubahan kode - Jalankan FrankenPHP dalam mode worker untuk menggunakan kembali kernel yang telah ter-boot lintas request, dan reset layanan stateful dengan `ResetInterface` untuk mencegah kebocoran state - Hangatkan cache Symfony dan kompilasi `.env` dengan `dump-env prod` saat build sehingga kontainer yang berjalan langsung terinisialisasi penuh - Aktifkan preloading OPcache dan tetapkan `validate_timestamps=0` di produksi, yang aman justru karena image bersifat tidak dapat diubah - Jauhkan `APP_SECRET`, kredensial database, dan kunci dekripsi vault dari layer image; suntikkan semuanya sebagai environment variable saat kontainer dimulai - Terapkan migrasi Doctrine di dalam kontainer sekali pakai sebelum replika baru menerima trafik, dan gerbangi rollout pada health check dengan `up -d --wait` --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/symfony/symfony-docker-development-production