# Docker Quickstart Run Paperclip in Docker without installing Node or pnpm locally. All commands below assume you are in the **project root** (the directory containing `package.json`), not inside `docker/`. ## Building the image ```sh docker build -t paperclip-local . ``` The Dockerfile installs common agent tools (`git`, `gh`, `curl`, `wget`, `ripgrep`, `python3`) and the Claude, Codex, and OpenCode CLIs. Build arguments: | Arg | Default | Purpose | |-----|---------|---------| | `USER_UID` | `1000` | UID for the container `node` user (match your host UID to avoid permission issues on bind mounts) | | `USER_GID` | `1000` | GID for the container `node` group | | `CLI_TOOLS_CACHE_EPOCH` | empty | Refresh the CLI-install layer; CI supplies the current ISO week | | `PAPERCLIP_BUILD_VERSION` | empty | Runtime version when Git metadata is unavailable | | `PAPERCLIP_BUILD_COMMIT` | empty | Source commit written into the server build stamp and runtime environment | Changing the build version or commit preserves the CLI-install cache. The tool layer refreshes when its weekly epoch, base image, installation command, or earlier build inputs change. Local builds can set a new epoch explicitly to refresh tools without clearing the entire build cache. ```sh docker build -t paperclip-local \ --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) . ``` ## One-liner (build + run) ```sh docker build -t paperclip-local . && \ docker run --name paperclip \ -p 3100:3100 \ -e HOST=0.0.0.0 \ -e PAPERCLIP_HOME=/paperclip \ -e BETTER_AUTH_SECRET=$(openssl rand -hex 32) \ -e PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=$(openssl rand -hex 32) \ -v "$(pwd)/data/docker-paperclip:/paperclip" \ paperclip-local ``` Open: `http://localhost:3100` Data persistence: - Embedded PostgreSQL data - uploaded assets - local secrets key - local agent workspace data All persisted under your bind mount (`./data/docker-paperclip` in the example above). ## Docker Compose ### Quickstart (embedded SQLite) Single container, no external database. Data persists via a bind mount. ```sh BETTER_AUTH_SECRET=$(openssl rand -hex 32) \ PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=$(openssl rand -hex 32) \ docker compose -f docker/docker-compose.quickstart.yml up --build ``` Defaults: - host port: `3100` - persistent data dir: `./data/docker-paperclip` Optional overrides: ```sh PAPERCLIP_PORT=3200 PAPERCLIP_DATA_DIR=../data/pc \ docker compose -f docker/docker-compose.quickstart.yml up --build ``` **Note:** `PAPERCLIP_DATA_DIR` is resolved relative to the compose file (`docker/`), so `../data/pc` maps to `data/pc` in the project root. If you change host port or use a non-local domain, set `PAPERCLIP_PUBLIC_URL` to the external URL you will use in browser/auth flows. Pass `OPENAI_API_KEY` and/or `ANTHROPIC_API_KEY` to enable local adapter runs. ### Full stack (with PostgreSQL) Paperclip server + PostgreSQL 17. The database is health-checked before the server starts. ```sh BETTER_AUTH_SECRET=$(openssl rand -hex 32) \ docker compose -f docker/docker-compose.yml up --build ``` PostgreSQL data persists in a named Docker volume (`pgdata`). Paperclip data persists in `paperclip-data`. ### Untrusted PR review Isolated container for reviewing untrusted pull requests with Codex or Claude, without exposing your host machine. See `doc/UNTRUSTED-PR-REVIEW.md` for the full workflow. ```sh docker compose -f docker/docker-compose.untrusted-review.yml build docker compose -f docker/docker-compose.untrusted-review.yml run --rm --service-ports review ``` ## Authenticated Compose (Single Public URL) For authenticated deployments, set one canonical public URL and let Paperclip derive auth/callback defaults: ```yaml services: paperclip: environment: PAPERCLIP_DEPLOYMENT_MODE: authenticated PAPERCLIP_DEPLOYMENT_EXPOSURE: private PAPERCLIP_PUBLIC_URL: https://desk.koker.net ``` `PAPERCLIP_PUBLIC_URL` is used as the primary source for: - auth public base URL - Better Auth base URL defaults - bootstrap invite URL defaults - hostname allowlist defaults (hostname extracted from URL) For fresh `authenticated/private` Docker or appliance-style installs, the first admin can now be claimed entirely from the browser after sign-in. Open the Paperclip URL, sign in or create an account, then choose `Claim this instance` on the setup screen. This browser claim is disabled for `authenticated/public`; public deployments should run the high-entropy CLI invite fallback instead: ```sh pnpm paperclipai auth bootstrap-ceo ``` Granular overrides remain available if needed (`PAPERCLIP_AUTH_PUBLIC_BASE_URL`, `BETTER_AUTH_URL`, `BETTER_AUTH_TRUSTED_ORIGINS`, `PAPERCLIP_ALLOWED_HOSTNAMES`). Set `PAPERCLIP_ALLOWED_HOSTNAMES` explicitly only when you need additional hostnames beyond the public URL host (for example Tailscale/LAN aliases or multiple private hostnames). ### Optional Vercel Connect credentials Vercel Connect's backend integration is retained for controlled testing and existing Vercel-backed connections, but its new-connection UI is currently withheld from **Apps → Browse**. Setting `PAPERCLIP_VERCEL_CONNECT_ENABLED=true` does not expose a customer-facing setup entry. Native provider setup screens remain unchanged. Vercel-hosted deployments use the workload OIDC token Vercel injects. Other hosted and self-hosted deployments can provide `PAPERCLIP_VERCEL_CONNECT_ACCESS_TOKEN` as a deployment bootstrap secret only when that token type is accepted by the live Connect API: ```yaml services: paperclip: environment: PAPERCLIP_VERCEL_CONNECT_ENABLED: "true" PAPERCLIP_VERCEL_CONNECT_ACCESS_TOKEN: ${PAPERCLIP_VERCEL_CONNECT_ACCESS_TOKEN} ``` Do not save that access token in a company secret or connection config. It is instance bootstrap authority for the operator-selected Vercel account. A token's long expiry and broad Vercel scope do not prove Connect compatibility; validate it with connector metadata before rollout. Workload OIDC takes precedence when both authorities are present. Turning the feature flag off hides new Vercel-backed setup; existing connections keep resolving while workload OIDC or the bootstrap token remains available. Missing or invalid authority fails closed. See the [Vercel Connect operator guide](./connections/VERCEL-CONNECT.md). ## Claude + Codex Local Adapters in Docker The image pre-installs: - `claude` (Anthropic Claude Code CLI) - `codex` (OpenAI Codex CLI) If you want local adapter runs inside the container, pass API keys when starting the container: ```sh docker run --name paperclip \ -p 3100:3100 \ -e HOST=0.0.0.0 \ -e PAPERCLIP_HOME=/paperclip \ -e OPENAI_API_KEY=... \ -e ANTHROPIC_API_KEY=... \ -v "$(pwd)/data/docker-paperclip:/paperclip" \ paperclip-local ``` Notes: - Without API keys, the app still runs normally. - Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites. ## Podman Quadlet (systemd) The `docker/quadlet/` directory contains unit files to run Paperclip + PostgreSQL as systemd services via Podman Quadlet. | File | Purpose | |------|---------| | `docker/quadlet/paperclip.pod` | Pod definition — groups containers into a shared network namespace | | `docker/quadlet/paperclip.container` | Paperclip server — joins the pod, connects to Postgres at `127.0.0.1` | | `docker/quadlet/paperclip-db.container` | PostgreSQL 17 — joins the pod, health-checked | ### Setup 1. Build the image (see above). 2. Copy quadlet files to your systemd directory: ```sh # Rootless (recommended) cp docker/quadlet/*.pod docker/quadlet/*.container \ ~/.config/containers/systemd/ # Or rootful sudo cp docker/quadlet/*.pod docker/quadlet/*.container \ /etc/containers/systemd/ ``` 3. Create a secrets env file (keep out of version control): ```sh cat > ~/.config/containers/systemd/paperclip.env <` so bootstrap invite URLs and auth callbacks use the reachable host port instead of the container's internal `3100`. - In authenticated mode, the smoke script defaults `SMOKE_AUTO_BOOTSTRAP=true` and drives the real bootstrap path automatically: it signs up a real user, runs `paperclipai auth bootstrap-ceo` inside the container to mint a real bootstrap invite, accepts that invite over HTTP, and verifies board session access. - Run the script in the foreground to watch the onboarding flow; stop with `Ctrl+C` after validation. - Set `SMOKE_DETACH=true` to leave the container running for automation and optionally write shell-ready metadata to `SMOKE_METADATA_FILE`. - Set `SMOKE_CONTAINER_NAME` to fix the container's name up front. Automation that has to collect diagnostics when the script *fails* needs a name it already knows, rather than one it can only read back out of a successful run. Defaults to the image name. - The container's logs are dumped to `SMOKE_LOG_FILE` (default `$TMPDIR/.log`) before the script tears the container down, so a run that never became ready still leaves its logs behind. - The image definition is in `docker/Dockerfile.onboard-smoke`. ## General Notes - The `docker-entrypoint.sh` adjusts the container `node` user UID/GID at startup to match the values passed via `USER_UID`/`USER_GID`, avoiding permission issues on bind-mounted volumes. - Paperclip data persists via Docker volumes/bind mounts (compose) or at `~/.local/share/paperclip` (quadlet). ## Native Runner build cache The image compiles the native Runner in `runner-build`, before copying the application source. That stage includes the pinned Rust compiler, the complete Cargo workspace and lockfile, and the protocol schemas and fixtures embedded by Rust. Changes to those inputs rebuild the native binary. Ordinary server or UI changes can reuse it through the existing registry cache (`mode=max`). Each platform gets its own native build; no cross-architecture binary is reused. The application build inherits that stage and still runs the normal server build, including Cargo, binary staging, and generated-contract checks. Rust input file times are normalized in both stages so fresh checkouts do not force Cargo to rebuild unchanged source. Changes made by build scripts still reach Cargo's normal validation. The final application copy excludes Cargo's target directory as before. Cache misses only cost compilation time. Pull requests that change the Dockerfile, Docker ignore rules, or Runner native inputs also build the isolated `runner-build` target in `Docker Runner check`. This compiles against the actual reduced context and catches missing embedded inputs before the post-merge image build. It uses a GitHub-hosted runner with read-only repository access and does not publish images or cache artifacts.