Skip to content
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.json and /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.mjs in the repository. Then /release.json returns 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

Shell
# 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 --build

The 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:

Shell
docker compose -f deploy/docker/compose.yml logs server

Cloudflare 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.

Shell
docker compose -f deploy/docker/compose.yml -f deploy/docker/compose.tunnel.yml up -d --build
  • Put the tunnel token, TUNNEL_TOKEN, in deploy/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.yml gives cloudflared a fixed address in its own Docker network and tells the server to trust CF-Connecting-IP only 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 /29 and change all five values in the file: the subnet, the address range, both fixed addresses and CLOUDFLARE_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.yml to 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

Shell
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 --build

The server applies new migrations by itself. Before every update, export a backup. Also tag the current image so you can roll back quickly:

Shell
docker tag crunu-server:local crunu-server:prev

The 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:

Shell
SERVER_IMAGE=crunu-server:prev docker compose -f deploy/docker/compose.yml up -d

Agents 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.