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.