Build and Deploy Your Winter Boot PHP 8 Application
Winter Boot applications can be shipped as a self-contained Phar archive for servers and VMs, as a Docker image for containers, or behind PHP-FPM where PHP processes are already managed for you. The framework provides a Docker base image and a sample init.d script. A working example with a Dockerfile and a Box manifest is in the suvera/winter-example-service repository.
All builds below assume your entry point is bin/app.php and uses the WinterWebSwooleApplication runner:
(new WinterWebSwooleApplication())->run(MyApplication::class);Build Process
Section titled “Build Process”Use Box to compile your application and all its Composer dependencies into a single, self-contained Phar archive.
-
Install dependencies
Terminal window composer install --no-dev --optimize-autoloader -
Install Box
Terminal window composer global require humbug/boxVerify the installation:
Terminal window box --version -
Create box.json
Add a
box.jsonconfiguration file to your project root:box.json {"output": "target/my-app.phar","main": "bin/app.php","directories": ["bin", "src", "vendor"],"compression": "GZ","chmod": "0755"}Keep
config/out of the Phar and pass it at runtime with-c, so one build runs in every environment. -
Compile the Phar
Terminal window box compileRun the result, pointing
-cat the directory that holdsapplication.yml:Terminal window php target/my-app.phar -c /etc/my-app/config
Deployment
Section titled “Deployment”Docker Deployment
Section titled “Docker Deployment”Build on the official suvera/winter-boot base image. It is based on php:8.5-cli-alpine and includes swoole, winter_boot, redis, rdkafka, pcntl, curl, zip, pdo_pgsql, pgsql, and the PostgreSQL client (psql). For MySQL, build on suvera/winter-boot-mysql, which has pdo_mysql, mysqli, and the mysql client (MariaDB’s) in their place, so useCli: true migrations work out of the box. Both images are tagged with the framework version and latest. Their php.ini keeps variables_order = EGPCS, so $env.* property sources work. (All images come from build/docker/Dockerfile in the framework repository, selected with --build-arg DB=pgsql|mysql and --build-arg HOOK_ALL=0|1, if you need to build a variant.)
If the application sets server.swoole.hook_flags: SWOOLE_HOOK_ALL, build on the -hookall image of your database: suvera/winter-boot-hookall (PostgreSQL) or suvera/winter-boot-mysql-hookall (MySQL). They have the same extensions, but Swoole is compiled with:
| Swoole build option | Image | Effect under SWOOLE_HOOK_ALL |
|---|---|---|
--enable-swoole-curl |
both | cURL (including curl_multi, which Guzzle uses) runs on the native libcurl hook. Without it Swoole emulates cURL, and curl_multi crashes the worker. |
--enable-swoole-pgsql |
winter-boot-hookall |
PDO-pgsql queries yield to other coroutines instead of blocking the worker (SWOOLE_HOOK_PDO_PGSQL). |
pdo_mysql needs no extra option: it already yields under SWOOLE_HOOK_ALL in both MySQL images. With the default hook_flags (no hooks) the plain images are the right choice.
The base image does not include Composer, so install dependencies in a separate stage:
FROM composer:2 AS vendorWORKDIR /appCOPY composer.json composer.lock ./RUN composer install --no-dev --optimize-autoloader --ignore-platform-reqs
FROM suvera/winter-boot:latestWORKDIR /appCOPY --from=vendor /app/vendor/ vendor/COPY bin/ bin/COPY src/ src/
EXPOSE 8080CMD ["php", "bin/app.php", "-c", "/config"]docker build -t my-winter-service:latest .docker run -p 8080:8080 -v "$(pwd)/config":/config my-winter-service:latestRemember to set server.address: 0.0.0.0 in the mounted application.yml, otherwise the server is not reachable from outside the container.
Key Considerations
Section titled “Key Considerations”PHP 8.5+ required
Winter Boot requires PHP 8.5 or higher. The base image ships PHP 8.5 CLI.
PHP extensions
winter_boot is always required, and swoole is required for the WinterWebSwooleApplication runner. The base image includes both; for other images, see Installation.
Composer install at build time
Run composer install --no-dev --optimize-autoloader during the docker build step, not at container start. Installing at start adds latency to every container launch.
Config directory at runtime
Mount your application.yml at a known path and pass it with -c /config at container start. Avoid baking environment-specific config into the image layer.
Kubernetes — Init Container for Migrations
Section titled “Kubernetes — Init Container for Migrations”When deploying with SQL migrations, run the migration runner as a Kubernetes init container so the schema is up to date before the application container starts (see Migrations for the runner script):
initContainers: - name: db-migrate image: my-winter-service:latest command: ["php", "bin/migrate.php", "--configDir", "/config", "--sqlPath", "/app/migrations"] volumeMounts: - name: config mountPath: /configBare-Metal / VM Deployment
Section titled “Bare-Metal / VM Deployment”For traditional host-based deployments, build a Phar archive and manage the process with the provided init.d sample script.
Start the application with an explicit config directory:
php target/my-app.phar -c /etc/my-app/config/The -c flag points to the directory containing your application.yml (and any additional config files such as logger.yml).
init.d Service Management
Section titled “init.d Service Management”The framework repository includes an init.d template at build/init.d.sample.sh. Copy it and register it with your init system:
cp init.d.sample.sh /etc/init.d/my-winter-servicechmod +x /etc/init.d/my-winter-serviceReplace the <<placeholder>> values at the top of the file to match your environment:
USER=www-dataSERVICE_NAME=my-winter-serviceSERVICE="target/my-app.phar"CONFIG_DIR="/etc/my-app/config"ADMIN_PORT="9090"ADMIN_TOKEN_FILE="/etc/my-app/admin.token"LOG_FILE="/var/log/my-winter-service/app.log"PID_FILE="/var/run/my-winter-service.pid"PHP_BINARY="php"Then control the service with standard commands:
service my-winter-service startservice my-winter-service stopservice my-winter-service restartservice my-winter-service statussystemd Service Management
Section titled “systemd Service Management”To use systemd instead, create a native unit file:
[Unit]Description=My Winter Boot ServiceAfter=network.target
[Service]Type=simpleUser=www-dataExecStart=/usr/bin/php /var/www/my-app/target/my-app.phar -c /etc/my-app/configRestart=on-failureStandardOutput=journalStandardError=journal
[Install]WantedBy=multi-user.targetReload systemd and enable the service:
systemctl daemon-reloadsystemctl enable my-winter-servicesystemctl start my-winter-servicePHP-FPM Deployment
Section titled “PHP-FPM Deployment”When using the WinterWebApplication runner (no Swoole server), point your web server’s FastCGI configuration at your application entry point. The winter_boot extension is still required:
location / { fastcgi_pass unix:/run/php/php8.5-fpm.sock; fastcgi_param SCRIPT_FILENAME /var/www/my-app/bin/app.php; include fastcgi_params;}Full Example Project
Section titled “Full Example Project”The suvera/winter-example-service repository is a complete Winter Boot microservice with a Dockerfile built on the base image, a Box manifest (deploy/box.json) and build script for the Phar, and sample configuration for datasources, Redis, and DTCE.