Deployment

July 24, 2026 · View on GitHub

This index maps supported deployment targets to the files, variables, and persistence rules they need.

Instatic is one Bun server packaged by the root Dockerfile. The server reads runtime configuration from server/config.ts: PORT, DATABASE_URL, UPLOADS_DIR, STATIC_DIR, PUBLIC_ORIGIN, and TRUSTED_PROXY_CIDRS. Reversible server secrets, including AI provider credentials, plugin secret settings, and MFA TOTP seeds, are encrypted with INSTATIC_SECRET_KEY when configured. Database migrations run automatically on boot in server/index.ts.


TL;DR

TargetUse whenDatabasePersistent storageDocs
Railway SQLite templateFastest managed install for a single siteSQLite fileOne Railway app volume mounted at /app/storagerailway.md
Railway Postgres templateManaged install for teams or horizontal scale laterRailway PostgresApp volume for uploads, Postgres service volume for DBrailway.md
Render SQLite templateManaged Docker install outside RailwaySQLite fileOne Render disk mounted at /app/storagerender.md
Render Postgres templateManaged Postgres install outside RailwayRender PostgresRender disk for uploads, Render Postgres storage for DBrender.md
VPS Docker ComposeSelf-hosted server, full controlSQLite or bundled PostgresDocker named volumesvps.md
Generic Docker hostAny platform that runs the Dockerfile/imageSQLite or external PostgresA mounted directory/volume for DB/uploadsdocker-image.md
VPS HTTPSPublic domain on a VPSUnchangedCaddy cert volume plus app volumestls-caddy.md

Back up both the database and uploaded media. See backup-restore.md.

Runtime Contract

Every deployment target configures the same process:

PORT          HTTP port the Bun server listens on
DATABASE_URL  sqlite:/path/to/cms.db, file:/path/to/cms.db, postgres://..., or postgresql://...
UPLOADS_DIR   directory for media, plugin packs, fonts, and published disk artefacts
STATIC_DIR    built admin SPA directory; /app/dist in the Docker image
INSTATIC_SECRET_KEY  base64 32-byte key for encrypted server secrets
PUBLIC_ORIGIN        comma-separated public origin(s) the CSRF check trusts; auto-detected from RENDER_EXTERNAL_URL / RAILWAY_PUBLIC_DOMAIN on those platforms
TRUSTED_PROXY_CIDRS  optional; trusts proxy socket peers for forwarded client-IP attribution only (audit logs, rate-limit keys) — NOT used for CSRF

Generate INSTATIC_SECRET_KEY with bun run scripts/generate-secret-key.ts before adding Anthropic, OpenAI, or OpenRouter credentials or enabling TOTP MFA in production. Without it, the admin can load but saving reversible secrets fails because there is no stable encryption key.

The Docker image sets:

PORT=3001
STATIC_DIR=/app/dist
UPLOADS_DIR=/app/uploads

Managed platforms often override PORT. That is fine; the server uses process.env.PORT. When a managed platform terminates HTTPS before forwarding HTTP to the container, the CSRF origin check derives the site's public origin from PUBLIC_ORIGIN — auto-detected from RENDER_EXTERNAL_URL / RAILWAY_PUBLIC_DOMAIN on Render and Railway, so one-click deploys need no manual value. Set PUBLIC_ORIGIN explicitly (a comma-separated list) when adding a custom domain. TRUSTED_PROXY_CIDRS is independent of CSRF and only attributes the real client IP for audit logs and rate-limit keys.

Image Availability

Release bundles plus the published GHCR image are the default portable install path:

INSTATIC_IMAGE=ghcr.io/corebunch/instatic:latest docker compose -f compose.prod.yml -f compose.sqlite.yml up -d

Pin a semver tag for predictable upgrades:

INSTATIC_IMAGE=ghcr.io/corebunch/instatic:0.0.13 docker compose -f compose.prod.yml -f compose.sqlite.yml up -d

Source builds remain supported for contributors and release-candidate testing:

docker compose -f compose.prod.yml -f compose.sqlite.yml -f compose.build.yml up -d --build

The maintainer release target is ghcr.io/corebunch/instatic, documented in release-workflow.md.

Database Choice

The database engine is selected only by DATABASE_URL:

URL shapeEngine
sqlite:/path/to/cms.dbSQLite
file:/path/to/cms.dbSQLite
/path/to/cms.dbSQLite
postgres://...Postgres
postgresql://...Postgres

SQLite is the default for single-site installs. Postgres is for multiple simultaneous admin writers, more than one app container, or operators who already want managed Postgres.

Persistence Rules

UPLOADS_DIR is required for durable media regardless of the database engine. It stores:

  • uploaded media originals and variants
  • uploaded fonts
  • plugin packages and module packs
  • published static artefacts under published/current

SQLite installs also need the SQLite database file on persistent storage. On platforms with only one app volume, put both the SQLite file and uploads under the same mounted root.

Docs Inventory

FileRole
railway.mdRailway templates for SQLite and Postgres
render.mdRender Blueprint templates for SQLite and Postgres
vps.mdDocker Compose on a VPS, both SQLite and Postgres
docker-image.mdGeneric Docker image contract and docker run examples
tls-caddy.mdCaddy TLS overlay for VPS Compose installs
backup-restore.mdDatabase and uploads backup/restore
release-workflow.mdMaintainer image publishing workflow
  • server/config.ts — runtime env parsing
  • server/db/index.ts — database URL detection
  • server/index.ts — migrations, media storage, and server boot
  • Dockerfile — production image contract
  • compose.prod.yml, compose.sqlite.yml, compose.tls.yml, compose.build.yml — VPS Compose files
  • docs/deployment/render/sqlite/render.yaml, docs/deployment/render/postgres/render.yaml — Render Blueprint templates