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 32The 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 appMigrations 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.jsConfiguration#
Everything is an environment variable.
| Variable | Default | Meaning |
|---|---|---|
DATABASE_URL | required | PostgreSQL connection string |
SV_MASTER_KEY | required | 32 bytes as 64 hex characters (or base64) |
PORT / HOST | 8080 / 0.0.0.0 | Listen address |
SV_WEB_DIR | unset | Folder with the built web app (set in the Docker image) |
SV_ALLOW_REGISTRATION | true | Set 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_DAYS | 30 | Web sign-in lifetime |
SV_AUTH_RATE_LIMIT | 20 | Login and register attempts per minute per IP |
SV_LOG | true | Request logging (the Authorization header is redacted) |
SV_SMTP_URL | unset | SMTP 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_FROM | SecretVariable <no-reply@localhost> | The From address for emails |
SV_PUBLIC_URL | unset | The 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)#
secrets.example.com { reverse_proxy app:8080}nginx#
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:
- The database:
docker compose exec db pg_dump -U secretvariable secretvariable > backup.sql - 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 startBack 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=falseafter 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.