# AgentDrive, self-hosted: Postgres and the AgentDrive image, nothing else. # # This is the quickstart in README.md, and `scripts/selfhost-smoke.sh` runs it # verbatim (under its own project name, so it never touches an install made # from this file). Three services: `postgres` (plain postgres:16 — schema.sql # uses only built-in tsvector), `migrate` (the image running apply_schema # once, after Postgres is healthy), and `api` (the image under # AUTH_MODE=local with the filesystem object store on a named volume, the # MCP transport served at /mcp by the sidecar inside the same container, and # the job scheduler running garbage collection and usage maintenance there # too — without it deleted content is never reclaimed). # # Settings come from a `.env` beside this file, which every `docker compose` # command reads: # AGENTDRIVE_SESSION_SECRET required; >= 32 chars (`openssl rand -hex 32`) # AGENTDRIVE_PORT host port, default 8080 # AGENTDRIVE_BIND host interface, default 127.0.0.1 (loopback); # 0.0.0.0 to reach it from other machines — put # TLS in front, the API key crosses in the clear # AGENTDRIVE_PUBLIC_BASE_URL the URL clients use; share links and the MCP # discovery document are built from it. Default # http://localhost: # AGENTDRIVE_DB_PASSWORD Postgres password, default agentdrive # COMPOSE_PROJECT_NAME overrides `name` below for a second install on # one machine (each gets its own volumes) # # The project name is fixed so this file never collides with the developer # compose (`docker-compose.yml`, project `drive`) on the same machine: both # define a `postgres` service, and the same project name would make one # recreate the other's container. name: agentdrive services: postgres: image: postgres:16 environment: POSTGRES_USER: agentdrive POSTGRES_PASSWORD: ${AGENTDRIVE_DB_PASSWORD:-agentdrive} POSTGRES_DB: agentdrive volumes: - pgdata:/var/lib/postgresql/data # Deliberately no `ports:` — the database is not published on the host. # It is still reachable, with this password, by any container joined to # this project's network, which is why the password is a knob. healthcheck: test: ["CMD-SHELL", "pg_isready -U agentdrive -d agentdrive"] interval: 3s timeout: 3s retries: 20 restart: unless-stopped migrate: build: . image: agentdrive:selfhost command: ["python", "-m", "agentdrive.scripts.apply_schema"] environment: DATABASE_URL: postgresql://agentdrive:${AGENTDRIVE_DB_PASSWORD:-agentdrive}@postgres:5432/agentdrive depends_on: postgres: condition: service_healthy restart: "no" api: build: . image: agentdrive:selfhost environment: DATABASE_URL: postgresql://agentdrive:${AGENTDRIVE_DB_PASSWORD:-agentdrive}@postgres:5432/agentdrive # This installation mints its own opaque API keys (`agentdrive.keys`); # no issuer, no signing key, no OAuth server. The hosted product's Hub # settings are refused under this mode, so none appear here. AUTH_MODE: local STORAGE_BACKEND: fs STORAGE_FS_ROOT: /data # Tracks the published port unless set explicitly: a share link built # on the wrong port is a link nobody can open. PUBLIC_BASE_URL: ${AGENTDRIVE_PUBLIC_BASE_URL:-http://localhost:${AGENTDRIVE_PORT:-8080}} # There is no console in a self-hosted install; empty removes the # "Open in console" button from every share page. CONSOLE_BASE_URL: "" SESSION_SECRET: ${AGENTDRIVE_SESSION_SECRET:?set AGENTDRIVE_SESSION_SECRET in .env, e.g. `echo "AGENTDRIVE_SESSION_SECRET=$(openssl rand -hex 32)" >> .env`} # Start the MCP sidecar inside the container and proxy /mcp to it over # loopback; the supervisor puts the sidecar into api-key mode. MCP_PROXY_URL: http://127.0.0.1:8081 PORT: "8080" # Run the maintenance jobs on their schedule (src/agentdrive/jobs/ # schedule.py) inside this container, so they can never be pointed at # a different database or store than the API. The last handled minute # persists on the data volume; a restart catches up what came due. SCHEDULER_ENABLED: "true" SCHEDULER_STATE_FILE: /data/.agentdrive-scheduler.json ports: - "${AGENTDRIVE_BIND:-127.0.0.1}:${AGENTDRIVE_PORT:-8080}:8080" volumes: - data:/data depends_on: migrate: condition: service_completed_successfully healthcheck: # The image has no curl; Python is already there. test: ["CMD", "python", "-c", "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3).status == 200 else 1)"] interval: 5s timeout: 5s retries: 30 # The supervisor gives its children 10 s to exit; Docker's default grace # period is also 10 s, so give the supervisor room to finish first. A # running job is terminated with its process group; its advisory lock is # released with its database connection and the next run resumes. stop_grace_period: 20s # A real PID 1 that reaps orphaned processes. init: true restart: unless-stopped volumes: pgdata: data: