Browse the docs
Self-hosting
Run Crunu on your own server with Docker
Deploy the Crunu server with Docker Compose and Caddy for automatic TLS, or behind a Cloudflare Tunnel with no open ports, and keep it updated.
Last updated
On this page
Your own server gives you full control and the fewest outside dependencies. The Docker setup runs the Crunu server with Caddy in front of it. Caddy gets a TLS certificate by itself. The database is SQLite in a Docker volume by default, or Postgres next to the server.
Read Self-hosting: overview and platforms first.
What you need
- a server with Docker;
- a domain with an A or AAAA record pointing to the server;
- ports 80 and 443 open;
- a copy of the Crunu repository on the server.
The web app must be ready first
The server image embeds a ready-built web app from dist/web/ in the repository. It doesn't build the web app itself. Put the web app of the same version as the server code into dist/web/ before you start:
- A signed web release. Download it from the release directory and check its signature with
scripts/release/web-manifest.mjs verify. With a signed release, the server also answers on/release.jsonand/release.sig, so a local copy of the web app can compare the site against the signature. - Or an unsigned build: run
node scripts/build-web-v2.mjsin the repository. Then/release.jsonreturns 404.
If dist/web/ is missing, has no index.html, or holds a different version than the server code, the image build stops with an error instead of silently embedding the wrong site.
Start the server
# on the server, in the repository directory, with the web app in dist/web
node scripts/server-init.mjs --origin https://vault.company.example --out deploy/docker/.env
docker compose -f deploy/docker/compose.yml up -d --buildThe SQLite database lives in the crunu_data volume, in /data/crunu.db inside the container. For Postgres next to the server, pass --docker-postgres to server-init, which turns on the postgres profile in Compose.
The container runs with a read-only root filesystem, without extra capabilities and as an unprivileged user.
If the server fails its health check, Compose reports that a container is unhealthy. The reason is in the server log:
docker compose -f deploy/docker/compose.yml logs serverCloudflare in front of Caddy
If Cloudflare proxies your domain, set TRUST_PROXY=cloudflare:1 in deploy/docker/.env. The 1 stands for Caddy, your own proxy between Cloudflare and the server. The server then takes the client address from CF-Connecting-IP, but only when Caddy received the request from Cloudflare's networks. A request that bypasses Cloudflare gets no benefit from that header: the server uses the address the request came from.
To keep anyone from bypassing Cloudflare, allow ports 80 and 443 only from Cloudflare's addresses.
TRUST_PROXY=cloudflare without a number doesn't start behind Caddy. The server explains that it would see every client as Caddy's address and suggests cloudflare:1.
Cloudflare Tunnel instead of Caddy
You can expose the server through a Cloudflare Tunnel with no published ports. cloudflared talks to the server directly, Caddy doesn't run, and Cloudflare handles TLS.
docker compose -f deploy/docker/compose.yml -f deploy/docker/compose.tunnel.yml up -d --build- Put the tunnel token,
TUNNEL_TOKEN, indeploy/docker/.env. A placeholder line is already there. - In Cloudflare Zero Trust, set the tunnel's public hostname to the server's domain and the service to HTTP at
server:8080. compose.tunnel.ymlgivescloudflareda fixed address in its own Docker network and tells the server to trustCF-Connecting-IPonly from that one address. A wider network would include the Docker gateway, so the server would trust a header sent from the host itself.- If Docker reports that the subnet is taken, pick another free
/29and change all five values in the file: the subnet, the address range, both fixed addresses andCLOUDFLARE_IPS. - When switching from Caddy to the tunnel, stop and remove the Caddy container first with
docker compose -f deploy/docker/compose.yml rm -sf caddy. Otherwise it keeps holding ports 80 and 443. - Keep adding
-f deploy/docker/compose.tunnel.ymlto every command in this mode. Without it, Compose starts Caddy again.
After starting, run doctor with the operator token and check that the client address check shows no mismatch.
Updating
git pull # or check out a version tag
# put the web app of the new version into dist/web first
docker compose -f deploy/docker/compose.yml up -d --buildThe server applies new migrations by itself. Before every update, export a backup. Also tag the current image so you can roll back quickly:
docker tag crunu-server:local crunu-server:prevThe recommended order for an update: publish the agent and web releases of the new version, set AGENT_RELEASE_VERSION in .env, deploy the server, check it with doctor, then update the agents on your machines.
Rolling back
Migrations only add, so the previous version works with a database the new version already updated. To roll back the code, start the saved image:
SERVER_IMAGE=crunu-server:prev docker compose -f deploy/docker/compose.yml up -dAgents don't need to be rolled back. Rolling back below version 6.0 isn't possible. Restoring the database from an archive is needed only when the data itself is damaged. See Backups and restore.
Scheduled cleanup
On your own server, cleanup runs every minute. It deletes expired sessions, codes and links, trims the audit log to its retention period, and raises alerts that depend on state, such as expired machine leases.