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.
Quick start
Section titled “Quick start”# 1. Copy and edit the example configcp config/talond.example.yaml config/talond.yaml# Edit config/talond.yaml with your API keys and channel settings
# 2. Set up environment variablescp deploy/.env.example .env# Fill in your secrets (API keys, tokens)
# 3. Start the daemondocker compose -f deploy/docker-compose.yaml up -d
# 4. Verify it's runningdocker compose -f deploy/docker-compose.yaml psdocker compose -f deploy/docker-compose.yaml logs -fThat’s it. One docker compose up -d and Talon is running with health checks, automatic restarts, and resource limits.
Dockerfile
Section titled “Dockerfile”The image uses a multi-stage build to keep the final image small:
| Stage | Base image | Purpose |
|---|---|---|
builder | node:24-slim | Installs all dependencies, compiles TypeScript to dist/ |
production | node:24-slim | Minimal runtime with only production dependencies |
Key details
Section titled “Key details”- 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:talondwith fixed UID/GID1000: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 handlingRUN 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/GIDRUN groupadd --gid 1000 talond && \ useradd --uid 1000 --gid talond --no-create-home --shell /usr/sbin/nologin talond
# Switch to non-root userUSER talond
# Health check via PID fileHEALTHCHECK --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"]docker-compose.yaml
Section titled “docker-compose.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:
Volumes
Section titled “Volumes”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| Volume | Type | Purpose |
|---|---|---|
talond-data:/data | Named | SQLite database, PID file, IPC sockets — persists across restarts |
../config/talond.yaml:/config/talond.yaml | Bind-mount (read-only) | Your configuration — edit on the host, the container picks it up on restart |
Environment
Section titled “Environment”Secrets are loaded from a .env file and never committed to version control:
env_file: - ../.env
environment: NODE_ENV: productionDocker socket (optional)
Section titled “Docker socket (optional)”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.sockHealth check
Section titled “Health check”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: 3Security hardening
Section titled “Security hardening”The compose file applies defense-in-depth beyond what the Dockerfile alone provides:
# Drop ALL Linux capabilities — Talon doesn't need anycap_drop: - ALL
# Prevent privilege escalation via setuid/setgid binariessecurity_opt: - no-new-privileges:true| Setting | Effect |
|---|---|
cap_drop: ALL | Removes all Linux capabilities from the container |
no-new-privileges:true | Prevents 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 mount | Config cannot be modified from inside the container |
Resource limits
Section titled “Resource limits”Adjust based on your expected agent workload:
deploy: resources: limits: memory: 512M reservations: memory: 128MFor 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.
Baked CLIs
Section titled “Baked CLIs”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.
| CLI | Provider | Pre-installed in image |
|---|---|---|
| Codex CLI | codex | Yes |
| Gemini CLI | gemini | Yes |
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.
Permissions
Section titled “Permissions”The container runs as a non-root user with predictable UID/GID 1000:1000. This matters for bind-mounts:
# On the host, ensure the config file is readable by UID 1000:chown 1000:1000 config/talond.yamlchmod 644 config/talond.yaml
# For the data directory (if bind-mounting instead of using a named volume):mkdir -p /opt/talond/datachown 1000:1000 /opt/talond/dataNamed volumes (like talond-data) handle permissions automatically. Bind-mounts require the host directory to be accessible by UID 1000.
Security
Section titled “Security”Docker socket proxy
Section titled “Docker socket proxy”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 neededRootless Docker
Section titled “Rootless Docker”Run the entire Docker daemon in rootless mode. The socket then grants only the user’s permissions, not root:
# Install and configure rootless Dockerdockerd-rootless-setuptool.sh installsystemctl --user enable dockersystemctl --user start docker
# The socket is at /run/user/1000/docker.sock# Mount it instead of /var/run/docker.sockDefense-in-depth summary
Section titled “Defense-in-depth summary”| Layer | What it does |
|---|---|
Non-root user (1000:1000) | Process runs without root privileges |
cap_drop: ALL | No Linux capabilities available |
no-new-privileges:true | setuid/setgid binaries cannot escalate |
| Read-only config | Config 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 |
Troubleshooting
Section titled “Troubleshooting”Permission denied on config file
Section titled “Permission denied on config file”Error: EACCES: permission denied, open '/config/talond.yaml'The container runs as UID 1000. Fix permissions on the host:
chown 1000:1000 config/talond.yamlchmod 644 config/talond.yamlAuth directory not persisting
Section titled “Auth directory not persisting”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 automaticallyConfig changes not taking effect
Section titled “Config changes not taking effect”The config file is bind-mounted read-only. Changes on the host take effect after a container restart:
# Edit config/talond.yaml on the host, then:docker compose -f deploy/docker-compose.yaml restartContainer exits immediately
Section titled “Container exits immediately”Check the logs:
docker compose -f deploy/docker-compose.yaml logs talondCommon causes:
- Missing or invalid
config/talond.yaml - Missing
.envfile with required secrets - Config references environment variables that aren’t set in
.env
Health check failing
Section titled “Health check failing”If the container shows as unhealthy:
# Check what the health check seesdocker inspect talond --format='{{json .State.Health}}' | jqThe health check verifies the Node.js process is alive. If it’s failing, the daemon may have crashed — check the logs.