You've got ~5 minutes and want DAP running. This doc decides
for you which of the three deployment paths fits, then walks
through it concretely. Every other doc in docs/ goes deeper into
one slice — start here.
Q1. Who'll use it?
├─ Just me ────────────────────────────────► PATH A
└─ Team of 2-20
Q2. Where does it run?
├─ My laptop, ad-hoc ──────────────────────► PATH A
└─ Server / VPS (always on)
Q3. Database?
├─ SQLite file (default, simplest) ────────► PATH B
└─ Postgres (managed DB or sidecar) ───────► PATH C
| Path | Sounds like | Time to "logged in" |
|---|---|---|
| A — Local single-user (pipx + SQLite) | "I want to try it on my laptop" | ~5 min |
| B — Self-host on a VPS (Docker + SQLite) | "Small team, one machine, I'll add Postgres later" | ~15 min |
| C — Production (Docker + managed Postgres + TLS reverse proxy) | "Real users, real domain, real backups" | ~30-60 min |
You can always start at A and graduate to B or C later — the data
moves cleanly (SQLite → Postgres has a documented migration in
docs/admin-guide.md).
For: trying DAP on your dev machine. Single operator. Data
lives in ./.dap/state.db next to wherever you run dap.
| Tool | Version | Check |
|---|---|---|
| Python | 3.13+ | python3 --version |
| pipx | any | pipx --version |
| Node | 20+ (optional — for bundled dashboard) | node --version |
If node is missing, dap start will run engine-only and print a
hint. Install Node 20+ later via nvm install 20 if you want the
web UI.
# 1. Install the CLI (pulls dap-engine + bundled dashboard transitively).
pipx install dap-cli
# 2. Initialise the project (creates ./.dap/ + bootstraps admin).
dap init --admin-email=you@example.com
# Prompts for password (twice). Or use --admin-password=... for
# automation. Or pipe via stdin: echo 'pw' | dap init --admin-password-stdin
# 3. Start engine + dashboard.
dap start
# 4. Open the dashboard manually:
# Engine → http://localhost:7333 (FastAPI Swagger at /docs)
# Dashboard → http://localhost:7332 (Next.js UI)# In a second terminal:
curl http://localhost:7333/health
# → {"status":"ok","service":"dap-engine","version":"<x.y.z>",
# "db_dialect":"sqlite","timestamp":"..."}
dap status
# ✓ admin bootstrap: you@example.com (...)
# ● engine: running PID <pid> port 7333 uptime <duration>Login via the dashboard with the credentials you passed to
dap init. You should see the Admin link in the sidebar.
dap stop # graceful shutdown
dap start # again.dap/state.db persists between restarts in your CWD.
For: team of 2-20 sharing one always-on instance. Docker compose. SQLite by default (file in a named volume). Upgrade to Postgres later by uncommenting the sidecar in the compose file.
| Tool | Version | On the VPS |
|---|---|---|
| Docker | 24+ | docker --version |
| Docker compose | v2 | docker compose version |
| Reverse proxy with TLS | recommended | Caddy 2 / nginx / Traefik |
| Open ports | 80/443 to internet, 3000 internal | ss -tlnp |
# 1. Grab the compose example from the repo (or clone).
mkdir -p /opt/dap && cd /opt/dap
curl -O https://raw.githubusercontent.com/lagowski/dap/main/examples/standalone/docker-compose.yml
curl -O https://raw.githubusercontent.com/lagowski/dap/main/examples/standalone/.env.example
# 2. Configure secrets.
cp .env.example .env
# Edit .env — the only REQUIRED value is DAP_AUTH_JWT_SECRET.
# Mint one with: openssl rand -hex 32.
# Also pin DAP_IMAGE_TAG=0.3.0 (or whichever tag you intend), so
# compose doesn't silently pull `latest`.
nano .env
# 3. Start.
docker compose pull # fetches ghcr.io/lagowski/dap:$DAP_IMAGE_TAG
docker compose up -d
# 4. Bootstrap the admin (idempotent — re-run with --force later
# if you forget the password).
# Omitting --admin-password triggers auto-generation; the random
# password is printed exactly once on stdout, capture it from
# the command output below.
docker compose exec dap dap init \
--admin-email=you@example.com --force
# → "⚠ Generated random password — copy it now: <random>"# Engine health (inside compose network):
docker compose exec dap curl http://localhost:7333/health
# Dashboard externally:
curl http://<vps-ip>:3000/ # → 200 OK on login page
docker compose ps # → dap should be "healthy"
# Bootstrap state:
docker compose exec dap dap statusPut Caddy in front (one-line config, auto-HTTPS via Let's Encrypt):
# /etc/caddy/Caddyfile
dap.example.com {
reverse_proxy localhost:3000
}Now https://dap.example.com works; only Caddy is public. Full
nginx + rate-limiting template in security.md.
Uncomment the postgres service in docker-compose.yml and set
DAP_DATABASE_URL in .env. See Path C below for what that looks
like — same compose file, just different env var.
For: real users, real domain, real backups. Postgres handles concurrent writes properly; managed (RDS / Supabase / Crunchy Bridge / Aiven / DO Managed) handles backups, point-in-time recovery, automated upgrades.
Internet
│
▼ HTTPS :443
┌──────────────┐
│ Caddy / nginx│ (TLS termination + rate limiting)
└──────┬───────┘
│ HTTP :3000
▼
┌──────────────────┐
│ DAP container │ ghcr.io/lagowski/dap:<version>
│ - Engine :7333 │ (engine + dashboard, monolith)
│ - Dashboard:3000│
└────────┬─────────┘
│ TCP :5432 over private network or internet+TLS
▼
┌────────────────┐
│ Postgres │ managed (RDS / Supabase / ...) or sidecar
│ (separate) │
└────────────────┘
Everything from Path B, plus:
- A Postgres database accessible from the VPS — managed service or a separate Postgres container/server.
- A connection URL:
postgresql+asyncpg://user:pass@host:5432/dbname.
# 1-2. Same as Path B (compose + .env).
cd /opt/dap
# ...
# 3. Set DAP_DATABASE_URL in .env. Comment out DAP_DB_PATH.
echo 'DAP_DATABASE_URL=postgresql+asyncpg://dap:secret@db.example.com:5432/dap_prod' >> .env
# 4. Start. Engine detects DAP_DATABASE_URL, runs migrations on
# the Postgres schema automatically (in-code, no separate
# alembic step).
docker compose up -d
# 5. Bootstrap admin against Postgres.
# NOTE: `dap init` is SQLite-only today; for Postgres deployments
# the admin user is created via the running engine's HTTP API:
docker compose exec dap python -c "
import httpx
r = httpx.post('http://127.0.0.1:7333/auth/register',
json={'email': 'you@example.com', 'password': 'CHANGE_ME_xyz'})
print(r.status_code, r.text)
"
# Then promote that row to admin (in-DB UPDATE — no admin endpoint
# for this yet; a managed UI lands in v0.4):
docker compose exec dap python -c "
import asyncio
from dap_engine.persistence.db import create_async_engine_for_url
from sqlalchemy import text
async def main():
eng = create_async_engine_for_url(
'postgresql+asyncpg://dap:secret@db.example.com:5432/dap_prod', None)
async with eng.begin() as conn:
await conn.execute(
text(\"UPDATE users SET is_superuser=TRUE WHERE email=:e\"),
{'e': 'you@example.com'})
asyncio.run(main())
"A dap init Postgres mode is on the v0.4 roadmap; until then the
two-step register-then-promote above is the documented path.
# 1. Confirm engine started against Postgres (not silently fell
# back to SQLite). The /health endpoint returns the active
# SQLAlchemy dialect:
docker compose exec dap curl -s http://127.0.0.1:7333/health
# → {"status":"ok",...,"db_dialect":"postgresql","timestamp":"..."}
# 2. Check the engine log line that announces which backend it
# bound to on startup:
docker compose logs dap | grep -i "database"
# → engine.db ... using Postgres backend (postgresql+asyncpg)Required. Use the Caddy snippet from Path B, or the full nginx
template from security.md which adds rate limiting
on /auth/* (recommended for any public deployment).
Managed Postgres handles this. If you're running Postgres yourself (sidecar mode):
# Daily backup via cron:
docker compose exec -T postgres \
pg_dump -U dap dap | gzip > "/var/backups/dap-$(date +%F).sql.gz"Test restore quarterly. Backup is not a backup until you've restored from it.
Before opening the dashboard for the first time, run through this quick sanity list:
| Check | Command | Expected |
|---|---|---|
| Engine alive | curl <engine-url>/health |
{"status":"ok",...} |
| Dashboard alive | curl -fI <dashboard-url>/ |
HTTP/1.1 200 |
| Admin bootstrapped | dap status (or docker compose exec dap dap status) |
✓ admin bootstrap: you@example.com (...) |
| DB reachable | <engine-url>/health returns version + no startup errors in logs |
Check docker compose logs dap |
| JWT secret set (prod) | docker compose exec dap printenv DAP_AUTH_JWT_SECRET | wc -c |
≥ 32 chars (not the per-process random fallback) |
| CORS allow-list set (prod) | docker compose exec dap printenv DAP_CORS_ORIGINS |
Your dashboard origin(s), comma-separated |
If anything in this table fails, jump to the troubleshooting matrix
in self-hosting.md.
After install, every agent needs to know which LLM to call and
how to authenticate. DAP ships 8 runtime adapters; you pick one
per agent in runtime_config. The choice has big consequences for
deployment, especially in Docker.
| Runtime | What it does | Needs binary on PATH? | Auth |
|---|---|---|---|
api-call |
HTTPS to provider's API | ❌ No | Env var (e.g. ANTHROPIC_API_KEY) |
claude-code |
subprocess to claude CLI |
✅ Yes | ~/.claude/ OAuth state OR ANTHROPIC_API_KEY |
gemini-cli |
subprocess to gemini CLI |
✅ Yes | ~/.config/google-generative-ai/ OAuth OR GEMINI_API_KEY |
codex |
subprocess to codex CLI |
✅ Yes | ~/.codex/ OAuth OR OPENAI_API_KEY |
aider |
subprocess to aider CLI (stub — lands later) |
✅ Yes | provider's API key env var |
bash |
Arbitrary shell command | system bash |
n/a (whatever the script needs) |
python-func |
In-process Python callable | ❌ No | n/a (runs in the engine process) |
http |
Generic HTTP request | ❌ No | Whatever the endpoint wants |
CLI runtimes (claude-code, gemini-cli, codex, aider) let you
reuse provider OAuth logins (e.g. Claude Code Pro / Max plan
without per-token billing). api-call is simpler but pays per
token.
The default Docker image (ghcr.io/lagowski/dap:0.3.0) ships with
Python + Node + the bundled dashboard — NOT with the LLM
CLIs. If you want claude-code / gemini-cli / codex /
aider to work from a Dockerised DAP, you have three options:
Skip CLI runtimes entirely. Everything claude-code does, you can
do via api-call with provider: "anthropic". Pass the API keys
as env vars to the container.
# docker-compose.yml
services:
dap:
image: ghcr.io/lagowski/dap:0.3.0
environment:
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
OPENAI_API_KEY: ${OPENAI_API_KEY}
GEMINI_API_KEY: ${GEMINI_API_KEY}
GLM_API_KEY: ${GLM_API_KEY}Then in each agent's runtime_config (dashboard → /agents/<id>/edit):
{
"provider": "anthropic",
"model_id": "claude-opus-4-7",
"api_key_env": "ANTHROPIC_API_KEY",
"max_tokens": 4096
}The engine reads os.environ["ANTHROPIC_API_KEY"] at call time.
The key never lands in the database or logs. Covers >90% of use
cases.
If you need a CLI runtime (e.g. using a Claude Code Pro
subscription, or aider for repo-scoped edits):
# Dockerfile.dap-with-clis
FROM ghcr.io/lagowski/dap:0.3.0
USER root
RUN npm install -g @anthropic-ai/claude-code @google-ai/gemini-cli \
&& pip install aider-chat \
# codex CLI install per its own README
&& true
USER 1000# docker-compose.yml
services:
dap:
build:
context: .
dockerfile: Dockerfile.dap-with-clis
volumes:
# Share OAuth state from host (read-only).
- ${HOME}/.claude:/home/dap/.claude:ro
- ${HOME}/.config/google-generative-ai:/home/dap/.config/google-generative-ai:ro
environment:
HOME: /home/dap # CLIs look up $HOME/.claude etc.
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} # fallback for api-call agentsLimitations:
- The mounted OAuth state is read-only.
claude auth loginfrom inside the container won't work (no browser, no$DISPLAY). Log in on the host first, container just reads the state. - Anyone with root in the container can read your OAuth tokens. Acceptable for self-hosted single-team, not "secure by default".
- CLI updates require image rebuild.
If the host already has claude / gemini / codex configured
and you have permission to install Python packages there, this is
the cleanest path:
# Single host, single $HOME, native processes — no container.
pipx install dap-cli
dap init --admin-email=ops@example.com --admin-password=$(openssl rand -hex 16)
dap startDAP runs as your user, uses your $PATH (finds claude etc.),
reads your ~/.claude/ directly. Zero duplication of credentials,
zero mounts.
Strategy 3 is also the answer for any environment where Docker
isn't available (locked-down VPS, internal STG / dev hosts with
strict admin policies, etc.). Python 3.13+ is the only hard
requirement. Add a systemd unit if you need it to survive
reboots — see self-hosting.md
for the unit file template.
| Layer | Where | What it controls |
|---|---|---|
| Engine startup | docker-compose.yml environment: (Docker) OR .env.local sourced before dap start (pipx) OR systemd Environment= directive |
DAP_AUTH_JWT_SECRET, DAP_DATABASE_URL, DAP_CORS_ORIGINS, provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, GLM_API_KEY, ...) |
| Per-agent runtime | Dashboard /agents/<id>/edit → "Runtime config" block, stored in agents.runtime_config JSON column |
Which runtime, which provider, which model, api_key_env (name of env var to read), max_tokens, temperature, CLI-specific paths |
| Per-pipeline run | Trigger form / POST /runs initial_state |
Per-run overrides — input values fed into the first agent |
| Provider OAuth state | Host filesystem (~/.claude/, ~/.config/google-generative-ai/, etc.) — bind-mounted into Docker via volumes if needed |
Long-lived OAuth tokens for CLI runtimes when you don't want per-token API keys |
When you change runtime_config on an agent, the change is
immediate — every subsequent run uses the new config. Existing
in-flight runs keep their snapshot. Engine doesn't need a restart.
When you change a startup env var (ANTHROPIC_API_KEY, etc.),
you do need to restart the engine for it to pick up the new
value (docker compose restart dap, or dap stop && dap start).
The engine reads env at startup only, not per-request.
| Your situation | Use |
|---|---|
Local laptop, you already have claude CLI logged in |
Strategy 3 (pipx) — your claude auth state just works |
| VPS without Docker, want simple ops | Strategy 3 + systemd unit |
| VPS with Docker, ops via compose, fine paying per-token | Strategy 1 — api-call only, API keys in .env |
| VPS with Docker, want Claude Code Pro subscription | Strategy 2 — custom image + OAuth mount |
| Mixed team, some pipelines per-token, some CLI | Strategy 2 — covers both since api-call agents just ignore the bundled CLIs |
Once you're logged in:
- First pipeline —
README.md - Invite a teammate —
admin-guide.mdwalks through/admin/users - GitHub / Google login —
auth.mdfor the OAuth app registration walkthrough - API tokens for CI / scripts —
auth.mdfor mint + revoke - Backups, JWT rotation, recovery from lost admin password —
admin-guide.md - Hardening checklist —
security.md(12 items, in impact order) - Cutting a release of your own fork —
release.md
Order of operations for triage:
docker compose logs dap --tail=100(ordapstdout for Path A) — startup errors land here.dap status— confirms the bootstrap marker exists and engine is alive.- Try the symptom in
self-hosting.mdtroubleshooting — most common failure modes are listed. - If it's auth-related,
auth.mdtroubleshooting. - If you've lost the admin password,
admin-guide.mdrecovery.