
Why Didn’t I Know About this Sooner?
If you spend most of your time in graphical user interfaces (GUIs), you can probably skip this post. However, if you spend a lot of time in terminal windows and would like a way to back up, sync, and search your command line history across all of the hosts you access, make sure you check out Atuin.
This all started when I had a few spare hours on a train ride and wanted to write a post about some recent network troubleshooting I was doing to figure out a Podman Macvlan issue. I didn’t have access to the computer I had done the work from and was having a hard time remembering the specific commands involved. My first thought was, “Why don’t I have a backup of my CLI history?” All of my source code is in Git, my docs and files are synced between computers with Syncthing, and my photos are all stored in Immich. How do I not have something similar for my CLI history?
My next thought was, “Maybe I can build something to store/sync it,” which was quickly followed by “maybe someone else has already built something.” It only took a few minutes to find Atuin and realize someone else had indeed built exactly what I was thinking but even better.
Key Atuin Features
The key things I wanted included:
- nice integration with the shells I use - zsh and bash
- end-to-end encrypted sync
- easy search of history
- self-hosted server support
Atuin took this further with:
- adjustable ctrl-r toggle to filter for commands from all hosts, local host, current directory, etc
- easily viewable ctrl-r history list
- import of prior history
- up-arrow history search (I found this was hard to adjust to and I disabled it)
Setting up an Atuin Server with Ansible
You can use the central server the project provides or you can self-host. Here are some excerpts from the Ansible playbook I used to set up the self-hosted server:
- set_fact:
service: atuin
- name: Create config dir
become: true
file:
path: "/opt/{{ service }}/data/config"
state: directory
owner: 1000
group: 1000
- name: copy systemd container
become: true
copy:
src: "{{ item }}"
dest: "/etc/containers/systemd/{{ item }}"
mode: "0644"
with_items:
- "{{ service }}.container"
notify: restart atuin
- name: start service
become: true
systemd:
enabled: true
state: started
name: "{{ service }}"
daemon_reload: yes
I’m using Podman Quadlets with a Traefik frontend. I use Traefik for TLS and container routing, but you can also skip that part by pointing directly to the Atuin port or use something equivalent.
[Unit]
Description=Atuin Container
Requires=traefik-network.service
After=traefik-network.service
[Container]
AutoUpdate=registry
ContainerName=atuin
Image=ghcr.io/atuinsh/atuin:18.15
Exec=start
Network=traefik.network
Volume=/opt/atuin/data/config:/config
EnvironmentFile=/opt/atuin/.env
Environment=ATUIN_HOST="0.0.0.0"
Environment=ATUIN_OPEN_REGISTRATION="true"
Environment=ATUIN_DB_URI="sqlite:///config/atuin.db"
Environment=RUST_LOG=info,atuin_server=debug
Label=traefik.enable="True"
Label=traefik.docker.network=traefik
Label=traefik.http.routers.atuin.rule="Host(`atuin.example.com`)"
Label=traefik.http.routers.atuin.entrypoints="websecure"
Label=traefik.http.services.atuin.loadbalancer.server.port="8888"
[Install]
WantedBy=multi-user.target
Atuin Shell Config on Hosts
I have 6 “servers” and 3 “clients” on which I regularly use the CLI, including both x86_64
and aarch64 hosts. This Ansible playbook installs the Atuin client to sync their shell history
with the central server.
- name: Set vars
set_fact:
atuin_version: "18.15.2"
atuin_checksum: "sha256:97391908ceaf8cd2c74e2175ee01741a80ecf620b45131d34c7fe0f1ae925b91"
atuin_unzip_dir: "atuin-{{ ansible_facts.architecture }}-unknown-linux-gnu"
- set_fact:
atuin_checksum: "sha256:857b4918f1019b6e9efe23f16c5231b6a61f09452030e93e027423f862ffd944"
when: ansible_facts.architecture == "aarch64"
- name: Set install info from version
set_fact:
atuin_install_path: "/opt/atuin/atuin-{{ atuin_version }}"
atuin_url: "https://github.com/atuinsh/atuin/releases/download/v{{ atuin_version }}/atuin-{{ ansible_facts.architecture }}-unknown-linux-gnu.tar.gz"
- name: Create app dir
become: true
file:
path: /opt/atuin
state: directory
- name: Download
get_url:
url: "{{ atuin_url }}"
dest: "/tmp/atuin-{{ atuin_version }}.tar.gz"
checksum: "{{ atuin_checksum }}"
- name: Unzip Atuin
unarchive:
src: "/tmp/atuin-{{ atuin_version }}.tar.gz"
dest: "/tmp"
creates: "{{ atuin_install_path }}"
remote_src: true
register: atuin_unzip
- name: Move to proper location
become: true
command: "mv /tmp/{{ atuin_unzip_dir }}/atuin {{ atuin_install_path }}"
when: atuin_unzip.changed
- name: Clean unzip dir
file:
path: "/tmp/{{ atuin_unzip_dir }}"
state: absent
- name: Create config dir
file:
path: ~/.config/atuin
state: directory
- name: Create config
copy:
src: config.toml
dest: ~/.config/atuin/config.toml
- name: Create an atuin bin symlink
become: true
file:
src: "{{ atuin_install_path }}"
dest: /usr/local/bin/atuin
state: link
My Atuin config.toml (referenced above) is rather stock.
auto_sync = true
update_check = false
sync_address = "https://atuin.example.com"
sync_frequency = "1m"
enter_accept = true
[sync]
records = true
[ui]
columns = ["time", { type= "host", width = 10 }, "command"]
Registering on First Host
The fact that Atuin needs a username, password, and an encryption key felt a bit more complicated
than necessary, but you can still set up most of this with Ansible. First you need to register
with the Atuin server on one host to set up a user profile and get an encryption key by following the
instructions in their docs.
Then you can grab the key by running atuin key on that host.
NOTE: Don’t grab the key from
~/.local/share/atuin/keylike I first did. It is stored in a different format than what theatuin logincommand expects.
Login on other Hosts
Once you have an account set up, you can add the following to your Ansible playbook and have Ansible login on your other hosts.
- name: Set vars
set_fact:
atuin_username: "<YOUR-USER>"
atuin_password: "<YOUR-PASSWORD>"
atuin_key: "<YOUR-KEY>"
- name: Check if already logged in
command: atuin status
register: atuin_status
failed_when: false
changed_when: false
- name: Log into Atuin if status check failed
shell: >
atuin login
-u {{ atuin_username }}
-p {{ atuin_password }}
-k "{{ atuin_key }}"
when: atuin_status.rc > 0
no_log: true
NOTE: I’d suggest storing the
atuin_passwordandatuin_keysecurely by reading the Ansible Docs. I’m just trying to keep the examples simple.
Activate in Your Shell
Then in my ~/.zshrc file we start up Atuin.
eval "$(atuin init zsh --disable-up-arrow)"
And happily back up, sync, and search our CLI history!
