Deployment profiles
These are the three existing deployment layouts for the same server binary.
The settings registry is settings.json; its only
required: true entries are ADMIN_API_KEY and DB_DSN
(docs/advanced/settings.json:585, docs/advanced/settings.json:3130).
Addresses, persistence and sign-in still need the profile-specific choices below.
The offline check is bash scripts/check-profiles.sh.
Hosted systemd
What runs
Workers run inside each server process; see the shared worker list below.
How settings are supplied
systemd loads the respective EnvironmentFile. Neither inspected unit exposes individual setting names or values; the env files were not opened. Each must supply ADMIN_API_KEY and DB_DSN, with different databases for the two instances. Hosted family addresses use PERSON_HOSTS=canonical and SITE_HOSTS=canonical (ARCHITECTURE.md, Deploy).
Migration ownership
Hosted: the operator owns migrations; startup migration is off.
Neither inspected unit sets MIGRATE_ON_START. Apply release SQL before installing
the binary, then record each hand-applied file with simple-host migrate -mark FILE;
alternatively run the release’s simple-host migrate against each database.
The migration command takes the advisory lock; hand-applied SQL does not acquire
that lock automatically. A fresh empty database needs db/schema.sql, or
simple-host migrate -init; ordinary startup only checks the schema
(CLAUDE.md:46, db/migrations/migrations.go:38, cmd/server/migrate.go:17,
cmd/server/main.go:119). The shared migration rules below apply.
Upgrade steps and rollback
Before changing either instance, take paired database and site-directory backups
and retain its settings/sealing keys. Build under the memory cap, apply schema
changes to each target database, back up the installed binary, install and restart
the target services. The installed path is shared, so the next restart of either
service uses that binary (CLAUDE.md:62, unit ExecStart lines above).
Rollback installs the saved .bak-<timestamp> binary and restarts the affected
services; it does not undo SQL. See the shared compatibility limits below.
Verify /healthz, /readyz, the changed route and site/person/legacy addresses
from a client, check neighbouring sites and startup logs, and run
/usr/local/bin/simple-host version (CLAUDE.md:93, CLAUDE.md:98,
internal/handler/health.go:14).
Docker Compose small box
What runs
The root compose.yaml runs Postgres 16, one server container and Caddy.
The server image is the nonroot Go binary, HTTP 8090, DATA_DIR=/data/sites;
Caddy publishes 80/443 by default. Named volumes hold the database, sites,
Caddy logs, certificates and config. Caddy proxies APIs and can serve site files
from its read-only sites mount (compose.yaml:14, compose.yaml:37,
compose.yaml:277, Dockerfile:31, deploy/compose/Caddyfile:86).
Edition comes from settings: root Compose passes EVENTS through.
Workers run inside the server process, as listed below.
The entrypoint sets EVENTS=hosted. Its /data volume holds sites, logs and Caddy state; the server listens on 127.0.0.1:8090, Caddy on 8081/8443 with health on 8080. Default published ports are 80/443 and loopback 18080.
How settings are supplied
Compose substitutes .env/shell values into explicit container environment entries. Root Compose requires DB_PASSWORD and ADMIN_API_KEY, constructs DB_DSN, and passes SITE_DOMAIN and PUBLIC_BASE_URL. Blank SITE_DOMAIN deliberately enters browser setup; configure the real domain/base URL before normal use. Dockerfile supplies the persistent DATA_DIR and port (compose.yaml:19, compose.yaml:40, Dockerfile:37). Standalone also requires PASSCODE_ENC_KEY, ANALYTICS_SALT and SITE_DOMAIN; it derives PUBLIC_BASE_URL and enables password sign-in by default. With password sign-in off, it requires either complete OIDC settings or RESEND_API_KEY plus MAIL_FROM for sign-in.
Migration ownership
Compose: the installer migrates before starting the new app. The root installer pulls the image, starts the database, runs docker compose run --rm -T app migrate, then starts the app. Failure leaves the app unrestarted. For an empty database volume, Postgres loads the mounted schema.sql first; migrate then applies/records pending files under the advisory lock (deploy/install/install.sh:273, compose.yaml:22). Plain compose up on an existing database does not do that upgrade step by itself. Standalone’s installer stops the app and runs release before starting it; Compose also gates app startup on successful release. Its entrypoint loads schema.sql with psql when public.users is absent, then executes migrate.
Upgrade steps and rollback
Take paired DB/files backups and keep .env and sealing keys. Re-run the versioned installer; root Compose uses the published image, not a local build. SQL stays applied, subject to the compatibility limits below. If restoring data, restore the DB and sites volume together (standalone: DB and /data). The database records live versions while files hold those versions and runtime SQLite/uploads (ARCHITECTURE.md, Data model; compose.yaml:290). Check public HTTPS, /healthz, /readyz, a deployed site and logs; check the actual image’s release with docker compose exec app simple-host version (deploy/install/install.sh:293, cmd/server/migrate.go:148).
Kubernetes
What runs
HTTP is 8090; TLS ends upstream. One replica and Recreate stop the old pod before the new one uses the ReadWriteOnce volume. /data/sites and /data/geoip are on /data; /tmp is an emptyDir.
How settings are supplied
envFrom loads a ConfigMap and Secret. Values set SITE_DOMAIN, PUBLIC_BASE_URL, EVENTS=hosted, persistence paths and port; extraConfig and extraSecrets add registry names. Set host, a database DSN (or existing Secret), and password sign-in (on by default), OIDC credentials or email sign-in credentials. ADMIN_API_KEY is generated and retained by Helm, or supplied in your existing Secret; DB_DSN is supplied by you.
Migration ownership
Kubernetes: the server migrates before listening. The default migrateOnStart: true renders MIGRATE_ON_START=true. On every start, the server waits for Postgres, initializes schema.sql if public.users is absent, applies pending files under the advisory lock, and checks the schema. The database role must own the database.
Upgrade steps and rollback
Back up Postgres and the /data volume as a pair, preferably with the pod scaled to zero, and preserve the Secret, especially PASSCODE_ENC_KEY. Change the image tag with helm upgrade (or update/apply rendered YAML), wait for rollout and inspect logs. Recreate means downtime during migration/startup. Use helm rollback or restore the previous manifest/image for binary rollback; SQL stays applied. Earlier successful migration files remain committed if a later file fails. Restore DB plus volume together if data rollback is needed (docs/kubernetes.md:152, docs/kubernetes.md:187, db/migrations/migrations.go:20). Check external /healthz, /readyz, a team site, and kubectl exec the pod’s /usr/local/bin/simple-host version.
Shared worker and migration details
Workers are goroutines in the server, not separate queue services in any profile. Conditions/settings can leave workers inactive; setup mode returns before normal worker wiring (:155).
Migrations are forward-only: there is no down command. New files must be
NNNN-name.sql, next free number, plain idempotent SQL, additive and safe on a
live DB. The runner uses the frozen legacy order then numbered files, skips
historical baseline files, and commits each file with its schema_migrations row
under a per-database Postgres advisory lock (db/migrations/migrations.go:4,
db/migrations/migrations.go:302). Init uses that same lock; manual SQL does not.
An older binary is intended to work on the newer additive schema, but startup does not prove full backwards compatibility. It does not compare schema versions, reject extra columns/newer migration rows, or check column types or index definitions. Missing requirements abort startup; extra schema is accepted (cmd/server/main.go:131, internal/db/schemacheck.go:170, internal/db/schemacheck.go:196). /healthz means HTTP is answering; /readyz also pings Postgres (internal/handler/health.go:19). The version command needs no database and prints release/commit plus embedded migration information (cmd/server/migrate.go:148).
| Choice | Hosted systemd | Compose small box | Kubernetes |
|---|---|---|---|
| Server | Two services, shared binary | One app; standalone bundles Caddy | One pod, Recreate |
| Settings | Separate env files | .env substitution, explicit environment | ConfigMap + Secret from values |
| Database | Separate DB per instance | Bundled Postgres 16 | Operator’s Postgres 16 |
| Files | Separate /srv site trees | Named sites or data volume | /data persistent volume |
| Migrations | Operator before restart | Installer/release before app | Server on start by default |
| Empty DB | schema.sql or migrate -init | Postgres init or standalone release | Server migrate -init path |
| Rollback | Saved binary + restart | Previous image/package | Helm rollback/previous image |
| Restore | Paired DB + site tree + keys | Paired DB + files volume + keys | Paired DB + /data + Secret |
Password sign-in by profile
Hosted systemd: leave PASSWORD_SIGNIN=off (binary default); existing email and provider sign-ins continue. No mail or identity provider is required for password sign-in. Kubernetes: signIn.password.enabled: true and signIn.password.selfSignup: true render both settings as on; setting either false renders off. Password sign-in satisfies the chart’s sign-in requirement without OIDC or Resend. An operator may set PASSWORD_SELF_SIGNUP=off and create/reset accounts through Admin Users. People must change temporary passwords before completing sign-in. Existing email accounts can receive passwords by admin reset; existing keys and sessions continue to work.