Operations
Deployment
Deploy externa-core like a standard Laravel + Vite app, then ensure Externa-specific workers, scheduler jobs, and AI/file storage are wired. Local setup details remain in Installation. Upgrading an existing host: Upgrade. Backups: Backup & restore.
Deploy checklist
1. Environment secrets
Copy .env.example → production env store and set at least:
| Variable | Production note |
|---|---|
APP_ENV | production |
APP_DEBUG | false |
APP_KEY | Unique; never reuse from another env |
APP_URL | Canonical URL users browse (prefer HTTPS; required for passkeys). No trailing-slash / proxy mismatch |
DB_* | Production database (see SQLite swap below) |
SESSION_DRIVER / CACHE_STORE / QUEUE_CONNECTION | Prefer database or Redis — not array/sync |
INITIAL_SUPER_ADMIN_* | Strong password before first seed |
AI_WEBHOOK_TOKEN | Long random HMAC secret if collection-import webhooks are used |
LOCAL_AI_* / cloud provider keys | Point at a reachable gateway; do not commit keys |
AI_REMOTE_IMPORT_HOSTS | Tight allowlist if remote import is enabled |
Real MAIL_* if verification / password reset emails matter |
Full catalog: Environment variables.
After changing env on a config-cached host:
php artisan config:cache
# or config:clear during debugging
2. Build & migrate
composer install --no-dev --optimize-autoloader
npm ci
npm run build
php artisan migrate --force
Use your platform’s release process (zero-downtime, maintenance mode, etc.) as appropriate.
3. Seed / sync permissions
Fresh database:
php artisan db:seed --force
Runs PermissionSeeder → RoleSeeder → CreateSuperAdminSeeder.
Existing database after permission enum changes:
php artisan permissions:sync
# optional cleanup:
php artisan permissions:sync --prune
Do not re-seed blindly in production if it would reset roles or recreate a known super-admin password unexpectedly. Prefer sync + controlled role admin UI.
4. Storage link
php artisan storage:link
Required for public / assets disk URLs (/storage/...). See Files configuration. Ensure storage/ and bootstrap/cache/ are writable by the app user.
5. Queue worker (+ optional Reverb / Pulse)
Prefer Redis + Horizon when available:
php artisan horizon
Or a plain worker:
php artisan queue:work --tries=3 --timeout=300
If using the full realtime/observability stack, also supervise:
- A Reverb (or compatible) WebSocket server with matching
BROADCAST_*/REVERB_*/ builtVITE_REVERB_* php artisan pulse:workwhenPULSE_INGEST_DRIVER=redis
Dashboard Health surfaces native Pulse/Horizon metrics (no primary Open Pulse/Horizon CTAs). Restart workers on every deploy. Details: Operations · Minimal vs full stack.
6. Scheduler crontab
* * * * * cd /path/to/externa-core && php artisan schedule:run >> /dev/null 2>&1
This runs:
activitylog:clean(daily)files:cleanup-uploads/files:cleanup-zips(hourly)ai:cleanup-attachments(daily)ai:run-sync-sources(every minute)
7. AI gateway
If the assistant is enabled in production:
- Set
AI_DEFAULT_PROVIDER(defaultlocal) and the matching provider keys/URL. - Ensure the app server can reach
LOCAL_AI_URL(or the cloud provider) with enough timeout for tool loops (AppAssistantuses 300s). Behind nginx/Herd, also raisefastcgi_read_timeout(often ≥ 600s) so long local-model reasoning does not drop the browser SSE. - Prefer tool-calling-capable models.
- Set
AI_WEBHOOK_TOKENif using collection import webhooks; callers must HMAC-signbody + "\n" + collection_id(X-AI-Webhook-Signature). Keep CSRF exception only for that path (machine clients).
See AI configuration.
8. APP_URL and proxies
- Match the public scheme/host (Herd, reverse proxy, CDN).
- Set trusted proxies if TLS terminates upstream.
- Optionally set
FILE_PUBLIC_URL_BASEwhen asset URLs should use a CDN origin while the app stays on another host. - The CMS (password, TOTP, APIs) can run on HTTP, but prefer HTTPS in production.
- Passkeys: require HTTPS in production;
APP_URLmust match Fortify WebAuthnrelying_party_id/allowed_origins. See Passkeys — Requirements.
9. Production database (leave SQLite)
.env.example defaults to DB_CONNECTION=sqlite for local ease. For production, pick from the Supported databases matrix (PostgreSQL recommended; MySQL 8+ / MariaDB 10.6+ also supported).
- Provision Postgres 14+ (16 preferred), MySQL 8+, or MariaDB 10.6+.
- Set
DB_CONNECTION(pgsql|mysql|mariadb),DB_HOST,DB_PORT,DB_DATABASE,DB_USERNAME,DB_PASSWORD. - MySQL/MariaDB: keep
utf8mb4/utf8mb4_unicode_ci. - Run migrations against the new database.
- Keep session/cache/queue tables on a durable store (database or Redis).
SQLite ceilings
SQLite is fine for demos and single-user local work. Concurrent writers, large activity logs, and multi-worker queues push you toward Postgres/MySQL/MariaDB quickly.
Docker production
Prefer the copy-paste operator guide: Deploy with Docker (GHCR quick Compose, gated first-boot, healthchecks).
Official build-from-source path uses the multi-stage Dockerfile and compose.prod.yaml. Sail is not for production (and not required locally either).
Image stages
| Target | Role |
|---|---|
php-base | PHP 8.4-fpm + extensions (pgsql/mysql, redis, gd, zip, …) |
build | composer install --no-dev + npm ci + npm run build (Wayfinder needs vendor/PHP) |
vite | Local Compose only — PHP + Node for HMR |
development | nginx + php-fpm; source bind-mounted |
production | Baked app from build + nginx + php-fpm + prod opcache |
Roles via docker/entrypoint.sh: app, horizon, reverb, scheduler, pulse, queue.
Multi-arch images (amd64 + arm64)
Production and development targets build for linux/amd64 and linux/arm64 (Apple Silicon + typical VPS).
# Bake both platforms (Buildx). Fails if either arch fails.
docker buildx bake -f docker-bake.hcl production
# Helper wrapper (creates builder `externa-multiarch` if needed):
./scripts/docker-buildx.sh production
# Optional push to a registry (set TAG first):
# TAG=ghcr.io/qiick-io/externa-core:1.0.0-beta.3 PUSH=1 ./scripts/docker-buildx.sh production
CI: on pushes to develop / main, the multiarch-bake job runs QEMU + Buildx for both targets. Pull requests keep the faster single-arch image-build job. GHCR publish runs on v* tags (and workflow_dispatch) — image ghcr.io/qiick-io/externa-core. Quick pull path: Deploy with Docker.
Compose on Apple Silicon usually pulls/builds the matching arch automatically. To pin:
services:
app:
platform: linux/arm64 # or linux/amd64
Bring up
cp .env.docker.prod.example .env
# Set APP_KEY, strong DB_PASSWORD / REVERB_*, real mail, TRUSTED_PROXIES
# Recommended: FILES_DISK=s3 + AWS_*
docker compose -f compose.prod.yaml up --build -d
Default published app port is host 8080→80 (APP_PORT in the prod sample — different from local :8000). Reverb still defaults to host 8081→8080.
| Piece | Notes |
|---|---|
| Services | app, horizon, reverb, scheduler, pulse, pgsql, redis — no Vite, Mailpit, or MinIO |
| Volumes | Named externa_prod_storage for storage/ (zips + local disks); DB/Redis volumes when Compose-hosted |
| Migrations | RUN_MIGRATIONS=true (sample default) → migrate --force on app boot |
| Config cache | CACHE_CONFIG=true → config / route / view / event:cache |
| Healthcheck | GET /health/ready — start_period: 60s (image already baked) |
Proxy, TLS, WebSockets
Copy-paste Caddy / Traefik / nginx: Reverse proxy cookbook.
Put TLS in front (Caddy / Traefik / nginx):
- Set
APP_URL=https://your.domainandTRUSTED_PROXIES=*(or your proxy CIDRs). PreferSESSION_SECURE_COOKIE=truebehind HTTPS. - Proxy HTTP to
app:80(or publishedAPP_PORT). - Proxy WebSocket
Upgrade/Connectionto the Reverb service (reverb:8080on the Compose network, or publishedREVERB_HOST_PORT). - Point public
REVERB_HOST/REVERB_PORT/REVERB_SCHEME(and matchingVITE_REVERB_*) at the browser-facing WebSocket URL. Rebuild the production image when Vite Reverb env baked into assets changes.
Storage / S3
Prefer FILES_DISK=s3 in production so app replicas do not need sticky local binds for CMS files. Prepared zips under storage/app/zips remain local ephemeral — share the storage volume with Horizon (Compose already does) or accept zip loss across hosts. See Files configuration.
Managed DB / Redis (Compose overlay)
When Postgres and Redis run outside Compose (RDS, Upstash, etc.):
cp .env.docker.prod.managed.example .env
# Set APP_KEY, DB_HOST, REDIS_HOST, secrets, TRUSTED_PROXIES, browser-facing REVERB_*
docker compose -f compose.prod.yaml -f compose.prod.managed.yaml up --build -d
- Starts app + horizon + reverb + scheduler + pulse only.
- Bundled
pgsql/redisservices are behind profiles (bundled-db/bundled-redis) and do not start. GET /health/readystill probes the external DB and Redis when drivers require them.- Network/TLS: allow the app containers to reach managed endpoints (security groups / VPC / TLS redis as required by the provider).
Full bundled stack remains: docker compose -f compose.prod.yaml up --build -d.
Local Docker full stack: Installation — Docker Compose.
Health probes
Auth-free machine probes (not the admin Pulse/Health UI):
| Endpoint | Meaning | Response |
|---|---|---|
GET /up | Laravel liveness | 200 when the app boots |
GET /health/live | Process up | JSON { "status": "ok" } |
GET /health/ready | Ready for traffic | JSON { "status", "checks" }; 503 if DB (or required Redis) fails |
Redis is checked when QUEUE_CONNECTION, CACHE_STORE, SESSION_DRIVER, or BROADCAST_CONNECTION is redis.
Compose example (local cold boot — long start_period):
healthcheck:
test: ['CMD-SHELL', 'curl -fsS http://127.0.0.1/health/ready || exit 1']
interval: 10s
timeout: 5s
retries: 12
start_period: 300s
Production Compose uses a shorter start_period (baked image, typically 60s).
Smoke test after deploy
GET /upandGET /health/ready→ healthy JSON.- Login as super admin (or your provisioned admin) — password and, if enabled, Sign in with passkey.
- Open dashboard, files, collections (permissions permitting).
- Trigger a small queued job (e.g. zip) and confirm Horizon/worker processes it.
- If AI is enabled:
GET /ai/statusand a short chat with a read-only tool. - Confirm scheduled cleanups appear in logs after the next hour/day.
Related
- Operations — ongoing worker/scheduler/observability
- Horizon · Reverb & Echo · Pulse & Health
- Configuration — app/Fortify/session defaults
- Passkeys · Threat model & hosting · Security checklist
- Environment variables · Minimal vs full stack
- Supported databases — versions, utf8mb4, CI jobs
- Deploy with Docker — GHCR quick Compose, gated first-boot
- Installation — Herd /
composer setup/ Docker local - Files configuration —
FILES_DISK/ S3 / MinIO