Operate

Self-hosting.

One container, one SQLite file, and every environment variable.

Self-hosting

The compose file binds to 127.0.0.1:9001; put your TLS reverse proxy in front.

Quick start#

sh
git clone https://github.com/shibumistack/shibumi-forms.git
cd shibumi-forms
cp .env.example .env.production   # fill in every value
docker compose up --build -d      # or podman compose

Startup validates the whole configuration and names the missing field, so a bad deploy stops before accepting traffic.

Where data lives#

The supplied Compose setup writes the SQLite database to /data/shibumi-forms.sqlite inside the forms-data named volume. Container replacement leaves that volume in place. Removing the volume deletes the live database.

Set DATABASE_PATH to another mounted path if you manage storage yourself. Keep the database and its WAL files on local persistent storage, not an ephemeral container layer or network filesystem.

Environment variables#

VariablePurpose
PUBLIC_URLHTTPS origin the app is served from
DATABASE_PATHSQLite file path, /data/shibumi-forms.sqlite in the supplied container
SESSION_SECRET32+ bytes, generate with openssl rand -base64 48
EMAIL_PROVIDERresend in production, discard for development
RESEND_API_KEY, EMAIL_FROMResend delivers the sign-in links
TERMS_URL, PRIVACY_URL, TERMS_VERSIONYour policy documents
TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEYOptional Cloudflare Turnstile on sign-in pages
MAX_FORMS_PER_ACCOUNTForms per account, default 10
MAX_SUBMISSIONS_PER_FORMStored submissions per form, default 10,000
MAX_EMAILS_PER_DAYGlobal daily sign-in email budget, default 80
TRUSTED_PROXYloopback (default) trusts X-Forwarded-For from a local proxy; none never does
BACKUP_RETENTION_DAYSBackup retention, default 30
PORTListen port, default 9001 (above 9000). Production is set explicitly via compose.yaml/Dockerfile; host access is above 9000 through ${SHIBUMI_PORT}

The container#

  • Runs unprivileged with a read-only filesystem; only /data and /tmp are writable.
  • GET /healthz answers process liveness, GET /readyz checks SQLite.
  • Migrations run at startup before traffic is accepted.
  • Expired sessions and stale magic links are cleaned on boot and every six hours.

Reverse proxy#

Terminate TLS in front (Caddy, nginx, Traefik) and forward to the loopback port. With TRUSTED_PROXY=loopback the app reads the client IP from X-Forwarded-For only when the request arrives from loopback, which keeps IP-keyed rate limits honest.