Self-Hosting Atuin with Docker and Coolify

Recently at work, or rather in my home lab, I found myself increasingly frustrated with my fragmented terminal history. Like many developers, my workflow is split across multiple machines. At home, I’m at my desk with a powerful workstation; on the move, I’m typically on my MacBook Air.

The biggest friction in this context-switching was always my terminal history. I’d find myself frantically searching for a complex ffmpeg command or a specific docker one-liner, only to realize I’d run it on the other machine. It’s a very human thing to assume you’ll remember a command, only to draw a complete blank when you need it most.

This is the exact problem that Atuin solves. It completely replaces your existing shell history with a SQLite database and seamlessly syncs it across all your devices.

Why Self-Host?

Atuin offers a fantastic hosted service, and while it is end-to-end encrypted, I have a dedicated home lab and a strong preference for self-hosting my own data. This post chronicles my journey to get a private Atuin sync server running reliably on Coolify 4. I don’t have the first clue about how Atuin’s sync server is built internally, so I started searching and experimenting.


Phase 1: Foundations (January 2026)

I started this project in late January 2026 with a deceptively simple goal: create a straightforward Docker Compose configuration for the Atuin server.

  • Initial Setup: The first iteration introduced the docker-compose.yaml and a basic server.toml.
  • The First Bug: Almost immediately, I hit a snag. I realized that hardcoding the database host as db wasn’t flexible enough for all environments.
  • The Fix: I updated ATUIN_DB_URI to use the POSTGRES_HOST environment variable. This allowed the server to find its database regardless of whether it was in the same Docker network or hosted externally (a very common scenario in Coolify).

Phase 2: Documentation & Refinement (February 2026)

In February, the focus shifted to making the project truly “Coolify-ready.”

  • Deployment Guide: Added a comprehensive README.md detailing the exact steps to deploy on Coolify 4. This included handling internal networking and persistent storage.
  • Cleaning Up: Removed unnecessary container_name fields and version tags from the Compose file to adhere to modern Docker best practices.
  • Dependency Management: Introduced Dependabot to keep our Atuin and Docker images up to date automatically. I am inherently lazy and couldn’t keep doing that by hand.

Phase 3: Solving the “Last Mile” (March 2026)

This is where the most significant technical hurdles were cleared. Despite having what looked like a “correct” configuration, local development was still failing with cryptic “No Data Sent” and “Empty Reply” errors.

The Mystery of the Empty Reply

Accessing localhost:8888 from the host machine resulted in an infuriating Empty reply from server.

  • The Discovery: By inspecting the container’s internal state, I found the Atuin server was listening strictly on 127.0.0.1.
  • The Fix: Inside a container, 127.0.0.1 is strictly local to that container. I had to explicitly set ATUIN_HOST: "0.0.0.0" to allow Docker to forward external traffic to the Atuin process. What do you know, it actually worked.

The Silent Crash: Environment Interpolation

The server would often crash on startup because the ATUIN_DB_URI became malformed when environment variables were missing in the local shell.

  • The Fix: I adopted the ${VAR:-default} syntax in docker-compose.yaml. This provided “local-friendly” fallbacks that allowed the stack to boot instantly for testing, while still seamlessly allowing production overrides.

The PostgreSQL 18 Curveball

Upgrading to postgres:18-alpine for future-proofing immediately broke my persistence.

  • The Discovery: It turns out there’s a better way to mount volumes in newer Postgres versions. PostgreSQL 18 changed its official Docker image standards. The old mount point at /var/lib/postgresql/data is now discouraged in favor of the parent /var/lib/postgresql directory to support better version-specific data management.
  • The Fix: I updated the volume mounts to perfectly align with the latest PostgreSQL Docker standards.

Phase 4: Full Automation

To ensure long-term stability without constant babysitting, I implemented a complete CI pipeline:

  • GitHub Actions: A new workflow now spins up the entire stack on every PR and push to main. It verifies not just that the container starts, but that it successfully returns the expected “Great A’Tuin” homage.
  • Dependabot Expansion: Expanded Dependabot to track GitHub Actions versions, ensuring the CI tools stay as current as the application itself.

Final Architecture

The result is a robust, hybrid setup that just works everywhere:

  • Production: Clean, variable-driven config specifically optimized for Coolify.
  • Local: A mise-powered workflow with a completely self-contained test stack.
  • CI: Automated verification on every single change.

That’s a wrap. I hope this helps anyone else trying to wrangle Atuin into their own self-hosted setup.

Resources