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:
gitis installed (all tiers).openssh-clientis installed -git pushto an SSH remote needsssh, and pinning GitHub's host key needsssh-keyscan.- 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 bakessafe.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
0400and owned by the runtime user. ssh refuses group- or world-readable private keys outright (bad permissions ... This private key will be ignored) -0444does NOT work, we tested. On Linux:chown 33:33 deploy_key && chmod 400 deploy_key(or setPUID/PGIDto your host UID and keep the key0400under your own ownership - thenwww-dataruns as that UID and reads it as the owner). /etc/ssh/ssh_known_hostsis the system-wide known-hosts file, so no per-user~/.sshsetup is needed forwww-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.
- Keep
content/(plus whatever else editors change -users/, asset containers) in a dedicated content repository. - Mount that repository as a volume; the baked image carries the code, the volume carries the editable state.
- Point Statamic's git integration at the mounted paths (
git.pathsinconfig/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/andbootstrap/cache/permissions fixed at boot (PUID-aware)LARAVEL_SCHEDULER=truerunsschedule:work(Statamic uses it for scheduled entries)LARAVEL_QUEUE=trueruns queue workers with graceful stop contracts (SIGTERM + 60s, so an in-flight git commit finishes ondocker stop)LARAVEL_OPTIMIZE_ENABLED=truecaches config/routes/views at startup
For the general Laravel features, see the Laravel Guide.