Operations
MarkdownDeploy with Docker
Operator path for self-hosted Externa: pull a published image (or build once), set a small .env, docker compose up, healthy stack. Mirrors the Directus-style friction goal.
Related
Deployment (bare metal + full Docker notes) · Installation — Docker local · Reverse proxy · Environment variables
Security: re-pull GHCR images (CVE-2026-109618 / CVE-2026-109619)
Official ghcr.io/qiick-io/externa-core:1.2.2 and latest ship PHP 8.4.27 on php:8.4.27-fpm-bookworm with a hardened docker/php/php.ini (session.upload_progress.enabled=0, session.use_strict_mode=1, body limits matching nginx client_max_body_size 100M) — core#216, Composer php: ^8.4.27 from #248. Re-pull and recreate containers (docker compose pull && docker compose up -d), then check with docker compose exec <php-service> php -v. Source builds: docker build --pull --no-cache so the base image updates. Details: discussion #247. Bare-metal hardening: Deployment — PHP runtime hardening.
Quick deploy (pull GHCR)
Published multi-arch image: ghcr.io/qiick-io/externa-core (linux/amd64 + linux/arm64). CI pushes on version tags (v*) and optional workflow dispatch.
# From an externa-core checkout (Compose + env sample live in the repo)
cp .env.docker.quick.example .env
# Required: APP_KEY, strong DB_PASSWORD, APP_URL
php -r "copy('.env', '.env.bak'); file_put_contents('.env', preg_replace('/^APP_KEY=.*/m', 'APP_KEY=base64:'.base64_encode(random_bytes(32)), file_get_contents('.env')));"
# First boot only — create roles + super admin (then set RUN_SEED=false)
# RUN_SEED=true
# INITIAL_SUPER_ADMIN_EMAIL=you@example.com
# INITIAL_SUPER_ADMIN_PASSWORD='…strong…'
docker compose -f compose.quick.yaml up -d
Open http://localhost:8080 (host APP_PORT, default 8080→80). Reverb publishes on host 8081.
| Piece | Default |
|---|---|
| Image | ghcr.io/qiick-io/externa-core:latest (EXTERNA_IMAGE override) |
| Services | app, horizon, reverb, scheduler, pulse, pgsql, redis |
| Volumes | externa_quick_storage, externa_quick_pgsql, externa_quick_redis |
| Healthcheck | GET /health/ready on app (start_period: 90s) |
Pin a release:
EXTERNA_IMAGE=ghcr.io/qiick-io/externa-core:1.3.0 \
docker compose -f compose.quick.yaml up -d
Upgrades: bump EXTERNA_IMAGE (or rebuild from a newer git tag), keep named volumes, then migrate / externa:upgrade inside the app container when RUN_MIGRATIONS is off — Upgrade — Path C (Docker).
Local smoke without GHCR pull (bake once, then quick Compose):
docker build --target production -t externa:prod .
EXTERNA_IMAGE=externa:prod docker compose -f compose.quick.yaml up -d
Build context (core 1.3.0+)
When building from a checkout, .dockerignore excludes /backups, /storage/app/*, agent tooling folders (.agents, .claude, .codex, …), .devcontainer, compose bake files, and related paths so local data is not baked into the image (core PR #294). Entrypoint recreates storage/app/{public,private,zips,tmp} at start.
/storage/ sandbox (core 1.3.0+)
Official nginx config serves public /storage/ with nosniff and a CSP sandbox that blocks scripts/objects (core PR #314). Complements the upload extension denylist (SSI/markup) — File manager — Upload security.
Minimal .env
| Variable | Required | Notes |
|---|---|---|
APP_KEY | yes | base64:… from key:generate / random 32 bytes |
APP_URL | yes | Public URL browsers hit (match proxy / port) |
DB_PASSWORD | yes | Shared with Compose pgsql |
DB_* | defaults | pgsql / externa / host pgsql inside Compose |
INITIAL_SUPER_ADMIN_* | when seeding | Email + strong password before RUN_SEED=true |
RUN_MIGRATIONS | sample true | Entrypoint runs migrate --force only when true |
RUN_SEED | sample false | Opt-in db:seed --force (roles + super admin) |
REVERB_* | sample | Change app key/secret for anything beyond laptop demos |
TRUSTED_PROXIES | behind TLS proxy | * or CIDRs — see Reverse proxy |
Full catalog: Environment variables — Docker Compose knobs.
First boot (gated)
Entrypoint defaults RUN_MIGRATIONS=false and RUN_SEED=false — restarts do not surprise-mutate the database unless Compose/env opts in.
- First bring-up: keep
RUN_MIGRATIONS=true(quick/prod samples). - Optional admin bootstrap: set strong
INITIAL_SUPER_ADMIN_*, thenRUN_SEED=truefor one start. - After login works: set
RUN_SEED=false(and optionallyRUN_MIGRATIONS=falseonce schema is stable). - Seeders are idempotent on permissions/roles/super-admin email — still treat seed-on-boot as a bootstrap flag, not a forever default.
No automatic destructive wipe. migrate --force only applies pending migrations.
Healthchecks
Compose app probes GET /health/ready (DB + required Redis). Also available:
| Endpoint | Role |
|---|---|
GET /up | Laravel liveness |
GET /health/live | Process up |
GET /health/ready | Ready for traffic (503 when checks fail) |
Details: Deployment — Health probes.
Publish / multi-arch path
# Bake amd64+arm64 (no push)
docker buildx bake -f docker-bake.hcl production
# or
./scripts/docker-buildx.sh production
# Push to GHCR (needs docker login ghcr.io)
VERSION=1.0.0 PUSH=1 ./scripts/docker-buildx.sh production
CI: workflow docker — compose-config (includes compose.quick.yaml), single-arch image-build, multiarch-bake on PR/push, ghcr-publish on v* tags (+ workflow_dispatch).
Build notes: phpredis 6.3.0 pinned from GitHub tarball (not flaky pecl); npm prepare skips Lefthook without .git (#94). Details: Deployment — multi-arch.
Build-from-source production
When you need to bake assets yourself (custom Vite Reverb public URL, patches):
cp .env.docker.prod.example .env
# APP_KEY, secrets, APP_URL, TRUSTED_PROXIES, browser-facing REVERB_*
docker compose -f compose.prod.yaml up --build -d
Same healthchecks and gated RUN_MIGRATIONS / RUN_SEED as quick deploy.
Follow-through checklist
| Path | When | Link |
|---|---|---|
| Managed DB / Redis | Postgres/Redis outside Compose (RDS, Upstash, …) | compose.prod.yaml + compose.prod.managed.yaml — Deployment — Managed · core #15 |
| Reverse proxy / TLS | Public HTTPS + WebSocket Upgrade to Reverb | Reverse proxy cookbook · core #17 |
| Local full stack | Dev with Vite/Mailpit/MinIO | Installation — Docker |
| Bare metal | No Compose | Deployment |
Smoke after up
curl -fsS http://localhost:8080/health/ready→"status":"ok".- Login with
INITIAL_SUPER_ADMIN_*(if seeded) or your provisioned admin. - Confirm Horizon/scheduler containers are up; trigger a small queued job if useful.
Related
- Core README Docker sections · samples:
.env.docker.quick.example,.env.docker.prod.example,.env.docker.prod.managed.example - Upgrade · Backup & restore · Operations