Install auspex in a Container (Docker) on Linux

Last updated: September 3, 2026

This guide is for running the Span agent (auspex) inside a Linux container — a Docker image you build and run yourself — so an AI coding tool running in that container has its activity captured in Span. It uses the supervised posture (no systemd): auspex install --supervised at build time and auspex daemon --supervise at run time.

It's validated on Ubuntu 24.04 and Amazon Linux 2023 base images, amd64 and arm64.

⚠️ Beta. Linux support is a beta release. The install flow and this guide work end-to-end, but the Linux path is newer and less battle-tested than macOS and Windows — expect the occasional rough edge, and please report anything unexpected to the Span team.

Not what you're looking for?

  • For a Linux laptop / VM with a login session, use the Linux self-install guide (--service + systemd --user).
  • For Cursor or Claude Code cloud agents (managed by those platforms), use the Cursor cloud or Claude cloud guides — they wire their platform's lifecycle for you.
  • For a VS Code / devcontainer workflow, the maintained auspex-distribution Dev Container Feature does this end-to-end; this guide is the hand-rolled Docker equivalent.

How it works

A container has no systemd, so auspex runs in its supervised posture — an auspex-owned supervisor process keeps the daemon alive, instead of the OS. The work splits across two phases:

  1. Build time (auspex install --supervised) — places the binary, wires the capture hooks into the coding tools, and arms the cold-start "relax" marker so a hook that fires before the daemon's first heartbeat still spools its event. It does not start the daemon or register any OS service.
  2. Run time (auspex daemon --supervise) — the container's entrypoint. It runs in the foreground (suitable as PID 1), supervises/respawns the daemon worker, enrolls with Span using the token from the environment, and forwards SIGTERM to the worker on docker stop so it drains before exit.

The token is supplied at run time via an environment variable, so it's never baked into the image.

Before you start

  • Docker (or a compatible builder/runtime), on a host that can build for your target architecture
  • A base image of Ubuntu 24.04 or Amazon Linux 2023
  • Your Span auth token (the global org token from your Span administrator — not a PAT)
  • (Optional) a work email for attribution

Two things to know before you write the Dockerfile:

  • auspex installs per-user and refuses to run as root. Containers often default to root, but auspex install --supervised will refuse it. Create a normal user in the image and install/run as that user (the Dockerfile below does this).
  • The token is a run-time secret. Pass AUSPEX_CLOUD_TOKEN with docker run -e … (or your orchestrator's secret store) — do not put it in the image or the Dockerfile.

Step 1: Build an image with auspex installed

This Dockerfile creates a non-root user, fetches the signed Linux binary, and runs install --supervised as that user. TARGETARCH is set automatically by BuildKit/docker buildx to amd64 or arm64 — which is exactly the download path's architecture segment.

# syntax=docker/dockerfile:1
FROM ubuntu:24.04

# ca-certificates: TLS to the Span ingest; curl: the fetch below; git: optional,
# lets the daemon attribute events to the repo's git identity; bash: some tools'
# hooks invoke bash. Drop what you don't need.
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates curl git bash \
 && rm -rf /var/lib/apt/lists/*

# auspex installs per-user and refuses root — install and run as a normal user.
RUN useradd --create-home --shell /bin/bash auspex
USER auspex
ENV HOME=/home/auspex
# ~/.local/bin is where install symlinks the launcher; put it on PATH for the CLI.
ENV PATH=/home/auspex/.local/bin:$PATH

# TARGETARCH is amd64 or arm64 — BuildKit sets it automatically from the build/target
# platform. Declare it WITHOUT a default: a default (e.g. `=amd64`) shadows the auto
# value and would pin every build to that arch (an amd64 binary in an arm64 image).
ARG TARGETARCH
RUN curl -fSL "https://auspex.span.app/releases/latest/linux/${TARGETARCH}/auspex" -o /tmp/auspex \
 && chmod +x /tmp/auspex \
 && /tmp/auspex install --supervised \
 && rm -f /tmp/auspex

# The supervisor is the container's main process (PID 1): it enrolls from the
# environment, keeps the daemon alive, and drains on `docker stop`.
ENTRYPOINT ["/home/auspex/.auspex/bin/auspex", "daemon", "--supervise"]

Build it (buildx sets TARGETARCH from --platform):

docker buildx build --platform linux/amd64 -t my-auspex-image .

Amazon Linux 2023 base image. Swap the first two instructions for AL2023's package manager. AL2023 already ships curl (as curl-minimal), so don't install the full curl package — it conflicts:

FROM amazonlinux:2023
RUN dnf install -y ca-certificates git bash shadow-utils && dnf clean all

The rest of the Dockerfile (user creation, fetch, install --supervised, entrypoint) is identical. shadow-utils provides useradd.

Step 2: Run the container with your token

The token is a run-time input. At boot the daemon resolves it, in order, from the environment (AUSPEX_CLOUD_TOKEN), a managed identity file, then auspex's own user identity file — so you can supply it as an environment variable or from a file. The file route is more private and is recommended for anything beyond local testing (see below).

For the standard Span backend, the token is the only value you must supply. AUSPEX_CLOUD_WORK_EMAIL is optional — without it the daemon falls back to the repo's git identity, and if neither resolves, capture still runs but events are unattributed. Point auspex at a self-hosted or proxied plane only if you run one:

Variable Required Meaning
AUSPEX_CLOUD_TOKEN yes (or a file, below) Span org auth token (enrollment + egress auth)
AUSPEX_CLOUD_WORK_EMAIL optional attribution email; falls back to git identity, then unattributed
AUSPEX_CLOUD_API_BASE_URL optional control-plane host (default https://api.span.app)
AUSPEX_CLOUD_LOGS_URL optional logs-ingest host (default https://agent-traces.span.app)

Simplest: an environment variable (local testing)

docker run --rm \
  -e AUSPEX_CLOUD_TOKEN="span_…" \
  -e AUSPEX_CLOUD_WORK_EMAIL="you@company.com" \
  my-auspex-image

This is the quickest way to try it, but a token passed with -e is the least private option: it's recorded in docker inspect, persisted in the container's configuration, and (as -e VAR=value) left in your shell history and the docker process's command line. --env-file keeps it out of your shell history, but the value still lands in the container config and docker inspect. For anything beyond local testing, supply the token from a file instead.

More secure: supply the token from a file (recommended)

Mount the token as a file — a Docker/Compose/Kubernetes secret, a read-only bind mount, or a tmpfs — so it never appears in the image, in docker inspect, or on the docker command line. Make sure the mounted file is readable by the container's auspex user, then feed it to auspex one of two ways (wire either script as your ENTRYPOINT — see Adding auspex to an existing image for the COPY / ENTRYPOINT lines):

A. Provision it into auspex's identity file (token never stays in the environment). auspex install is the one command that reads a token file; it decodes the file (tolerating a UTF-16/BOM one, e.g. authored on Windows) and writes it to a private (0600) identity file only your user can read. Point the entrypoint at the mounted secret:

#!/usr/bin/env bash
set -euo pipefail
AUSPEX=/home/auspex/.auspex/bin/auspex

# Provision the org token from the mounted secret into auspex's 0600 identity file.
# Idempotent; re-runs each start. Nothing lands in the environment or `docker inspect`.
[ -f /run/secrets/auspex_token ] && "$AUSPEX" install --supervised --token-file /run/secrets/auspex_token

exec "$AUSPEX" daemon --supervise

B. Read the file into the environment at start-up (the classic *_FILE pattern). If you'd rather not persist the token to disk inside the container, read the mounted secret into AUSPEX_CLOUD_TOKEN in the entrypoint — it's then only in the daemon's process environment, still absent from docker inspect and the image:

#!/usr/bin/env bash
set -euo pipefail
AUSPEX=/home/auspex/.auspex/bin/auspex

[ -f /run/secrets/auspex_token ] && export AUSPEX_CLOUD_TOKEN="$(tr -d '\r\n' < /run/secrets/auspex_token)"

exec "$AUSPEX" daemon --supervise

(Route A normalizes odd file encodings for you; route B expects a plain UTF-8 secret.)

With Docker Compose, a secrets: entry mounts the file at /run/secrets/<name> for you:

services:
  auspex:
    image: my-auspex-image
    secrets: [auspex_token]
    # environment: ["AUSPEX_CLOUD_WORK_EMAIL=you@company.com"]   # email isn't secret
secrets:
  auspex_token:
    file: ./auspex_token.txt   # or an external / swarm secret

Don't set both. The environment token overrides the identity file, so if you use route A do not also pass -e AUSPEX_CLOUD_TOKEN — a stale env value would shadow the file. The attribution email (AUSPEX_CLOUD_WORK_EMAIL) isn't a secret, so an env var for it is fine either way.

Adding auspex to an existing image (reusable entrypoint)

If auspex should run alongside your container's real workload (rather than being the container's only job), keep your existing ENTRYPOINT/CMD and wrap it: start the supervised daemon in the background, run your command, and forward SIGTERM so the daemon drains on docker stop.

Add the install steps from Step 1 to your image (the useradd / USER / fetch / install --supervised lines), then add this entrypoint script:

#!/usr/bin/env bash
# docker-entrypoint.sh — run the auspex supervised daemon alongside the workload.
set -euo pipefail

AUSPEX=/home/auspex/.auspex/bin/auspex

# Provision the token from a mounted secret file (see Step 2, route A) — or drop this
# line and pass the token another way. Then start the supervised daemon.
[ -f /run/secrets/auspex_token ] && "$AUSPEX" install --supervised --token-file /run/secrets/auspex_token
"$AUSPEX" daemon --supervise & auspex_pid=$!

# Run the container's real command.
"$@" & app_pid=$!

# On stop, tell both children to drain/exit; the daemon flushes its spool on SIGTERM.
trap 'kill -TERM "$app_pid" "$auspex_pid" 2>/dev/null || true' TERM INT

# Wait for the workload. Capture its exit code with `|| status=$?` so a non-zero exit
# — including the 143 from `docker stop` interrupting it via the TERM trap — does NOT
# trip `set -e` and skip the drain below. The daemon must always get its window.
status=0
wait "$app_pid" || status=$?
kill -TERM "$auspex_pid" 2>/dev/null || true
wait "$auspex_pid" 2>/dev/null || true
exit "$status"

Wire it up in the Dockerfile:

COPY --chmod=0755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["your-app", "--your-flags"]

Running the daemon as a background process means signals reach it only through the wrapper's trap; the PID-1 entrypoint in Step 1 gets docker stop's SIGTERM directly and is the cleaner option when auspex is the container's primary job.

Step 3: Verify

With a container running, exec in and check:

docker exec -it <container> auspex status
docker exec -it <container> auspex auth show
  • auspex status should print auspex daemon: ok (and, on stderr, auspex: install mode: user).
  • auspex auth show should show your work email and a masked token — with (source: env) if you passed it as an environment variable, or (source: user) if you provisioned it from a file via route A.
  • auspex status --verbose --check-token additionally probes that the token is accepted by Span.

Then have a coding tool do some work inside the container and confirm the trace appears in the Span dashboard (ask your Span rep to enable Agent Traces for your org if you don't see anything yet).

(Optional) Verify the binary before baking it

The curl in Step 1 trusts the download host. For a tamper-evident image, verify the binary against auspex's cosign-signed release manifest first and pin it by digest — the same mechanism described in the laptop guide's Pin and verify an exact build, using the raw-binary selector (mediaType=="application/octet-stream", platform.os=="linux"). Bake the verified sha256: digest as a --build-arg and fetch by digest from https://auspex.span.app/blobs/sha256/<digest> for a reproducible build.

Egress allowlisting (only if your org restricts it)

A container enrolls and exports over the same endpoints as any auspex host, plus the build-time binary fetch. The full list — hostnames and IP ranges, and how to shrink it — is maintained in the security overview: see Network Access.

Limitations

  • Teardown flush. On docker stop, the daemon drains within its shutdown budget (~10s by default); a burst of events in the final moments before the container is killed may not flush. Give docker stop enough grace (--time) for a clean drain.
  • Out-of-band restart. The supervisor is (re)launched by the container's start command. docker run and docker start replay the entrypoint, so it comes back up; an orchestrator that resumes a container without re-running its start command won't relaunch it.
  • Attribution scope. The work email is the identity you provide (or the container's git identity), so an agent driven by multiple people is attributed to that one identity.
  • Read-only root filesystems. auspex needs a writable ~/.auspex (its runspace/spool). If you run with --read-only, mount a writable volume at /home/auspex/.auspex.

Troubleshooting

  • refusing to run as root during build — the install --supervised step ran as root. Add a non-root USER (and ENV HOME=…) before it, as in Step 1.
  • No events in Span — confirm the token reached the daemon: docker exec <container> auspex auth show should report it set (source: env for an env var, source: user for the file route). If it's unset with the file route, the mounted secret wasn't readable by the container user or the path was wrong; if you use route A, make sure you didn't also pass -e AUSPEX_CLOUD_TOKEN (env shadows the file). Also confirm egress to agent-traces.span.app is allowed.
  • auspex: command not found inside the container — ~/.local/bin isn't on PATH; add ENV PATH=/home/auspex/.local/bin:$PATH (as in Step 1) or use the full path ~/.auspex/bin/auspex.
  • Events unattributed — set -e AUSPEX_CLOUD_WORK_EMAIL=you@company.com, or install git in the image so the daemon can read the repo's git identity.
  • The daemon didn't flush on stop (wrapper entrypoint) — the background daemon only gets SIGTERM via the wrapper's trap; make sure you used the trap/wait form above, and allow enough docker stop --time.

If you're stuck on something not listed here, share the container's startup log and the output of auspex status --verbose with the Span team.