Docker

The included Dockerfile and compose.yaml run one site per container. A container sends every request, whatever its host name, to the FLEXAGON_PUBLIC directory (default public). Which domain reaches which container is up to the load balancer, ingress or Cloud Run domain mapping.

Target Purpose Contents
dev Local development (compose.yaml) Project mounted at /app, php.ini-development, composer, Xdebug (installed, not loaded)
alpha, beta, production Deploy image per stage Project copied in and application/ overwritten by the files in application/_Config/<stage>/, composer install --no-dev, php.ini-production, opcache timestamp checks off, runs as www-data, flexagon check during the build

A PHP 8.3 + Apache image; the container listens on PORT (default 8080). The extensions to install are set in PHP_EXTENSIONS at the top of the Dockerfile: delete the ones you do not use and add the ones that are missing (see DEPLOY.md).

Default PHP extensions in the image: pdo_mysql, pdo_pgsql, opcache, bcmath, exif, gd, gettext, intl, zip

Development

docker compose up -d                       # web (:8080)
docker compose exec web composer install   # when vendor/ is missing
docker compose --profile xdebug up -d      # + web-xdebug with Xdebug on (:8081)

The MySQL 8.4 and PostgreSQL 17 containers are included as comments, so by default they do not start and the application connects as .env.php says. To use one, uncomment the parts of compose.yaml marked MySQL or PostgreSQL (DB_* / PGSQL_DB_* under environment, depends_on, the service, and volumes).

Once uncommented, the DB_* and PGSQL_DB_* values take precedence over .env.php, so the application in the container reaches these database containers whatever .env.php says. Every port listens on 127.0.0.1 only and can be changed with FLEXAGON_HTTP_PORT, FLEXAGON_XDEBUG_PORT, FLEXAGON_MYSQL_PORT and FLEXAGON_PGSQL_PORT.

More than one site — a public_* directory next to public/ is another site. It shares application/ and needs its own __flexagon.php, .htaccess, _entry.php, _prepare.php and _router.php (copy them from public/). Run one container per site with, for example, FLEXAGON_PUBLIC: public_admin (see the admin example in compose.yaml). Locally the sites differ by port, but cookies do not, so open each under its own host name as well: http://admin.localhost:8082.

Deployment — step-by-step instructions are in DEPLOY.md.

docker build --target production -t myapp .
docker run -p 8080:8080 -e DB_HOST=... -e DB_PASSWORD=... myapp                 # public/
docker run -p 8080:8080 -e FLEXAGON_PUBLIC=public_admin -e DB_HOST=... myapp    # public_admin/
  • Hand secrets in as environment variables through your platform's secret store. .dockerignore keeps .env.php and .env out of the build, so they never reach an image.
  • Build one image and run it as one service per site, changing only FLEXAGON_PUBLIC. Each site can then have its own database account, scaling and network exposure (an admin site on the internal network only, say).
  • Each stage (alpha, beta, production) is built as its own image with --target, so tell them apart by tag, such as myapp:1.0.0-beta.
  • The build does not use the host's vendor/; it installs from composer.lock inside the container. Path repositories inside the project (./packages/...) are installed too.
  • Sessions are encrypted cookies, so more instances need no shared store. Give every instance the same SESSION_ENCRYPTION_STRING.
  • A container's file system is gone on restart. Do not use SQLite or local uploads in production; use a managed database and object storage.
  • With a read-only root file system, mount tmpfs at /var/run/apache2 and /tmp.
  • The number of Apache processes (MaxRequestWorkers) is set at start-up from the container's memory limit: (limit − 64MB) ÷ 48MB, at least 2 and at most 256 (512MB → 9, 1GB → 20, 2GB → 41). Raise APACHE_WORKER_MEMORY_MB if your requests use more memory, or set APACHE_MAX_REQUEST_WORKERS to choose the number yourself. Requests beyond it wait rather than fail. On platforms that set a per-instance concurrency, such as Cloud Run, match it to the process count. A process is replaced after 1000 connections (APACHE_MAX_CONNECTIONS_PER_CHILD).