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.json and site/data/env.cli.json
  • site/pages, site/data/collections, site/data/users, site/data/git, site/data/storage
  • site/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.