R.E.C.R.E.C. rec.farm
Valtr docs 3 pages

Valtr docs

Deploying Valtr.

Valtr is one container: a single Rust binary and a SQLite database on a volume. It sits behind your TLS reverse proxy. This guide uses https://valtr.rec.farm as the example origin and https://id.rec.farm as the Handstamp issuer.

Before you start

Generate the master key and keep it safe.

openssl rand -hex 32

This is VALTR_MASTER_KEY. It encrypts every secret. Store it in your password manager and next to your database backups. A database without its key is useless, and changing the key makes existing secrets unreadable.

People sign in with Handstamp, so Valtr needs it before it starts. The minimum .env:

VALTR_MASTER_KEY=…                       # from openssl above
VALTR_PUBLIC_ORIGIN=https://valtr.rec.farm
VALTR_TRUST_PROXY=1                      # one reverse proxy hop (Traefik, Caddy, nginx)
OIDC_ISSUER=https://id.rec.farm          # from the next step
OIDC_CLIENT_ID=valtr
OIDC_CLIENT_SECRET=hs_sec_…

Register Valtr in Handstamp

In Handstamp, Admin → Apps, as described in its Connecting an app guide:

Field Value
Name Valtr
Client ID valtr
Redirect URIs https://valtr.rec.farm/api/auth/oauth2/callback/oidc
Scopes it may ask for openid profile email
Client type Server app, secret in the header
Who may sign in Members of a valtr-users group (recommended)
Require PKCE on
Back-channel logout URI https://valtr.rec.farm/api/auth/backchannel-logout

Copy the client secret into OIDC_CLIENT_SECRET. Any other OpenID Connect provider works the same way. The issuer must be https://, except a provider on the same machine (localhost).

Signing out everywhere

With the back-channel logout URI set, signing out of Handstamp ends the Valtr sessions that came from it: when someone signs out there, uses "Sign out everywhere", has a session ended, or is turned off by an admin. Handstamp POSTs a signed logout_token to the URI from its server, and Valtr checks it like an ID token (the provider's keys, issuer, audience OIDC_CLIENT_ID, issued in the last five minutes, the logout event, no nonce, each token once). A token naming a Handstamp session (sid) ends the Valtr sessions from that one; a token naming only the person (sub) ends all of theirs. Without the URI, a Valtr session lasts its seven days or until signing out of Valtr.

The call comes from the Handstamp server, so the URI must be reachable from it, not only from browsers. API keys aren't sessions: they keep working until they expire or are revoked.

Dokploy

  1. Create a Compose service in your project. Source: this Git repository, compose path ./compose.yaml. It builds the image from the Dockerfile and keeps the database on the valtr-data volume.
  2. Environment: paste the .env from above into the service's Environment tab. Dokploy writes it to .env next to compose.yaml, which is where env_file reads it.
  3. Domain: in the Domains tab, add valtr.rec.farm → service valtr, container port 3000, HTTPS on with a Let's Encrypt certificate. Point the DNS record at your Dokploy host.
  4. Deploy. Dokploy's health view uses the image's HEALTHCHECK, which asks /readyz: ok once the database answers. /healthz only says the process is up.

Keep one replica: SQLite lives on one volume.

Then open https://valtr.rec.farm and sign in with Handstamp.

Plain Docker Compose

cp .env.example .env     # fill it in
docker compose up -d --build

Valtr listens on 127.0.0.1:3000 (VALTR_PORT to change). Put your TLS proxy in front, for example Caddy:

valtr.rec.farm {
	reverse_proxy 127.0.0.1:3000
}

Who can sign in

Handstamp decides. Anyone it lets through gets their own account on the first sign-in, with no projects. A project belongs to the account that created them; its owner shares it with an invite link (Settings → Invite someone), which works once for someone who signs in here. Limit Valtr with Handstamp's "Who may sign in" setting, for example to a valtr-users group.

People are their Handstamp identity (issuer and subject). Valtr never matches accounts by email, so changing an email in Handstamp keeps the same Valtr account.

To see the accounts:

docker compose exec valtr valtr accounts

Backups

Everything is in the volume's /data: valtr.db, plus valtr.db-wal and valtr.db-shm while it runs. For a consistent copy, stop Valtr for a moment:

docker compose stop valtr
docker compose cp valtr:/data ./valtr-backup-$(date +%F)
docker compose start valtr

Keep VALTR_MASTER_KEY with the backups.

Upgrading from the TypeScript version

Stop the old server. Mount its valtr.db at /data/valtr.db, or set VALTR_DB_PATH, and use the same VALTR_MASTER_KEY. On start, Valtr migrates the schema in place: it rebuilds the audit log without foreign keys, adds tables for sessions, Handstamp sign-in, secret history and shared projects, and drops the password hashes. The TypeScript server can't open the database after that, so back up the file first. To migrate without starting the server, run valtr migrate (docker compose run --rm valtr valtr migrate). The image runs as uid 1000, so the file must be writable by that user. API keys and secrets keep working.

Old accounts signed in with passwords, which Valtr no longer accepts. Each keeps its projects but has no Handstamp identity yet, and Valtr won't guess one from the email: a person whose Handstamp email matches an old account is turned away with "ask whoever runs Valtr to link it". Link each one to their Handstamp sub (on their user page in Handstamp's admin):

docker compose exec valtr valtr accounts                       # who isn't linked yet
docker compose exec valtr valtr link me@example.com <sub>

See the README for what else behaves differently.

Settings

Variable Default Meaning
VALTR_MASTER_KEY required 64 hex characters (or 32 raw bytes). Encrypts every secret.
VALTR_PUBLIC_ORIGIN http://localhost:3000 Public origin. https:// makes cookies Secure and __Host-. pnpm dev uses the portless URL.
VALTR_DB_PATH ./data/valtr.db SQLite file. /data/valtr.db in the image.
VALTR_TRUST_PROXY off 1 behind exactly one proxy: the client IP comes from X-Forwarded-For.
VALTR_ENV production development turns on the dev login and lets Valtr run without Handstamp. Never in production.
OIDC_ISSUER required Handstamp's issuer, https://. No trailing slash.
OIDC_CLIENT_ID required The app's client ID in Handstamp.
OIDC_CLIENT_SECRET From the app's page in Handstamp.
OIDC_SCOPES openid profile email Must include email.
OIDC_DISPLAY_NAME Handstamp The sign-in button's label.
VALTR_WEB_DIST found automatically The built web app. /app/web/dist in the image. Without one, Valtr serves the API alone.
HOST, PORT 127.0.0.1, 3000 Listen address. The image uses 0.0.0.0.
RUST_LOG valtr=info Log filter.

From rec-farm/valtr/docs/DEPLOYING.md · master@3d56e62 · 2026-10-10