Skip to content

Docker Deployment

Talon ships a production-ready Docker image with a multi-stage build, baked-in CLIs, health checks, and security hardening. This is the fastest way to get a Talon daemon running.

Terminal window
# 1. Copy and edit the example config
cp config/talond.example.yaml config/talond.yaml
# Edit config/talond.yaml with your API keys and channel settings
# 2. Set up environment variables
cp deploy/.env.example .env
# Fill in your secrets (API keys, tokens)
# 3. Start the daemon
docker compose -f deploy/docker-compose.yaml up -d
# 4. Verify it's running
docker compose -f deploy/docker-compose.yaml ps
docker compose -f deploy/docker-compose.yaml logs -f

That’s it. One docker compose up -d and Talon is running with health checks, automatic restarts, and resource limits.

The image uses a multi-stage build to keep the final image small:

StageBase imagePurpose
buildernode:24-slimInstalls all dependencies, compiles TypeScript to dist/
productionnode:24-slimMinimal runtime with only production dependencies
  • Base image: node:24-slim — small, up-to-date, and official
  • Init system: tini — forwards signals correctly and reaps zombie processes
  • Non-root user: Runs as talond:talond with fixed UID/GID 1000:1000
  • Health check: Verifies the daemon process is alive by checking the PID file
  • Volumes: /data (SQLite, PID file, sockets) and /config (talond.yaml)
# The production stage in deploy/Dockerfile:
FROM node:24-slim AS production
# Install tini for proper signal handling
RUN apt-get update -qq && \
apt-get install -y --no-install-recommends tini && \
rm -rf /var/lib/apt/lists/*
# Create non-root user with predictable UID/GID
RUN groupadd --gid 1000 talond && \
useradd --uid 1000 --gid talond --no-create-home --shell /usr/sbin/nologin talond
# Switch to non-root user
USER talond
# Health check via PID file
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "..."
ENTRYPOINT ["/usr/bin/tini", "--", "node", "/opt/talond/dist/daemon/main.js", "--config", "/config/talond.yaml"]

The compose file in deploy/docker-compose.yaml is the recommended way to run Talon. It layers on production hardening that goes beyond the Dockerfile:

Two volumes keep your data safe across container restarts and upgrades:

volumes:
# Named volume — survives container removal, managed by Docker
- talond-data:/data
# Bind-mount — your config file, read-only inside the container
- type: bind
source: ../config/talond.yaml
target: /config/talond.yaml
read_only: true
VolumeTypePurpose
talond-data:/dataNamedSQLite database, PID file, IPC sockets — persists across restarts
../config/talond.yaml:/config/talond.yamlBind-mount (read-only)Your configuration — edit on the host, the container picks it up on restart

Secrets are loaded from a .env file and never committed to version control:

env_file:
- ../.env
environment:
NODE_ENV: production

If you want Talon to spawn sandbox containers for agent execution, mount the Docker socket:

# Uncomment to enable sandbox container spawning:
# - /var/run/docker.sock:/var/run/docker.sock

The compose-level health check works alongside the Dockerfile HEALTHCHECK. If the container becomes unhealthy, Compose can restart it:

healthcheck:
test: ["CMD", "node", "-e", "process.exit(0)"]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3

The compose file applies defense-in-depth beyond what the Dockerfile alone provides:

# Drop ALL Linux capabilities — Talon doesn't need any
cap_drop:
- ALL
# Prevent privilege escalation via setuid/setgid binaries
security_opt:
- no-new-privileges:true
SettingEffect
cap_drop: ALLRemoves all Linux capabilities from the container
no-new-privileges:truePrevents setuid/setgid binaries from escalating privileges
Non-root user (1000:1000)Container process runs without root, even if the daemon is compromised
Read-only config mountConfig cannot be modified from inside the container

Adjust based on your expected agent workload:

deploy:
resources:
limits:
memory: 512M
reservations:
memory: 128M

For heavier workloads (multiple concurrent agents, large context windows), increase the memory limit. The reservation ensures the container gets at least 128M even under host memory pressure.

The Docker image comes with Codex CLI and Gemini CLI pre-installed. You don’t need to install any provider CLI on the host — the image is self-contained.

CLIProviderPre-installed in image
Codex CLIcodexYes
Gemini CLIgeminiYes

This means your talond.yaml provider configuration works out of the box without any host-side setup. Just configure your API keys in .env and the container has everything it needs.

The container runs as a non-root user with predictable UID/GID 1000:1000. This matters for bind-mounts:

Terminal window
# On the host, ensure the config file is readable by UID 1000:
chown 1000:1000 config/talond.yaml
chmod 644 config/talond.yaml
# For the data directory (if bind-mounting instead of using a named volume):
mkdir -p /opt/talond/data
chown 1000:1000 /opt/talond/data

Named volumes (like talond-data) handle permissions automatically. Bind-mounts require the host directory to be accessible by UID 1000.

Instead of mounting the raw Docker socket, use a proxy that restricts which API endpoints are available:

# In docker-compose.yaml, add a proxy service:
services:
docker-proxy:
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
CONTAINERS: 1 # Allow container create/start/stop
IMAGES: 1 # Allow image pull/list
NETWORKS: 1 # Allow network operations
# Deny everything else by default
ports:
- "127.0.0.1:2375:2375"
talond:
environment:
DOCKER_HOST: tcp://docker-proxy:2375
# No Docker socket mount needed

Run the entire Docker daemon in rootless mode. The socket then grants only the user’s permissions, not root:

Terminal window
# Install and configure rootless Docker
dockerd-rootless-setuptool.sh install
systemctl --user enable docker
systemctl --user start docker
# The socket is at /run/user/1000/docker.sock
# Mount it instead of /var/run/docker.sock
LayerWhat it does
Non-root user (1000:1000)Process runs without root privileges
cap_drop: ALLNo Linux capabilities available
no-new-privileges:truesetuid/setgid binaries cannot escalate
Read-only configConfig cannot be tampered with from inside
Socket proxy (optional)Restrict Docker API access to needed endpoints
Rootless Docker (optional)Docker daemon itself runs without root
Error: EACCES: permission denied, open '/config/talond.yaml'

The container runs as UID 1000. Fix permissions on the host:

Terminal window
chown 1000:1000 config/talond.yaml
chmod 644 config/talond.yaml

If you’re using a provider that stores authentication state (e.g., Gemini CLI’s auth tokens), make sure the auth directory is inside a volume:

volumes:
- talond-data:/data
# Auth tokens live under /data, so they persist automatically

The config file is bind-mounted read-only. Changes on the host take effect after a container restart:

Terminal window
# Edit config/talond.yaml on the host, then:
docker compose -f deploy/docker-compose.yaml restart

Check the logs:

Terminal window
docker compose -f deploy/docker-compose.yaml logs talond

Common causes:

  • Missing or invalid config/talond.yaml
  • Missing .env file with required secrets
  • Config references environment variables that aren’t set in .env

If the container shows as unhealthy:

Terminal window
# Check what the health check sees
docker inspect talond --format='{{json .State.Health}}' | jq

The health check verifies the Node.js process is alive. If it’s failing, the daemon may have crashed — check the logs.