Self-hosting Atuin on TrueNAS SCALE
Step-by-step guide to running the Atuin shell history sync server on TrueNAS SCALE with Postgres and Tailscale access, plus four gotchas to avoid.
Atuin replaces your shell history with an SQLite-backed, searchable, context-aware version that syncs across machines. The public sync server is fine, but if you’re already running a homelab, self-hosting is a 10-minute job — assuming you avoid the four gotchas below.
Stack: Atuin server + Postgres as a TrueNAS SCALE Custom App, accessed over Tailscale. No reverse proxy, no TLS cert juggling. Atuin encrypts client-side, so HTTP over a private network is fine.
The Custom App
TrueNAS SCALE’s Custom App takes Docker Compose YAML directly:
services:
atuin:
image: ghcr.io/atuinsh/atuin:latest
restart: unless-stopped
user: "568:568"
command: start
volumes:
- /mnt/tank/apps/atuin/config:/config
ports:
- "8888:8888"
environment:
ATUIN_HOST: "0.0.0.0"
ATUIN_OPEN_REGISTRATION: "true" # flip to false after registering
ATUIN_DB_URI: postgres://atuin:STRONGPASSWORD@db/atuin
depends_on:
- db
db:
image: postgres:16
restart: unless-stopped
user: "568:568"
volumes:
- /mnt/tank/apps/atuin/database:/var/lib/postgresql/data
environment:
POSTGRES_USER: atuin
POSTGRES_PASSWORD: STRONGPASSWORD
POSTGRES_DB: atuin
The 568:568 is TrueNAS SCALE’s built-in apps user. getent passwd apps confirms.
Gotcha 1: permissions on the bind mount
On first boot, Postgres logs Operation not permitted on the data dir, and Atuin can’t write /config/server.toml. Docker creates the host paths as root:root when it bind-mounts them, and the containers run as 568.
sudo chown -R 568:568 /mnt/tank/apps/atuin
Then restart the app.
Gotcha 2: password special characters
Strong passwords with !, #, @, %, ^ cause Atuin’s Postgres URI parser to choke — # and @ have special meaning in URI syntax — and return invalid port number.
The Atuin docs mention this: stick to [A-Za-z0-9.~_-]. Regenerate with pwgen or equivalent, alphanumeric only.
Gotcha 3: Postgres ignores new passwords
After fixing the password you may still get password authentication failed. Postgres only reads POSTGRES_PASSWORD on first init — if the data directory already exists, it skips setup entirely and keeps the old credentials.
To reset:
# Stop the app first
sudo rm -rf /mnt/tank/apps/atuin/database/*
sudo chown -R 568:568 /mnt/tank/apps/atuin
# Start the app
Watch the logs for "PostgreSQL init process complete" — that confirms it ran init with the new password.
Gotcha 4: encryption key mismatch after the wipe
If you registered an account, uploaded records, then wiped Postgres, your local atuin state will be out of sync with the now-empty server. Sync fails with:
Error: attempting to decrypt with incorrect key
Nuke both ends:
# Inside the db container:
DELETE FROM users WHERE username='nacho';
# On the client:
rm -rf ~/.local/share/atuin ~/.config/atuin
Then re-register clean. Save the encryption key to a password manager the moment atuin key prints it — you need it on every other machine.
Client setup
brew install atuin
mkdir -p ~/.config/atuin
echo 'sync_address = "http://nas.tail-xxxx.ts.net:8888"' > ~/.config/atuin/config.toml
atuin register -u me -e me@example.com
atuin key # → 1Password
atuin import auto
atuin sync -f
Add eval "$(atuin init zsh)" to the end of your .zshrc. If it’s near the top, other plugins (fzf, history-substring-search) load after and clobber its keybindings — Ctrl+R silently does the wrong thing.
Every other machine: same flow but atuin login instead of register, paste the key from 1Password when prompted.
Once all machines are registered, flip ATUIN_OPEN_REGISTRATION to "false" and redeploy. New accounts blocked, existing sessions unaffected.
On Cloudflare Tunnels
CF Tunnels work if Tailscale isn’t an option on some machine, but two things will break if you don’t disable them: Rocket Loader and Browser Integrity Check (both interfere with the CLI). Also don’t put Cloudflare Access in front — the Atuin CLI can’t complete the browser auth flow.
If Tailscale covers everything you care about, skip it. Fewer moving parts.
Total time end-to-end: maybe 45 minutes, most of it on the four gotchas above. The payoff is real: commands you ran six months ago on one machine come up in Ctrl+R on another like they’d always been there.