Skip to content

Latest commit

 

History

History
253 lines (172 loc) · 8.57 KB

vps.md

File metadata and controls

253 lines (172 loc) · 8.57 KB

VPS Deployment

This guide covers Docker Compose installs on a single VPS.

The VPS stack uses the same production image as managed platforms. Compose only supplies local persistence, an optional bundled Postgres service, and an optional Caddy TLS proxy.


TL;DR

Mode Source-build command Containers Persistent volumes
SQLite docker compose -f compose.prod.yml -f compose.sqlite.yml -f compose.build.yml up -d --build app data, uploads
Postgres docker compose -f compose.prod.yml -f compose.build.yml up -d --build app, postgres postgres_data, uploads
SQLite + TLS docker compose -f compose.prod.yml -f compose.sqlite.yml -f compose.tls.yml -f compose.build.yml up -d --build app, caddy data, uploads, caddy_data
Postgres + TLS docker compose -f compose.prod.yml -f compose.tls.yml -f compose.build.yml up -d --build app, postgres, caddy postgres_data, uploads, caddy_data

SQLite is the default for most single-site installs. Postgres is for multiple simultaneous admin writers, horizontal app scale, or operators who already want Postgres.

When using a published image, set INSTATIC_IMAGE and omit compose.build.yml plus --build. Before adding AI provider credentials, saving plugin secret settings, or enabling TOTP MFA in production, set INSTATIC_SECRET_KEY to the output of bun run scripts/generate-secret-key.ts.

Install From A Release Bundle

  1. Download instatic-<version>-release-bundle.tar.gz from the GitHub Release.
  2. Unpack it on the server.
  3. Choose SQLite or Postgres.

SQLite:

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

Postgres:

cp .env.production.example .env
# Set POSTGRES_PASSWORD and INSTATIC_SECRET_KEY in .env.
INSTATIC_IMAGE=ghcr.io/corebunch/instatic:<version> docker compose -f compose.prod.yml up -d

Prerequisites

Install Docker Engine and Docker Compose on the VPS. If using TLS, point a domain's DNS A/AAAA records at the server and open ports 80 and 443.

Install Files

Use a source checkout:

git clone https://github.com/CoreBunch/Instatic.git
cd instatic

For plain SQLite without TLS, source builds use compose.prod.yml, compose.sqlite.yml, and compose.build.yml. Image-pull installs use only compose.prod.yml and compose.sqlite.yml, but they still need the Compose files from a checkout or release bundle.

SQLite Install

For reversible server secrets such as AI credentials, plugin secret settings, and TOTP MFA seeds, copy the env template and set INSTATIC_SECRET_KEY first:

cp .env.production.example .env
bun run scripts/generate-secret-key.ts

Paste the printed key into .env as INSTATIC_SECRET_KEY.

Run:

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

This starts one app container. compose.sqlite.yml disables the Postgres service and sets:

DATABASE_URL=sqlite:/app/data/cms.db

Persistent data:

Volume Mount path Contents
data /app/data SQLite database
uploads /app/uploads Media, fonts, plugins, published artefacts

Open:

http://server-ip:3001/admin

The first visit creates the site and admin account.

Postgres Install

Copy the env template:

cp .env.production.example .env

Edit .env and set a real password:

POSTGRES_PASSWORD=replace-with-a-long-random-password
INSTATIC_SECRET_KEY=replace-with-output-of-generate-secret-key

Generate one with:

openssl rand -hex 24

Start the stack:

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

This starts app and postgres. compose.prod.yml sets the app's DATABASE_URL to the bundled Postgres service:

postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}

Persistent data:

Volume Mount path Contents
postgres_data /var/lib/postgresql/data Postgres data directory
uploads /app/uploads Media, fonts, plugins, published artefacts

HTTPS

Add compose.tls.yml when the VPS has a public domain. Set these in .env:

DOMAIN=cms.example.com
LETSENCRYPT_EMAIL=ops@example.com
PUBLIC_ORIGIN=https://cms.example.com

Caddy terminates TLS and forwards plain HTTP to the container, so the container's own request URL is http://app:3001. PUBLIC_ORIGIN is how Instatic knows the real public origin for its CSRF check — set it to https:// plus your DOMAIN. (Leave PUBLIC_ORIGIN unset only for plain-HTTP installs with no proxy in front.)

Run SQLite + TLS:

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

Run Postgres + TLS:

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

See tls-caddy.md for the Caddy details.

Operations

Check status:

docker compose -f compose.prod.yml ps
curl http://localhost:3001/health

View logs:

docker compose -f compose.prod.yml logs -f app
docker compose -f compose.prod.yml logs -f postgres   # Postgres installs only

Update a source-build install:

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

For Postgres source-build installs, omit compose.sqlite.yml:

git pull
docker compose -f compose.prod.yml -f compose.build.yml up -d --build

Update an image-pull install:

docker compose -f compose.prod.yml pull app
docker compose -f compose.prod.yml up -d

SQLite image-pull installs include the SQLite override:

docker compose -f compose.prod.yml -f compose.sqlite.yml pull app
docker compose -f compose.prod.yml -f compose.sqlite.yml up -d

Without Docker (Direct Bun Install)

This is the one install path that uses your own Bun. Keep it inside engines.bun in package.json (>=1.4.0 <1.5.0 for this release): the Docker image and the Desktop server bundle carry that Bun themselves, and it is the only one releases are built and tested on. Bun does not enforce the range, so the server checks it at boot (server/bunVersion.ts) and logs [server] Bun <version> is outside the supported range … before continuing; bun run dev refuses a Bun older than 1.4.1 outright, because its Vite proxy needs that release to forward the editor's WebSocket. bun upgrade moves an older install forward.

The CMS runs directly on the host without Docker. From a source checkout:

bun install
bun run build
DATABASE_URL=sqlite:./data/cms.db \
  STATIC_DIR=./dist \
  UPLOADS_DIR=./uploads \
  INSTATIC_SECRET_KEY=replace-with-output-of-generate-secret-key \
  TRUSTED_PROXY_CIDRS=127.0.0.1/32,::1/128 \
  PORT=3001 \
  bun run server/index.ts

Replace DATABASE_URL with a Postgres connection string for Postgres mode. STATIC_DIR must point at the built admin SPA (dist/ after bun run build).

Update a direct install by pulling, reinstalling dependencies, rebuilding, and restarting the process. bun install is not optional: a release can add a server dependency (0.0.18 added jsdom for the richtext sanitizer), and without it the server exits on startup with Cannot find module.

git pull
bun install --frozen-lockfile
bun run build
# then restart the bun process under your supervisor

Wrap the command in a process supervisor (systemd, pm2, supervisord) for auto-restart on crash and on server boot. Put an HTTPS-capable reverse proxy (Caddy, Nginx, Cloudflare Tunnel) in front for TLS, and set PUBLIC_ORIGIN=https://your-domain so the CSRF origin check matches the public URL even though the proxy hands the Bun process plain HTTP. TRUSTED_PROXY_CIDRS is independent of CSRF: set it to the proxy's source CIDR only if you want real client IPs in audit logs and rate-limit keys, and leave it empty if the app is directly exposed.

Data Safety

docker compose down stops containers and keeps named volumes.

docker compose down -v deletes named volumes. For Instatic that means deleting the CMS database and uploaded media. Use it only when intentionally wiping the install.

Backups are covered in backup-restore.md.

Related