# Symfony กับ Docker ในปี 2026: สภาพแวดล้อมพัฒนาและการดีพลอยขึ้น Production > เวิร์กโฟลว์ Symfony Docker ที่สมบูรณ์สำหรับปี 2026: สภาพแวดล้อมพัฒนาด้วย FrankenPHP, อิมเมจ production แบบหลายสเตจ, โหมด worker, secret ที่เข้ารหัส, การปรับขนาดอิมเมจ และการดีพลอยแบบไม่มีดาวน์ไทม์พร้อมไมเกรชัน - Published: 2026-06-25 - Updated: 2026-07-06 - Author: SharpSkill - Tags: symfony, docker, frankenphp, php, deployment, devops - Reading time: 10 min --- บทความ Symfony Docker ฉบับนี้จะพาสร้างเวิร์กโฟลว์คอนเทนเนอร์ที่สมบูรณ์ ทั้งสภาพแวดล้อมสำหรับพัฒนาในเครื่องที่ทำซ้ำได้และอิมเมจสำหรับ production ที่ผ่านการเสริมความแข็งแกร่ง โดยยึด FrankenPHP และ Symfony 7.4 LTS เป็นแกนกลาง การรัน Symfony ภายในคอนเทนเนอร์ช่วยกำจัดช่องว่างของความไม่ตรงกันระหว่างสภาพแวดล้อมบนเครื่องนักพัฒนากับเซิร์ฟเวอร์ และเปลี่ยนการดีพลอยแต่ละครั้งให้กลายเป็นการส่งมอบอาร์ติแฟกต์ที่ไม่เปลี่ยนแปลงเพียงชิ้นเดียว แทนที่จะต้องรันคำสั่งบนเซิร์ฟเวอร์แบบแมนวลเป็นลำดับที่เปราะบาง > **สแตก Symfony Docker ประจำปี 2026** > > เทมเพลต Symfony Docker อย่างเป็นทางการปัจจุบันมาพร้อม [FrankenPHP](https://frankenphp.dev/) ในฐานะรันไทม์ เข้ามาแทนที่คู่ Nginx กับ PHP-FPM แบบดั้งเดิม ไบนารีเพียงตัวเดียวให้บริการทั้ง HTTP/2 และ HTTP/3 พร้อมทั้งรัน Symfony ในโหมด worker แบบต่อเนื่อง โดยคงเคอร์เนลที่บูตไว้แล้วระหว่างคำขอ แทนที่จะสร้างขึ้นใหม่ในทุกครั้งที่มีการเรียก ## สภาพแวดล้อมพัฒนา Symfony ด้วย Docker Compose การตั้งค่าสภาพแวดล้อมพัฒนาที่ดีจะมอบเวอร์ชัน PHP เดียวกัน เอกซ์เทนชันชุดเดียวกัน และฐานข้อมูลเดียวกันให้ผู้ร่วมพัฒนาทุกคน โดยไม่ต้องแตะต้องเครื่องโฮสต์ Docker Compose อธิบายสแตกนั้นในเชิงประกาศ (declarative) ตัวอย่างด้านล่างจับคู่คอนเทนเนอร์ FrankenPHP ที่สร้างจาก `Dockerfile` ในเครื่อง เข้ากับเซอร์วิส PostgreSQL 17 โดยเชื่อมโยงกันบนเครือข่าย Compose เริ่มต้น ```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 บนเซอร์วิส `php` แมปรากของโปรเจกต์เข้าไปในคอนเทนเนอร์ ทำให้การเปลี่ยนแปลงไฟล์มีผลทันทีโดยไม่ต้องสร้างอิมเมจใหม่ FrankenPHP สร้างใบรับรอง TLS ในเครื่องขึ้นเมื่อบูตครั้งแรก จึงเป็นเหตุผลที่ต้องเปิดพอร์ต 443 และให้ `caddy_data` เป็น named volume เพื่อให้ใบรับรองคงอยู่แม้จะรัน `docker compose down` ส่วนคำใบ้ `serverVersion=17` ใน DSN ช่วยให้ Doctrine ข้ามการคิวรีตรวจเวอร์ชันในทุกการเชื่อมต่อ เมื่อสแตกทำงานแล้ว ทุกคำสั่ง Symfony จะรันภายในคอนเทนเนอร์ `php` แทนที่จะรันบนโฮสต์ ซึ่งรับประกันเวอร์ชัน PHP และเอกซ์เทนชันที่ถูกต้อง การสร้างฐานข้อมูล การรันไมเกรชัน หรือการเปิดเชลล์ ล้วนทำตามรูปแบบเดียวกันผ่าน `docker compose exec` > **การรันคำสั่ง Console ภายในคอนเทนเนอร์** > > เติม `docker compose exec php` นำหน้าการเรียก Symfony และ Composer เพื่อรันคำสั่งเหล่านั้นบนรันไทม์ PHP ของคอนเทนเนอร์ เช่น `docker compose exec php bin/console make:entity` หรือ `docker compose exec php composer require symfony/uid` โฮสต์ไม่จำเป็นต้องติดตั้ง PHP เลย ## Dockerfile แบบหลายสเตจสำหรับ Symfony บน Production [Dockerfile แบบหลายสเตจ](https://docs.docker.com/build/building/multi-stage/) ใช้ฐานร่วมกันแล้วแยกออกเป็นทาร์เก็ตสำหรับพัฒนาและทาร์เก็ตสำหรับ production สเตจ production จะติดตั้งเฉพาะดีเพนเดนซีที่จำเป็นตอนรันไทม์ ตัดแพ็กเกจ dev ของ Composer ออก และวอร์มแคชตอนบิลด์ เพื่อให้คอนเทนเนอร์ที่กำลังรันเริ่มทำงานได้ทันที ```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 ``` การคัดลอก `composer.json` และไฟล์ล็อกก่อนซอร์สโค้ดส่วนที่เหลือคือการปรับให้เหมาะสมที่สำคัญที่สุด Docker จะแคชเลเยอร์การติดตั้งดีเพนเดนซีไว้และรันใหม่เฉพาะเมื่อ manifest เปลี่ยน ไม่ใช่ทุกครั้งที่แก้โค้ด แฟล็ก `--classmap-authoritative` บอก Composer ว่า classmap สมบูรณ์แล้ว ออโตโหลดเดอร์จึงไม่ต้องย้อนกลับไปค้นหาในระบบไฟล์ตอนรันไทม์ การวอร์มแคชระหว่างบิลด์หมายความว่าคำขอ production ครั้งแรกจะได้พบกับคอนเทนเนอร์ที่คอมไพล์เสร็จสมบูรณ์แล้ว ## โหมด Worker ของ FrankenPHP กับ Symfony Runtime PHP-FPM แบบดั้งเดิมจะบูตเคอร์เนล Symfony จัดการคำขอหนึ่งครั้ง แล้วทิ้งทุกอย่างไป ส่วนโหมด worker จะคงเคอร์เนลและคอนเทนเนอร์ dependency-injection ให้มีชีวิตอยู่ข้ามคำขอ ซึ่งเป็นที่มาของการประหยัดเวลาแฝง (latency) ส่วนใหญ่ แพ็กเกจ `runtime/frankenphp-symfony` ทำให้กลไกนี้ทำงานได้ผ่านคอมโพเนนต์ Symfony Runtime และบูตสแตรป `public/index.php` มาตรฐาน ```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']); }; ``` บรรทัด `FRANKENPHP_CONFIG="worker ./public/index.php"` ที่กำหนดไว้ใน Dockerfile จะเปิดใช้งานลูปนี้ เนื่องจากคอนเทนเนอร์ถูกนำกลับมาใช้ซ้ำ เซอร์วิสใดก็ตามที่ถือสถานะเฉพาะของคำขอจึงต้องรีเซ็ตตัวเองระหว่างคำขอ Symfony จัดการเซอร์วิสของเฟรมเวิร์กให้โดยอัตโนมัติ แต่เซอร์วิสที่มีสถานะซึ่งเขียนขึ้นเองควรอิมพลีเมนต์ `Symfony\Contracts\Service\ResetInterface` และติดแท็ก `kernel.reset` เพื่อไม่ให้สถานะที่สะสมไว้รั่วไหลไปยังคำขอถัดไป นี่คือวินัยเดียวกันกับที่ [Messenger worker](/blog/symfony/symfony-messenger-queues-workers-async-architecture) ซึ่งทำงานยาวนานต้องการ และผลตอบแทนคือปริมาณคำขอที่รองรับได้ (throughput) สูงกว่า PHP-FPM หลายเท่าบนฮาร์ดแวร์ตัวเดียวกัน สำหรับการพัฒนาในเครื่อง การรัน FrankenPHP ด้วยแฟล็ก `--watch` จะรีสตาร์ท worker โดยอัตโนมัติเมื่อไฟล์เปลี่ยนแปลง ทำให้โหมด worker ไม่ขัดขวางลูปแก้แล้วรีเฟรช ## FrankenPHP เทียบกับ PHP-FPM บน Symfony การเลือกระหว่าง FrankenPHP กับสแตก Nginx และ PHP-FPM แบบคลาสสิกส่งผลต่อทั้ง Dockerfile และพฤติกรรมตอนรันไทม์ ตารางด้านล่างสรุปความแตกต่างเชิงปฏิบัติสำหรับการตั้งค่า Symfony บน production | ประเด็น | โหมด worker ของ FrankenPHP | Nginx + PHP-FPM | |--------|------------------------|-----------------| | คอนเทนเนอร์ | หนึ่งตัว | สองตัว (เว็บเซิร์ฟเวอร์ + PHP) | | การบูตเคอร์เนล | ครั้งเดียวต่อ worker | ครั้งเดียวต่อคำขอ | | HTTP/3 | มีในตัว | ต้องตั้งค่าเพิ่ม | | TLS | อัตโนมัติ (Caddy) | ตั้งค่าใบรับรองด้วยตนเอง | | การจัดการสถานะ | ต้องรีเซ็ตระหว่างคำขอ | แยกอิสระต่อคำขอ | | ต้นทุน cold-start | จ่ายครั้งเดียวตอนบูต | จ่ายในทุกคำขอ | โหมด worker เหนือกว่าในด้าน throughput เพราะเฉลี่ยขั้นตอนราคาแพงอย่างการบูตเคอร์เนลและการคอมไพล์คอนเทนเนอร์ให้กระจายไปทั่วคำขอนับพัน ส่วน PHP-FPM ยังคงมีความสำคัญเมื่อแอปพลิเคชันพึ่งพาตัวแปร global หรือไลบรารีบุคคลที่สามที่สมมติว่ามีโปรเซสใหม่สดต่อคำขอ เพราะสิ่งเหล่านั้นจะพังภายใต้ worker ที่ถูกใช้ซ้ำ > **การรั่วไหลของสถานะในโหมด Worker** > > เซอร์วิสที่แคชข้อมูลของคำขอไว้ใน property แบบ private จะส่งข้อมูลของคำขอหนึ่งไปยังคำขอถัดไปหากไม่รีเซ็ต ควรตรวจสอบ singleton, event subscriber และทุกสิ่งที่ถือ security token หรือคำขอปัจจุบัน ก่อนสลับไปใช้โหมด worker บน production ## การจัดการตัวแปรสภาพแวดล้อมและ Secret บน Production Symfony อ่านการกำหนดค่าจากตัวแปรสภาพแวดล้อม และมีสองวิธีที่มั่นคงในการจัดหาค่าเหล่านั้นบน production วิธีแรกคือฉีดตัวแปรแบบธรรมดาผ่านออร์เคสเทรเตอร์หรือไฟล์ Compose วิธีที่สองคือ vault สำหรับ secret ที่เข้ารหัสของ Symfony ซึ่งเก็บค่าที่อ่อนไหวไว้ในรีโพซิทอรีในรูปของ ciphertext แล้วถอดรหัสตอนรันไทม์ด้วยกุญแจส่วนตัวเพียงดอกเดียว ```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)" ``` ขั้นตอน `composer dump-env prod` ใน Dockerfile จะคอมไพล์ไฟล์ `.env` ให้เป็นไฟล์ `.env.local.php` ที่ปรับให้เหมาะสมเพียงไฟล์เดียว ซึ่งขจัดต้นทุนตอนรันไทม์ของการแจงไฟล์ dotenv ตอนบูต ตัวแปรใดก็ตามที่ตั้งค่าไว้ในสภาพแวดล้อมคอนเทนเนอร์จริงยังคงมีลำดับความสำคัญเหนือค่าเริ่มต้นที่คอมไพล์ไว้ ดังนั้น secret ที่ออร์เคสเทรเตอร์จัดหามาจึงชนะเสมอ [คู่มือการดีพลอย Symfony](https://symfony.com/doc/current/deployment.html) อธิบายลำดับความสำคัญทั้งหมดไว้ แต่กฎเชิงปฏิบัตินั้นเรียบง่าย นั่นคืออย่าฝัง `APP_SECRET` หรือข้อมูลรับรองฐานข้อมูลลงในอิมเมจ แต่ให้ฉีดเข้าไปตอนคอนเทนเนอร์เริ่มทำงาน ## การปรับอิมเมจ Symfony สำหรับ Production ให้เหมาะสม การตั้งค่า OPcache สองตัวคิดเป็นส่วนใหญ่ของช่องว่างประสิทธิภาพบน production นั่นคือการปิดการตรวจสอบ timestamp และการเปิดใช้ preloading เนื่องจากอิมเมจคอนเทนเนอร์ไม่เปลี่ยนแปลง ซอร์สโค้ดจึงไม่มีทางเปลี่ยนตอนรันไทม์ OPcache จึงไม่ควรทำ stat ไฟล์เพื่อตรวจหาการแก้ไข ส่วน preloading ยิ่งไปไกลกว่านั้นด้วยการโหลดคลาสของ Symfony และของแอปพลิเคชันเข้าสู่หน่วยความจำที่แชร์ร่วมกันครั้งเดียวตอนเซิร์ฟเวอร์เริ่มทำงาน ```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 สร้างไฟล์ `preload.php` ที่ระบุไว้ด้านบนระหว่าง `cache:warmup` ไฟล์นี้จึงมีอยู่แล้วในอิมเมจหลังบิลด์ [OPcache preloading](https://www.php.net/manual/en/opcache.preloading.php) จะลิงก์คลาสเหล่านี้ตอนสตาร์ทและข้ามขั้นตอนการคอมไพล์ในคำขอแรกที่ต้องใช้คลาสนั้น การตั้งค่า `validate_timestamps=0` ปลอดภัยเฉพาะกับการดีพลอยแบบ immutable ที่การรีลีสใหม่หมายถึงอิมเมจใหม่ ส่วนบนโฮสต์ที่เปลี่ยนแปลงได้จะเสิร์ฟโค้ดเก่าที่ล้าสมัย การกำหนดขนาด `max_accelerated_files` ให้สูงกว่าจำนวนไฟล์จริงช่วยให้ทั้งเฟรมเวิร์กถูกแคชไว้โดยไม่มีการ eviction ## การลดขนาดอิมเมจ Symfony ด้วย .dockerignore การขาดไฟล์ `.dockerignore` ทำให้บิลด์บวมขึ้นอย่างเงียบ ๆ หากไม่มีไฟล์นี้ ขั้นตอน `COPY . .` จะขนไดเรกทอรี `vendor` ในเครื่อง, `var/cache` จากสภาพแวดล้อมพัฒนา, ผลลัพธ์การบิลด์ของ node และประวัติ `.git` เข้าไปในอิมเมจและใน build context โดยตรง การยกเว้นสิ่งเหล่านี้จะลดขนาดอิมเมจ เร่งการอัปโหลด build context ไปยังเดมอน และป้องกันไม่ให้อาร์ติแฟกต์จากการพัฒนาไปเขียนทับดีเพนเดนซี production ที่เพิ่งติดตั้งสด ๆ ```gitignore # .dockerignore /.git/ /vendor/ /node_modules/ /var/ /.env.local /.env.*.local /tests/ /docker/ compose*.yaml Dockerfile ``` การยกเว้น `/vendor/` สำคัญที่สุด เพราะสเตจ production รัน `composer install --no-dev` การขนไดเรกทอรี vendor แบบ dev ของโฮสต์เข้าไปจึงทำลายขั้นตอนนั้นโดยสิ้นเชิง การยกเว้น `/var/` จะกันแคชและล็อกของการพัฒนาออกจากอิมเมจ เปิดทางให้ `cache:warmup` ตอนบิลด์สร้างแคช production ที่สะอาด เมื่อรวมกับบิลด์แบบหลายสเตจและเอกซ์เทนชันที่อิงกับ Alpine อิมเมจ Symfony ที่กระชับมักมีขนาดต่ำกว่า 150 MB อย่างสบาย ## การดีพลอยคอนเทนเนอร์ Symfony และการรัน Migration ไฟล์ Compose สำหรับ production จะอ้างอิงอิมเมจที่บิลด์ไว้ล่วงหน้าจากรีจิสทรีแทนที่จะบิลด์ในเครื่อง และกำหนด health check เพื่อให้ออร์เคสเทรเตอร์ส่งทราฟฟิกไปยังคอนเทนเนอร์ที่ตอบสนองเท่านั้น secret มาถึงในรูปแบบตัวแปรสภาพแวดล้อม ไม่ใช่เลเยอร์ของอิมเมจ ```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 จะ curl ไปยังรูต `/health` แอปพลิเคชันจึงต้องเปิดเผยรูตนี้ คอนโทรลเลอร์แบบขั้นต่ำที่คืนค่า 200 โดยไม่แตะฐานข้อมูลจะทำให้การตรวจสอบรวดเร็ว และหลีกเลี่ยงการทำเครื่องหมายว่าคอนเทนเนอร์ไม่แข็งแรงระหว่างที่ฐานข้อมูลสะดุดชั่วคราว เมื่อความพร้อมของฐานข้อมูลสำคัญ อาจใช้โพรบเชิงลึกตัวที่สองรันคิวรีน้ำหนักเบา แต่เอนด์พอยต์ liveness ควรคงความเรียบง่ายไว้ ```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']); } } ``` การรันไมเกรชันฐานข้อมูลควรทำเป็นขั้นตอนแยกต่างหากก่อนที่คอนเทนเนอร์ใหม่จะรับทราฟฟิก ไม่ใช่ทำภายในการบูตแอปพลิเคชัน การรันในคอนเทนเนอร์แบบครั้งเดียว (one-off) รับประกันว่าสคีมาจะเป็นปัจจุบันเพียงครั้งเดียวต่อการรีลีส แม้ว่าจะมีเรพลิกาของแอปพลิเคชันหลายตัวเริ่มทำงานพร้อมกัน ```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 ``` แฟล็ก `--wait` จะบล็อกจนกว่าทุกเซอร์วิสจะรายงานว่าแข็งแรง มอบสัญญาณความสำเร็จที่แท้จริงให้สคริปต์ดีพลอย แทนที่จะเป็นการสตาร์ทแบบยิงแล้วลืม การจับคู่สิ่งนี้กับ rolling update ในออร์เคสเทรเตอร์อย่าง Kubernetes หรือ Docker Swarm จะให้การรีลีสแบบไม่มีดาวน์ไทม์ คอนเทนเนอร์เก่ายังคงให้บริการต่อไปจนกว่าคอนเทนเนอร์ใหม่จะผ่าน health check สำหรับภาพรวมที่กว้างขึ้นของเวอร์ชันเฟรมเวิร์กที่เวิร์กโฟลว์นี้มุ่งเป้า [ภาพรวมแพลตฟอร์ม Symfony](/technologies/symfony) และ [คู่มือฟีเจอร์ Symfony 8 กับ PHP 8.4](/blog/symfony/symfony-8-new-features-php-84-lazy-objects) ครอบคลุมสิ่งที่เปลี่ยนแปลงในสายรีลีสปัจจุบัน ## บทสรุป - ใช้ Dockerfile แบบหลายสเตจเพื่อให้อิมเมจสำหรับพัฒนาและ production แชร์ฐานเดียวกัน ขณะที่ทาร์เก็ต production ส่งมอบโดยไม่มีดีเพนเดนซี dev ของ Composer - คัดลอก `composer.json` และไฟล์ล็อกก่อนซอร์สโค้ด เพื่อให้เลเยอร์การติดตั้งดีเพนเดนซีถูกแคชไว้ข้ามการเปลี่ยนแปลงโค้ด - รัน FrankenPHP ในโหมด worker เพื่อใช้เคอร์เนลที่บูตแล้วซ้ำข้ามคำขอ และรีเซ็ตเซอร์วิสที่มีสถานะด้วย `ResetInterface` เพื่อป้องกันการรั่วไหลของสถานะ - วอร์มแคชของ Symfony และคอมไพล์ `.env` ด้วย `dump-env prod` ตอนบิลด์ เพื่อให้คอนเทนเนอร์ที่กำลังรันเริ่มทำงานโดยเริ่มต้นค่าครบถ้วน - เปิดใช้ OPcache preloading และตั้งค่า `validate_timestamps=0` บน production ซึ่งปลอดภัยได้ก็เพราะอิมเมจไม่เปลี่ยนแปลงนั่นเอง - เก็บ `APP_SECRET` ข้อมูลรับรองฐานข้อมูล และกุญแจถอดรหัส vault ออกจากเลเยอร์ของอิมเมจ ให้ฉีดเข้าไปเป็นตัวแปรสภาพแวดล้อมตอนคอนเทนเนอร์เริ่มทำงาน - รัน Doctrine migration ในคอนเทนเนอร์แบบครั้งเดียวก่อนที่เรพลิกาใหม่จะรับทราฟฟิก และคุมการโรลเอาต์ด้วย health check ผ่าน `up -d --wait` --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/symfony/symfony-docker-development-production