Skip to content

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:

bin/app.php
(new WinterWebSwooleApplication())->run(MyApplication::class);

Use Box to compile your application and all its Composer dependencies into a single, self-contained Phar archive.

  1. Install dependencies

    Terminal window
    composer install --no-dev --optimize-autoloader
  2. Install Box

    Terminal window
    composer global require humbug/box

    Verify the installation:

    Terminal window
    box --version
  3. Create box.json

    Add a box.json configuration 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.

  4. Compile the Phar

    Terminal window
    box compile

    Run the result, pointing -c at the directory that holds application.yml:

    Terminal window
    php target/my-app.phar -c /etc/my-app/config

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:

Dockerfile
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --ignore-platform-reqs
FROM suvera/winter-boot:latest
WORKDIR /app
COPY --from=vendor /app/vendor/ vendor/
COPY bin/ bin/
COPY src/ src/
EXPOSE 8080
CMD ["php", "bin/app.php", "-c", "/config"]
Build
docker build -t my-winter-service:latest .
Run
docker run -p 8080:8080 -v "$(pwd)/config":/config my-winter-service:latest

Remember to set server.address: 0.0.0.0 in the mounted application.yml, otherwise the server is not reachable from outside the container.

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):

k8s/deployment.yaml
initContainers:
- name: db-migrate
image: my-winter-service:latest
command: ["php", "bin/migrate.php", "--configDir", "/config", "--sqlPath", "/app/migrations"]
volumeMounts:
- name: config
mountPath: /config

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:

Terminal window
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).

The framework repository includes an init.d template at build/init.d.sample.sh. Copy it and register it with your init system:

Terminal window
cp init.d.sample.sh /etc/init.d/my-winter-service
chmod +x /etc/init.d/my-winter-service

Replace the <<placeholder>> values at the top of the file to match your environment:

/etc/init.d/my-winter-service
USER=www-data
SERVICE_NAME=my-winter-service
SERVICE="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:

Terminal window
service my-winter-service start
service my-winter-service stop
service my-winter-service restart
service my-winter-service status

To use systemd instead, create a native unit file:

/etc/systemd/system/my-winter-service.service
[Unit]
Description=My Winter Boot Service
After=network.target
[Service]
Type=simple
User=www-data
ExecStart=/usr/bin/php /var/www/my-app/target/my-app.phar -c /etc/my-app/config
Restart=on-failure
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target

Reload systemd and enable the service:

Terminal window
systemctl daemon-reload
systemctl enable my-winter-service
systemctl start my-winter-service

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:

/etc/nginx/sites-available/my-app.conf
location / {
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_param SCRIPT_FILENAME /var/www/my-app/bin/app.php;
include fastcgi_params;
}

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.