Deploy
Run PhlatPage with Docker Compose, keep per-deployment config in env.json, ship releases with Deployer, and back up what matters.
On this page
A deployment is two things: the code, which is rebuilt from git on every release, and your state (site/pages/ and site/data/: pages, collections, users and secrets), which stays on the server between releases.
PhlatPage runs on PHP 8.4 behind any web server whose document root is site/public/. The project ships a Docker Compose setup, a Deployer config for releases over SSH, and a reusable image. If you already have a way to deploy PHP, use it: keep site/pages/ and site/data/ out of the release and back them up.
Build the CSS and JS before you deploy. The deploy does not run a build step.
Layout
A deployment has two halves. phlat/ and vendor/ are code from the framework repository, and site/ is the site's own repository (its views, config and content). All of it is rebuilt from git on every release. site/pages/ is the content, a git repository of its own that History commits to. site/data/ is state: collections/, users/, git/ (the History deploy key), storage/ (the registry, locks and rate limits), cache/ (compiled views and the like, which can be deleted at any time) and env.json. State is never baked into the image and survives redeploys, except cache/, which each release makes for itself.
Docker Compose
compose.yaml runs PHP-FPM, Caddy and Mailpit, with the project mounted at /app:
services:
fpm:
image: git.cohere.digital/phlat/php:latest
build:
context: phlat
dockerfile: Dockerfile.fpm
restart: unless-stopped
user: 1000:1000
volumes:
- .:/app
caddy:
image: caddy:alpine
restart: unless-stopped
depends_on:
- fpm
volumes:
- .:/app
- ./phlat/Caddyfile.fpm:/etc/caddy/Caddyfile
ports:
- 10000:80
mailpit:
image: axllent/mailpit
restart: unless-stopped
ports:
- 8025:8025
- 1025:1025
fpm is PHP 8.4 with gd, imagick and git (phlat/Dockerfile.fpm). caddy serves site/public/ on port 10000 and passes PHP requests to fpm (phlat/Caddyfile.fpm). mailpit catches outgoing mail: SMTP on 1025, the inbox on 8025. Put a reverse proxy in front for TLS; Caddy trusts private-range proxies for forwarded headers.
Config
Config merges in order, later wins: phlat/config.json, site/config.json, site/data/env.json. Put secrets and per-host overrides in site/data/env.json, which is gitignored. site/data/env.cli.json merges on top for CLI runs only.
Deployer
deploy.php ships the code to a host over SSH. Start from phlat/examples/deploy.example.php and set the repository, host and deploy_path. Host credentials, and site_repository (the site's own repository), go in the gitignored deploy.env.php. site_branch picks which branch of it is deployed.
vendor/bin/dep deploy domain.com
Each release clones the framework and installs it with composer install --no-dev. The deploy:site task then clones the site repository into the release's site/. It is a full clone, because History needs the repository. These paths are shared between releases and symlinked in:
site/data/env.jsonandsite/data/env.cli.jsonsite/pages,site/data/collections,site/data/users,site/data/git,site/data/storagesite/public
site/data/cache is not shared. Each release starts with its own, so a release never serves what an earlier one rendered.
The example deploy file restarts the PHP container after a deploy or rollback (docker restart app; name it after your own service), so PHP's opcache and realpath cache pick up the new release at once.
Reusable image
phlat/Dockerfile.dist builds a FrankenPHP runtime image with phlat/ and vendor/ baked in and no site/. A downstream project mounts its own site/, which holds its data/, over it:
services:
app:
image: git.cohere.digital/phlat/page:vX.Y.Z
volumes:
- ./site:/app/site
Backups
site/pages/ and site/data/ are the state to protect. One command backs up pages with their History, collections, users, git/ and the env files; restore with backup:restore. storage/ and cache/ are left out: restore rebuilds the registry from the pages and empties the cache.
Run it in the PHP container: fpm with this repository's compose.yaml, app in the reusable-image example above.
docker compose exec fpm php phlat/addons/cli/phlat backup:create
Directory structure
my-site/
phlat/ ← framework core (do not edit)
site/ ← your site, its own git repo
views/ ← templates, routers, and controllers (colocated per view)
fields/ ← your field definitions
fieldtypes/ ← custom field types
addons/ ← site-specific addons
public/ ← web document root
index.php
config.json ← site config, merged over phlat/config.json
pages/ ← your content, its own git repo (History commits here)
data/ ← per-deployment state, survives redeploys
collections/ ← collection items (gitignored)
users/ ← user accounts
git/ ← History's deploy key (gitignored)
storage/ ← registry, locks, rate limits (gitignored)
cache/ ← compiled views, fragments, thumbnails; delete any time (gitignored)
env.json ← deployment secrets and overrides (gitignored)
Switching branches
Because site/ is a repo of its own, switching its branch swaps the whole site: code, config and pages. The registry, cache and collections are not in git and stay behind, so after a switch run phlat registry:rebuild and phlat cache:clear.
Entry point
The web server's document root must be site/public/, whose index.php is the entry point:
<?php
// DOCUMENT_ROOT stays the logical path; __DIR__ resolves through a shared symlink on deploy
$public = $_SERVER['DOCUMENT_ROOT'];
require_once "{$public}/../../vendor/autoload.php";
Phlat::app(dirname($public))->execute();
Phlat::app() returns the singleton App for the site root. execute() resolves the URL to a page, runs the view's router and renders the response.