Skip to content
SecretVariable

Self-hosting

Run SecretVariable on your own server with Docker Compose, HTTPS, backups and email.

Updated

SecretVariable is one container (the API and the web app together) plus PostgreSQL. You run both, you hold the encryption key, and your secrets never leave your infrastructure.

Requirements#

  • Docker with Compose, or Node.js 20+ and PostgreSQL 14+.
  • A domain name and a TLS-terminating reverse proxy for anything beyond local testing.

Master key#

$openssl rand -hex 32

The master key wraps the encryption key of every project. Without it the stored data cannot be decrypted, and nobody, including the maintainers, can recover it. Keep it in a password manager or secret store, separate from your database backups. Rotating it is not automated yet.

Docker Compose#

$git clone https://github.com/OxygenOP/SecretVariable.git && cd SecretVariable$cp .env.example .env        # then edit .env: SV_MASTER_KEY, POSTGRES_PASSWORD$docker compose up -d$docker compose logs -f app

Migrations run automatically at start. Open http://localhost:8080. The first account you register owns its own workspace; invite others to projects from the web app.

Without Docker#

$pnpm install && pnpm build$export DATABASE_URL=postgres://user:pass@localhost:5432/secretvariable$export SV_MASTER_KEY=<64 hex characters>$export SV_WEB_DIR=$PWD/apps/web/dist$node apps/server/dist/index.js

Configuration#

Everything is an environment variable.

VariableDefaultMeaning
DATABASE_URLrequiredPostgreSQL connection string
SV_MASTER_KEYrequired32 bytes as 64 hex characters (or base64)
PORT / HOST8080 / 0.0.0.0Listen address
SV_WEB_DIRunsetFolder with the built web app (set in the Docker image)
SV_ALLOW_REGISTRATIONtrueSet false to close public sign-up once your team has accounts. Invited people must already have an account, so create theirs first or re-open registration briefly
SV_SESSION_TTL_DAYS30Web sign-in lifetime
SV_AUTH_RATE_LIMIT20Login and register attempts per minute per IP
SV_LOGtrueRequest logging (the Authorization header is redacted)
SV_SMTP_URLunsetSMTP server for sign-in codes and invitation emails. Unset means no email: email codes are hidden and invitations are links you share yourself
SV_MAIL_FROMSecretVariable <no-reply@localhost>The From address for emails
SV_PUBLIC_URLunsetThe address people open, such as https://secrets.example.com. Used for the link in invitation emails

Email#

Email is optional. It is used for two things: one-time sign-in codes as a backup for two-step verification, and invitations, which an admin can have the server email to the invitee instead of copying the link to them. Set SV_SMTP_URL (for example smtps://user:pass@smtp.example.com:465 or smtp://user:pass@host:587), SV_MAIL_FROM and SV_PUBLIC_URL, and use your provider's SMTP credentials (Postmark, SES, Mailgun, your own relay). Make sure the From domain has SPF and DKIM so messages are not marked as spam.

SV_PUBLIC_URL is the address people open, such as https://secrets.example.com. It is the address used in the link inside invitation emails. If you leave it unset, the link uses the address of the admin's browser, which is fine behind a single address but can be wrong behind some proxies.

Without SMTP, invitations are links you share yourself. With it, the invite dialog gets an Email the invitation option. An invitation link works once, only for the invited email address, and expires, so a message that ends up in the wrong inbox is of little use to anyone else. The invitation is still created if the mail server is down, and the app tells you so you can send the link by hand.

HTTPS and a reverse proxy#

The app trusts X-Forwarded-* headers (for client IPs in the audit log and rate limits), so expose only the proxy, never the app port. With a proxy on the same host, change the app's ports in docker-compose.yml to "127.0.0.1:8080:8080".

Caddy (automatic certificates)#

Caddyfile
secrets.example.com {    reverse_proxy app:8080}

nginx#

nginx.conf
server {    listen 443 ssl http2;    server_name secrets.example.com;    # ssl_certificate ...;    location / {        proxy_pass http://127.0.0.1:8080;        proxy_set_header Host $host;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto $scheme;    }}

Backups#

You need both of these, kept separately:

  1. The database: docker compose exec db pg_dump -U secretvariable secretvariable > backup.sql
  2. The master key.

To restore, start a fresh stack with the same SV_MASTER_KEY, then load the dump with psql. Test a restore before you rely on it.

Upgrades#

$git pull$docker compose build$docker compose up -d       # migrations run on start

Back up first. The changelog lists notable changes.

Hardening checklist#

  • Terminate TLS in front of the app and never expose port 8080 directly.
  • Back the master key up separately from database backups.
  • Set SV_ALLOW_REGISTRATION=false after onboarding.
  • Turn on two-step verification for everyone, and keep recovery codes safe.
  • Use read-only, environment-scoped, expiring service tokens in CI.
  • Limit who is an organisation owner or admin: they can see every project in the organisation.
  • Keep PostgreSQL private and patched, and keep the image updated.
  • Review the audit log (project > Audit log, organisation > Activity).

Health check: GET /api/health. Logs go to stdout and never contain secret values. Found a vulnerability? See the security policy. Next: the CLI guide.