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
- Register Valtr in Handstamp
- Dokploy
- Plain Docker Compose
- Who can sign in
- Backups
- Upgrading from the TypeScript version
- Settings
Before you start
Generate the master key and keep it safe.
openssl rand -hex 32This 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
- Create a Compose service in your project. Source: this Git repository, compose path
./compose.yaml. It builds the image from theDockerfileand keeps the database on thevaltr-datavolume. - Environment: paste the
.envfrom above into the service's Environment tab. Dokploy writes it to.envnext tocompose.yaml, which is whereenv_filereads it. - Domain: in the Domains tab, add
valtr.rec.farm→ servicevaltr, container port3000, HTTPS on with a Let's Encrypt certificate. Point the DNS record at your Dokploy host. - Deploy. Dokploy's health view uses the image's
HEALTHCHECK, which asks/readyz:okonce the database answers./healthzonly 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 --buildValtr 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 accountsBackups
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 valtrKeep 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. |