Requirements
- Docker with Compose v2, on Linux, macOS or Windows
- Internet access to pull the public image
ghcr.io/clane-ai/clane-vault(no registry login needed) - A Postgres 14 or later database, or the bundled Postgres that the installer can set up
- An HTTPS reverse proxy (nginx, Caddy, Traefik or a cloud load balancer) with a certificate for your hostname
The container keeps no state of its own, so it needs no volume unless you write backups to a local folder. Managed Postgres works too, including Supabase, Azure and Amazon RDS.
The install bundle
The bundle is a small folder: docker-compose.yml, .env.example, install and update scripts for Linux, macOS and Windows, and the full install guide. It is the deploy/client folder on GitHub; download it with the repository. This is the compose file:
name: clane-vault
services:
clane-vault:
image: ${CLANE_VAULT_IMAGE:-ghcr.io/clane-ai/clane-vault}:${CLANE_VAULT_VERSION:-1.6.2}
env_file: .env
ports: ["${HTTP_BIND:-127.0.0.1}:${HTTP_PORT:-8080}:3001"]
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
required: false # ignored when you use your own Postgres (bundled-db profile off)
# Optional bundled database. Only started when the bundled-db profile is enabled
# (install.sh sets COMPOSE_PROFILES=bundled-db in .env, or run: docker compose --profile bundled-db up -d).
# If you use your own Postgres, set DATABASE_URL in .env and do not enable this profile.
postgres:
profiles: ["bundled-db"]
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: vault
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-}
POSTGRES_DB: vault
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vault -d vault"]
interval: 5s
timeout: 5s
retries: 20
volumes:
pgdata:
Install
Run the installer from the bundle folder.
Linux or macOS./install.sh
Windows (PowerShell)
.\install.ps1
The installer:
- creates
.envwith random secrets if it doesn't exist yet (it never overwrites one) - asks whether to use the bundled Postgres, and for the public address people will open
- pulls the image, starts it and waits until
/api/v1/healthanswers
To use your own Postgres, answer n and enter its connection string. The database user needs to be able to create tables, because migrations run automatically every time the container starts.
Then check .env, in particular these settings:
CLANE_VAULT_VERSION=1.6.2
HTTP_BIND=127.0.0.1
HTTP_PORT=8080
DATABASE_URL=postgresql://user:password@host:5432/dbname
# The https address people open. No trailing slash.
PUBLIC_URL=https://vault.example.com
# Number of reverse proxies between the internet and the container.
TRUST_PROXY=1
COOKIE_SECURE=true
Keep a copy of .env in your secrets manager. SERVER_SECRET encrypts everyone's authenticator secrets. If it is lost or changed, nobody can sign in with their authenticator code until an admin resets their two-step sign-in. Never commit or share the file.
Create the first admin
docker compose exec clane-vault node dist/src/scripts/bootstrap.js --email [email protected]
This prints a one-time invite link that works for 48 hours. Open it, set a master password and set up an authenticator app. That account owns the main organisation and is the instance admin. The command refuses to run if an admin already exists; invite everyone else from the app.
To add or remove an instance admin later, or give an organisation a new owner:
docker compose exec clane-vault node dist/src/scripts/instance-admin.js --email [email protected] --grant # or --revoke
docker compose exec clane-vault node dist/src/scripts/org-owner.js --email [email protected] [--org <slug>]
Put an HTTPS proxy in front
The container speaks plain HTTP on 127.0.0.1:8080. Put your HTTPS reverse proxy in front of it and forward X-Forwarded-For and X-Forwarded-Proto. Don't expose the plain HTTP port to the internet.
Set TRUST_PROXY to the number of proxies between the internet and the container: 1 for a single proxy, 2 for two in a row. Don't set it to true. Each proxy in the chain must append to X-Forwarded-For, not overwrite it. If the app sees a proxy's address instead of the user's, everyone shares one rate-limit bucket and a few failed sign-ins lock everybody out for a minute.
Imports upload batches of up to about 2 MB, so allow at least 5 MB per request on the proxy (for nginx, client_max_body_size 5m;). File attachments travel in 1 MiB chunks and need no extra setting.
The app sends Strict-Transport-Security only when it sees X-Forwarded-Proto: https through the trusted chain. Check it with:
curl -sI https://your-host/ | grep -i strict
Running under a path
The same image can run at the root of a host or under a path such as https://example.com/vault. The path is read at start-up, so there's nothing to rebuild.
- In
.env, setPUBLIC_URLto the full address including the path, with no trailing slash. You can also setBASE_PATH, but it must then match the path inPUBLIC_URLor the server won't start. Path segments may use onlyA-Z a-z 0-9 . _ ~ -.PUBLIC_URL=https://example.com/vault # optional, must match the path above BASE_PATH=/vault - Run
docker compose up -d. The container now serves only under the path: the web app at/vault/and the API at/vault/api/v1. Requests outside/vaultget a 404.curl http://127.0.0.1:8080/vault/api/v1/health - Point the reverse proxy at the container without stripping the prefix: the app expects to see
/vault/…. For plain nginx:location /vault { proxy_pass http://127.0.0.1:8080; # no URI part: the /vault prefix is passed through proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
Invite, recovery and one-time links, the single sign-on redirect address, the session cookie, the installable app and the browser extension all follow the path. Register https://example.com/vault/api/v1/auth/sso/callback with your identity provider. Several instances can share one host under different paths.
To move an existing install to a path, change PUBLIC_URL, restart, update the redirect address at the identity provider and reconnect browser extensions. Invites and one-time links already sent point at the old address and must be sent again.
Running in production
- Run one replica. Rate limits and the clean-up of expired links run in the app's memory, so more replicas multiply the limits.
- Keep the host clock on NTP. Authenticator codes fail if the clock drifts by more than about 30 seconds.
GET /api/v1/healthreturns{"ok":true,"db":true}. It also checks the database, so allow a few retries before restarting the container.- Pin
CLANE_VAULT_VERSIONrather than following the latest image. - Restrict network access to Postgres to the container host, and keep
HTTP_BIND=127.0.0.1unless the proxy runs on another host over a private network.
On managed Postgres, use a dedicated role and schema and a session-mode connection (port 5432). On Supabase, avoid the schema name vault, which Supabase Vault uses.
DATABASE_URL=postgresql://clane_vault_app:…@host:5432/postgres?schema=clane_vault&sslmode=require&connection_limit=5
File attachments are stored in Postgres by default and are included in the database backup. To keep them in an S3-compatible bucket instead, set ATTACHMENT_STORAGE=s3 and the ATTACHMENT_S3_* settings, and back that bucket up yourself. On Supabase Free, set ATTACHMENT_INSTANCE_QUOTA_MB=200 or ATTACHMENTS_ENABLED=false.
Backups
The database holds vault items and files only as ciphertext, but losing it loses everything, so back it up. The app can do this itself on a schedule: it dumps its own schema with pg_dump, encrypts the dump with age to your public key before it leaves the container, and writes it to S3-compatible storage, a mounted folder, or both. Backups older than the retention period are then deleted.
- Make an age key pair on a trusted machine. Keep the private key offline or in your secrets manager, never next to the backups or on the vault server.
docker compose run --rm --no-deps clane-vault node dist/src/scripts/age-keygen.js > backup-key.txt # or, with age installed: age-keygen -o backup-key.txt - Set the backup variables in
.envand rundocker compose up -d. The app refuses to start with backups on but no recipient or destination.BACKUP_ENABLED=true BACKUP_AGE_RECIPIENT=age1… # your public key BACKUP_SCHEDULE=30 2 * * * # cron, UTC BACKUP_RETENTION_DAYS=14 BACKUP_S3_ENDPOINT= # empty for AWS S3 BACKUP_S3_REGION=us-east-1 BACKUP_S3_BUCKET=vault-backups BACKUP_S3_ACCESS_KEY_ID=… BACKUP_S3_SECRET_ACCESS_KEY=… # and/or a mounted folder: BACKUP_LOCAL_DIR=/backups - Open Admin → Backups in the app and press Back up now. That page shows the status and the run history. Every admin sees a banner if the last backup failed or the last good one is more than 26 hours old.
Use storage in a different account or with a different provider from the database if you can, and turn on object versioning or object lock there, so a compromised server can't erase the history. S3-compatible services such as Supabase Storage, Cloudflare R2 and MinIO work. The image ships pg_dump 17, which backs up Postgres 14 to 17.
Without the built-in schedule, a plain pg_dump works too. Keep copies off the host and encrypted:
pg_dump "postgresql://user:password@host:5432/db" --schema=public | gzip > clane-vault-$(date +%F).sql.gz
Restore
A restore goes into an empty schema, so you can restore next to the live data and switch over when you're satisfied.
docker compose cp ./backup-key.txt clane-vault:/tmp/backup-key.txt
docker compose exec clane-vault node dist/src/scripts/restore.js --list
docker compose exec clane-vault node dist/src/scripts/restore.js \
--from clane-vault/clane-vault-public-20261006-023000.dump.age \
--identity /tmp/backup-key.txt --target-schema vault_restored
docker compose exec clane-vault rm /tmp/backup-key.txt
- Point
DATABASE_URLat the restored schema (?schema=vault_restored), or rename the schemas in Postgres. - Use the same
.envas when the backup was taken. Without the sameSERVER_SECRET, nobody can use their authenticator codes. - Run
docker compose up -d. Any newer migrations run on start. Sign in and open a few records to check.
Test a restore into a scratch schema from time to time, and note how long it takes for your data.
Updates
Take a backup first, then run the update script with the version you want:
Linux or macOS./update.sh 1.6.2
Windows (PowerShell)
.\update.ps1 1.6.2
The script sets CLANE_VAULT_VERSION in .env, pulls the image and restarts the container. Database migrations run automatically on start. The changelog lists what each release changes and any upgrade steps.
If you'd rather not run it yourself, Clane can host it in the EU, with a data processing agreement. Contact us.