Skip to content

Statamic Guide

Statamic Guide

Statamic is a Laravel application, and the entrypoint detects it as one: you get storage permission fixes, the scheduler, queue workers and the optimize hooks without configuration. This guide covers the part that is Statamic-specific and historically painful in containers: git integration.

Quick start

services:
  statamic:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    ports:
      - "8080:80"
    volumes:
      - ./:/var/www/html
    environment:
      - PUID=1000
      - PGID=1000
      - LARAVEL_SCHEDULER=true

Expected startup log:

[INFO] Framework detected: laravel
[INFO] PHP-FPM listening on unix socket: /run/php/php-fpm.sock

Git integration

Statamic's git automation commits (and optionally pushes) content changes after every save. Three things have to be true inside the container, and all three ship in the image since 1.6.2:

  1. git is installed (all tiers).
  2. openssh-client is installed - git push to an SSH remote needs ssh, and pinning GitHub's host key needs ssh-keyscan.
  3. git trusts the mounted repo. git 2.35+ refuses to operate on a directory owned by another UID (fatal: detected dubious ownership), which is the normal state for a bind-mounted site. The image bakes safe.directory = * into /etc/gitconfig, so this never bites - in root, rootless and read-only-rootfs modes alike.

Wrong (the classic failure on generic images):

fatal: detected dubious ownership in repository at '/var/www/html'
ssh: not found

Correct (this image): both problems are handled at build time. You only supply the key and the remote.

1. Create a deploy key

ssh-keygen -t ed25519 -N "" -C "statamic deploy" -f ./deploy_key
ssh-keyscan -t ed25519 github.com > ./known_hosts

Add deploy_key.pub as a deploy key with write access on the repository (GitHub: Settings → Deploy keys → Allow write access).

2. Wire it into the container

services:
  statamic:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    volumes:
      - ./:/var/www/html
      - ./deploy_key:/run/secrets/git_key:ro
      - ./known_hosts:/etc/ssh/ssh_known_hosts:ro
    environment:
      - PUID=1000
      - PGID=1000
      - LARAVEL_QUEUE=true            # Statamic commits async via the queue
      - GIT_SSH_COMMAND=ssh -i /run/secrets/git_key -o IdentitiesOnly=yes
      - STATAMIC_GIT_ENABLED=true
      - STATAMIC_GIT_PUSH=true
      - STATAMIC_GIT_USER_NAME=Statamic Bot
      - [email protected]

Notes:

  • The key must be 0400 and owned by the runtime user. ssh refuses group- or world-readable private keys outright (bad permissions ... This private key will be ignored) - 0444 does NOT work, we tested. On Linux: chown 33:33 deploy_key && chmod 400 deploy_key (or set PUID/PGID to your host UID and keep the key 0400 under your own ownership - then www-data runs as that UID and reads it as the owner).
  • /etc/ssh/ssh_known_hosts is the system-wide known-hosts file, so no per-user ~/.ssh setup is needed for www-data.
  • Statamic runs git through the queue when a worker is available (LARAVEL_QUEUE=true), keeping saves fast in the control panel.

3. Verify from inside the container

docker compose exec -u www-data statamic php please git commit

Expected output:

Committing changes...
[main abc1234] Content saved
Changes committed.

And with push enabled the commit lands on the remote immediately after.

Troubleshooting

Symptom Cause Fix
Host key verification failed No known_hosts for the remote Mount the ssh-keyscan output at /etc/ssh/ssh_known_hosts
Permission denied (publickey) Key unreadable for www-data, or deploy key lacks write access Check mount ownership/mode; enable "Allow write access" on the deploy key
dubious ownership You are on an image without the baked safe.directory (pre-1.6.2) Upgrade, or git config --system --add safe.directory '*' in a derived image
Saves are slow in the control panel Git runs synchronously Set LARAVEL_QUEUE=true so commits go through the worker
Nothing commits Git integration is a Statamic Pro feature Enable Pro (config/statamic/editions.php); trial mode works locally

Deployment modes

Statamic runs in two fundamentally different shapes, and git integration works differently in each. Both patterns below are validated against this image.

Mount mode (dev, single server)

The whole site is a bind mount - everything above this section describes mount mode. The site directory IS the git repository, saves commit in place, and PUID/PGID keep host and container ownership aligned. Use it when the server is long-lived and the site directory is the source of truth.

Baked mode (production, Kubernetes, immutable deploys)

The site is compiled into the image; nothing is mounted. This is the multi-stage pattern:

# Build stage: composer in the CLI image (it ships Node.js too, so a
# `npm ci && npm run build` stage for the frontend fits the same pattern)
FROM ghcr.io/cboxdk/php-baseimages/php-cli:8.5-bookworm-v1 AS build
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-autoloader --no-scripts --no-interaction
COPY . .
RUN composer dump-autoload --optimize

# Runtime stage: the multi-service image
FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
COPY --from=build --chown=www-data:www-data /app /var/www/html
docker build -t my-site:$(git rev-parse --short HEAD) .
docker run -d -p 8080:80 my-site:abc1234

Expected: the container reports healthy and serves the site with no volumes at all.

Git-backed content in baked mode

An immutable image and runtime content edits pull in opposite directions. The pattern that reconciles them: the content directory is its own git repository on a volume, separate from the site code.

  1. Keep content/ (plus whatever else editors change - users/, asset containers) in a dedicated content repository.
  2. Mount that repository as a volume; the baked image carries the code, the volume carries the editable state.
  3. Point Statamic's git integration at the mounted paths (git.paths in config/statamic/git.php) and set the work tree to the volume.
services:
  statamic:
    image: my-site:abc1234
    volumes:
      - ./content-repo:/var/www/html/content
      - ./deploy_key:/run/secrets/git_key:ro
      - ./known_hosts:/etc/ssh/ssh_known_hosts:ro
    environment:
      - LARAVEL_QUEUE=true
      - GIT_SSH_COMMAND=ssh -i /run/secrets/git_key -o IdentitiesOnly=yes
      - STATAMIC_GIT_ENABLED=true
      - STATAMIC_GIT_PUSH=true

The empty-volume trap (we hit it validating this page): mounting a volume that does not exist yet over a baked path gives you an EMPTY directory on top of your baked content - Docker creates the empty host dir and the site 404s. A content volume must be seeded before first boot (clone the content repo to the host path first, or use an init container that clones when the volume is empty):

# Seed once, before first start
git clone [email protected]:you/site-content.git ./content-repo
docker compose up -d

New code deploy = new image, content volume untouched. Content edits = commits pushed from the running container, pulled into the next image build if you also bake a content snapshot as fallback.

Choosing

Mount mode Baked mode
Deploys git pull / rsync in place immutable image per release
Git integration whole site is the repo content volume is its own repo
Fits single server, dev, small sites Kubernetes, autoscaling, CI/CD
Trap to know key must be 0400 (above) seed the content volume first

What the Laravel detection gives Statamic for free

  • storage/ and bootstrap/cache/ permissions fixed at boot (PUID-aware)
  • LARAVEL_SCHEDULER=true runs schedule:work (Statamic uses it for scheduled entries)
  • LARAVEL_QUEUE=true runs queue workers with graceful stop contracts (SIGTERM + 60s, so an in-flight git commit finishes on docker stop)
  • LARAVEL_OPTIMIZE_ENABLED=true caches config/routes/views at startup

For the general Laravel features, see the Laravel Guide.