Skip to main content

Atuin and Tailscale and containers and 1Password

·5 mins·
howto

I’m not sure how it took me this long to discover1 atuin but I’ve been missing out for sure. Atuin is essentially (much) “better shell history” in a bunch of ways, as it:

  1. uses a database to enable more kinds of queries on the history
  2. has per-directory (or workspace) contextual filters, to better limit what is shown
  3. syncs your shell history across devices, via an end-to-end encrypted sync server

That last feature was one of the things I was most excited about, although 2 is really great as well in practice. While there is a public sync server available, I thought I should run and host my own, given that’s an option and I’m that sort of person.

So, we’re setting up a self-hosted Atuin sync server reachable via Tailscale, running in containers (Podman, but Docker would work), with 1Password providing secrets management, running on macOS. The completed configuration is available on GitHub , if you want to skip ahead. The documentation from Atuin is pretty thorough, but I did have to figure out a bit more glue, hence this post.

Containers (Podman, Docker etc.)
#

I use Podmanas most container workloads don’t need root. In this particular setup, we’ll deploy 3 containers via podman-compose:

  1. Atuin server
  2. the Postgres database for the Atuin server
  3. Tailscale routing so we can reach the Atuin server from our tailnet

Setting up the Postgres and Atuin containers is pretty well-covered in the official self-hosting docs , though I did have to figure out a small thing and ended up submitting a PR to fix the upstream docs.

Atuin server
#

Annotated container config:

atuin:
  image: ghcr.io/atuinsh/atuin:latest
  # start after the DB and Tailscale
  depends_on:
    - db
    - tailscale
  restart: always
  command: server start
  # Make config persistent
  volumes:
    - "./config:/config"
  # Map the ports
  ports:
    - 8888:8888
  environment:
    # Listen an all interfaces.
    ATUIN_HOST: "0.0.0.0"
    # same port as above
    ATUIN_PORT: 8888
    # I don't keep registration open generally, but you probably need to set
    # this to "true" once to set up a first account.
    ATUIN_OPEN_REGISTRATION: "false"
    # These env vars will be injected later. Make sure that `db` matches your
    # database container's name!
    ATUIN_DB_URI: postgres://${ATUIN_DB_USERNAME}:${ATUIN_DB_PASSWORD}@db/${ATUIN_DB_NAME}
    RUST_LOG: info,atuin_server=debug
  network_mode: service:tailscale

Given that we’re using Tailscale to access the server, I don’t worry about setting up TLS.

To re-emphasize, the main trouble I had was the DB container name not matching the ATUIN_DB_URI string. Now that the upstream docs are fixed, it’s probably less likely you’d run into this issue if you followed those.

Postgres database
#

Annotated container config:

db:
  image: postgres:17
  depends_on:
    - tailscale
  restart: unless-stopped
  volumes:
    # Don't remove permanent storage for index database files!
    - "./database:/var/lib/postgresql/data/"
    # If the WAL ever gets corrupt (e.g., due to abrupt container stops), fix it with:
    # docker run -it -v ./database:/var/lib/postgresql/data/ postgres:17 /bin/bash
    # su postgres && cd /var/lib/postgresql/data && pg_resetwal
  environment:
    POSTGRES_USER: ${ATUIN_DB_USERNAME}
    POSTGRES_PASSWORD: ${ATUIN_DB_PASSWORD}
    POSTGRES_DB: ${ATUIN_DB_NAME}

Tailscale
#

We want our Tailscale container to be able to route to and from our Atuin server, while being able to independently authenticate to the network so that we don’t lose connectivity periodically. Tailscale docs are pretty good on how to set up containers , including OAuth configuration.

Annotated container config:

tailscale:
  image: ghcr.io/tailscale/tailscale:latest
  restart: always
  # TS_AUTHKEY from the OAuth config
  environment:
    TS_AUTHKEY: ${TS_AUTHKEY}
    TS_HOSTNAME: atuin
    TS_STATE_DIR: /var/lib/tailscale
    TS_EXTRA_ARGS: --advertise-tags=tag:container
  volumes:
    - "./tailscale:/var/lib/tailscale/"
  devices:
    - /dev/net/tun:/dev/net/tun
  cap_add:
    - net_admin

1Password
#

I’ve known for a while that 1Password has a CLI integration, allowing secrets access from the command line, in scripts etc. This seemed like a good spot to try that. My approach was to reference the secrets as environment variables (via mise ), have their values be paths into a 1Password vault, and start my cluster by having op inject the values at start-up time.

So, for example, to set the database password for Atuin, in mise.toml I have:

[env]
ATUIN_DB_PASSWORD = "op://Personal/atuin/password"

All together now (hopefully)
#

With the finished compose.yaml for the cluster, and having set up the respective secrets in 1Password, all that’s left to do is:

op run -- podman compose up -d

This should prompt for authentication with 1Password, and then bring up all the containers and have everything ready to go! As mentioned, you may need to allow for registration once, to set up one account that you’ll use. Or keep it on if you’re doing this for a team, friend group, polycule, etc. Given the server has to be reached over Tailscale, it won’t matter too much if you leave this on.

If you have issues, check the logs for clues. I’ve had the occasional issue with the DB WAL.

podman compose logs -n -t -f

You can also configure 1Password integration (and mise populating the env) is working as expected by printing the environment unmasked:

op run --no-masking -- printenv # might want to pipe into grep ATUIN

Atuin client
#

Get Atuin (the client) from the official place or use homebrew / another package manager of your choice. You’ll need a little configuration to tell your client which is the right server to use.

Here’s my (annotated) configuration as an example, but only the sync_address is really needed. For the rest, find what works for you, and make sure to look at the Atuin docs :

# Tailnet address and correct port; remember this is HTTP.
sync_address = "http://atuin.<your-tailnet>.ts.net:8888"
# Workspaces use per-git-repository history, rather than directory-only
workspaces = true
# Prefer to see the workspace history by default.
filter_mode_shell_up_key_binding = "workspace" # or global, host, directory, etc
# Run commands directly; tab to edit.
enter_accept = true
# Use Ctrl-0 .. Ctrl-9 instead of Alt-0 .. Alt-9 UI shortcuts; better on macOS.
ctrl_n_shortcuts = true
# Invert window?
invert = false
# Window style
style = "auto"

At this point you should be able to run atuin status and see you’re green and connecting to the server you’ve just configured!


  1. It was probably this comic by Julia Evans that finally made me look. ↩︎