Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docker-compose.coolify.sqlite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# docker-compose.coolify.sqlite.yml
# Instatic on Coolify — single container, SQLite.
#
# The simplest Coolify deployment: one service, two volumes, no database server.
# Pick this for a single-site install with one or two admins. For multiple
# simultaneous editors or scheduled database backups, use the Postgres stack in
# docker-compose.coolify.yml instead. Full trade-offs and the setup walkthrough:
# docs/deployment/coolify.md.
#
# Coolify setup:
# Resource: Docker Compose (Git repository, or Empty + paste this file)
# Compose file: docker-compose.coolify.sqlite.yml (extension must match exactly)
# Base directory: /
# Domain: set it on the `instatic` service. Coolify issues the TLS
# certificate and routes :443 to container port 3001.
#
# The header comments in docker-compose.coolify.yml explain why this file has no
# `networks:`, `ports:`, `container_name:`, or `restart:` keys.

services:
instatic:
image: ${INSTATIC_IMAGE:-ghcr.io/corebunch/instatic:latest}
environment:
# Declares the public route. Coolify assigns this service a domain and
# points Traefik at container port 3001.
- SERVICE_URL_INSTATIC_3001
- PORT=3001
- DATABASE_URL=sqlite:/app/data/cms.db
- UPLOADS_DIR=/app/uploads
- STATIC_DIR=/app/dist
# See docker-compose.coolify.yml for why this is mandatory: without it the
# session cookie loses Secure, the CSRF check compares the wrong origin,
# real-time co-editing is rejected, and MCP connectors read local-only.
- PUBLIC_ORIGIN=${SERVICE_URL_INSTATIC}
# Required — the image runs NODE_ENV=production, where boot fails if this
# is unset. Coolify generates it once and keeps it stable across
# redeploys; changing it strands every already-encrypted secret.
- INSTATIC_SECRET_KEY=${SERVICE_REALBASE64_32_INSTATIC}
# Audit-log and rate-limit client-IP attribution only, not CSRF.
- TRUSTED_PROXY_CIDRS=${TRUSTED_PROXY_CIDRS:-172.16.0.0/12}
volumes:
# Two volumes rather than one shared root, and specifically at these two
# paths: the Dockerfile creates /app/uploads and /app/data and chowns them
# to `bun` before dropping to USER bun. A volume mounted at a path the
# image does NOT contain (e.g. the /app/storage layout the Railway and
# Render templates use) is created root-owned, and every write from the
# non-root container fails with EACCES. Railway works around that with
# RAILWAY_RUN_UID=0; Docker named volumes need no such workaround as long
# as they mount where the image already prepared the directory.
- instatic-uploads:/app/uploads
- instatic-data:/app/data
healthcheck:
# The oven/bun base image ships neither curl nor wget — see
# server/healthcheck.ts. Shorter start_period than the Postgres stack:
# there is no database server to wait on, only migrations.
test: ['CMD', 'bun', 'run', 'server/healthcheck.ts']
interval: 30s
timeout: 5s
start_period: 30s
retries: 5

volumes:
instatic-uploads:
instatic-data:
102 changes: 102 additions & 0 deletions docker-compose.coolify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# docker-compose.coolify.yml
# Instatic on Coolify — bundled Postgres.
#
# Coolify treats one Compose file as the single source of truth: there are no
# `-f a.yml -f b.yml` overlays, so this file is standalone rather than a layer on
# top of compose.prod.yml. For the single-container SQLite stack, use
# docker-compose.coolify.sqlite.yml instead. Trade-offs between the two, plus the
# full setup walkthrough: docs/deployment/coolify.md.
#
# Coolify setup:
# Resource: Docker Compose (Git repository, or Empty + paste this file)
# Compose file: docker-compose.coolify.yml (the extension must match exactly)
# Base directory: /
# Domain: set it on the `instatic` service. Coolify issues the TLS
# certificate and routes :443 to container port 3001.
#
# Conventions this file deliberately follows:
# - No `networks:`. Coolify creates an isolated bridge network per stack.
# Declaring one puts containers on two networks at once and makes Traefik
# route non-deterministically — Coolify documents this as a cause of
# intermittent HTTPS outages.
# - No `ports:`. Publishing a host port bypasses the proxy and would expose
# both the app and Postgres directly on the VPS. Routing comes solely from
# the SERVICE_URL_INSTATIC_3001 magic variable below.
# - No `container_name:`, `restart:`, or top-level `name:`. Coolify owns all
# three and renames services with a UUID suffix.

services:
instatic:
# Service name matters: it becomes the auto-generated subdomain, and the
# SERVICE_URL_INSTATIC_* variables below are keyed to it.
image: ${INSTATIC_IMAGE:-ghcr.io/corebunch/instatic:latest}
environment:
# Declares the public route. Coolify assigns this service a domain and
# points Traefik at container port 3001.
- SERVICE_URL_INSTATIC_3001
- PORT=3001
- DATABASE_URL=postgres://${POSTGRES_USER:-instatic}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-instatic}
- UPLOADS_DIR=/app/uploads
- STATIC_DIR=/app/dist
# The one setting that must not be omitted. Coolify's Traefik terminates
# TLS and forwards plain HTTP, and server/auth/security.ts deliberately
# never trusts X-Forwarded-Proto/Host. Without this the server believes it
# is http:// and four things break or silently degrade: the session cookie
# loses its Secure flag, the CSRF origin check compares the wrong origin,
# the real-time co-editing WebSocket is rejected by the same origin guard,
# and MCP connector URLs resolve local-only instead of public-https.
# ${SERVICE_URL_INSTATIC} resolves to the domain assigned above, scheme
# included, and follows a custom domain set later in the UI.
- PUBLIC_ORIGIN=${SERVICE_URL_INSTATIC}
# Base64 32-byte AES key for reversible server secrets (AI provider
# credentials, plugin secret settings, TOTP seeds). The image runs with
# NODE_ENV=production, where boot FAILS outright if this is unset.
# REALBASE64_32 produces base64 of 32 random bytes — exactly the shape
# server/secrets/masterKey.ts validates. Coolify generates it once and
# keeps it stable across redeploys; changing it strands every value
# already encrypted under the old key.
- INSTATIC_SECRET_KEY=${SERVICE_REALBASE64_32_INSTATIC}
# Client-IP attribution for audit logs and rate-limit keys only — NOT
# CSRF, which is driven entirely by PUBLIC_ORIGIN. Trusting the Docker
# bridge range is safe here precisely because no host port is published,
# so only Coolify's proxy can reach this container.
- TRUSTED_PROXY_CIDRS=${TRUSTED_PROXY_CIDRS:-172.16.0.0/12}
volumes:
- instatic-uploads:/app/uploads
depends_on:
postgres:
condition: service_healthy
healthcheck:
# The oven/bun base image ships neither curl nor wget, so the probe is a
# Bun script that fetches GET /health — see server/healthcheck.ts.
#
# start_period is longer than compose.prod.yml's 20s because Coolify gates
# both the deploy and Traefik routing on this check, and the first boot
# runs every migration against an empty database before /health answers.
test: ['CMD', 'bun', 'run', 'server/healthcheck.ts']
interval: 30s
timeout: 5s
start_period: 60s
retries: 5

postgres:
# Pinned to the same major as compose.prod.yml so the two stacks stay
# data-compatible — a dump from one restores into the other.
image: postgres:16
environment:
- POSTGRES_DB=${POSTGRES_DB:-instatic}
- POSTGRES_USER=${POSTGRES_USER:-instatic}
# Coolify generates this once and persists it. It is symbol-free, so it is
# safe to embed unescaped in the DATABASE_URL above.
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
volumes:
- instatic-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 10s
timeout: 5s
retries: 10

volumes:
instatic-uploads:
instatic-postgres-data:
3 changes: 3 additions & 0 deletions docs/deployment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Instatic is one Bun server packaged by the root `Dockerfile`. The server reads r
| Render SQLite template | Managed Docker install outside Railway | SQLite file | One Render disk mounted at `/app/storage` | [render.md](render.md) |
| Render Postgres template | Managed Postgres install outside Railway | Render Postgres | Render disk for uploads, Render Postgres storage for DB | [render.md](render.md) |
| VPS Docker Compose | Self-hosted server, full control | SQLite or bundled Postgres | Docker named volumes | [vps.md](vps.md) |
| Coolify | Self-hosted PaaS with managed TLS and deploys | SQLite or bundled Postgres | Docker named volumes | [coolify.md](coolify.md) |
| Generic Docker host | Any platform that runs the Dockerfile/image | SQLite or external Postgres | A mounted directory/volume for DB/uploads | [docker-image.md](docker-image.md) |
| VPS HTTPS | Public domain on a VPS | Unchanged | Caddy cert volume plus app volumes | [tls-caddy.md](tls-caddy.md) |

Expand Down Expand Up @@ -100,6 +101,7 @@ SQLite installs also need the SQLite database file on persistent storage. On pla
| [railway.md](railway.md) | Railway templates for SQLite and Postgres |
| [render.md](render.md) | Render Blueprint templates for SQLite and Postgres |
| [vps.md](vps.md) | Docker Compose on a VPS, both SQLite and Postgres |
| [coolify.md](coolify.md) | Coolify Compose templates for SQLite and Postgres |
| [docker-image.md](docker-image.md) | Generic Docker image contract and `docker run` examples |
| [tls-caddy.md](tls-caddy.md) | Caddy TLS overlay for VPS Compose installs |
| [backup-restore.md](backup-restore.md) | Database and uploads backup/restore |
Expand All @@ -112,4 +114,5 @@ SQLite installs also need the SQLite database file on persistent storage. On pla
- `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
- `docker-compose.coolify.yml`, `docker-compose.coolify.sqlite.yml` — Coolify Compose templates
- `docs/deployment/render/sqlite/render.yaml`, `docs/deployment/render/postgres/render.yaml` — Render Blueprint templates
Loading