11 KiB
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
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 |
docker build -t paperclip-local \
--build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) .
One-liner (build + run)
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.
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:
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.
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.
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:
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:
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:
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.
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:
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
-
Build the image (see above).
-
Copy quadlet files to your systemd directory:
# 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/ -
Create a secrets env file (keep out of version control):
cat > ~/.config/containers/systemd/paperclip.env <<EOL BETTER_AUTH_SECRET=$(openssl rand -hex 32) PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=$(openssl rand -hex 32) POSTGRES_USER=paperclip POSTGRES_PASSWORD=paperclip POSTGRES_DB=paperclip DATABASE_URL=postgres://paperclip:paperclip@127.0.0.1:5432/paperclip # OPENAI_API_KEY=sk-... # ANTHROPIC_API_KEY=sk-... EOL -
Create the data directory and start:
mkdir -p ~/.local/share/paperclip systemctl --user daemon-reload systemctl --user start paperclip-pod
Quadlet management
journalctl --user -u paperclip -f # App logs
journalctl --user -u paperclip-db -f # DB logs
systemctl --user status paperclip-pod # Pod status
systemctl --user restart paperclip-pod # Restart all
systemctl --user stop paperclip-pod # Stop all
Quadlet notes
- First boot: Unlike Docker Compose's
condition: service_healthy, Quadlet'sAfter=only waits for the DB unit to start, not for PostgreSQL to be ready. On a cold first boot you may see one or two restart attempts injournalctl --user -u paperclipwhile PostgreSQL initialises — this is expected and resolves automatically viaRestart=on-failure. - Containers in a pod share
localhost, so Paperclip reaches Postgres at127.0.0.1:5432. - PostgreSQL data persists in the
paperclip-pgdatanamed volume. - Paperclip data persists at
~/.local/share/paperclip. - For rootful quadlet deployment, remove
%hprefixes and use absolute paths.
Onboard Smoke Test (Ubuntu + npm only)
Use this when you want to mimic a fresh machine that only has Ubuntu + npm and verify:
npx paperclipai onboard --yescompletes- the server binds to
0.0.0.0:3100so host access works - onboard/run banners and startup logs are visible in your terminal
Build + run:
./scripts/docker-onboard-smoke.sh
Open: http://localhost:3131 (default smoke host port)
Useful overrides:
HOST_PORT=3200 PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
PAPERCLIP_DEPLOYMENT_MODE=authenticated PAPERCLIP_DEPLOYMENT_EXPOSURE=private ./scripts/docker-onboard-smoke.sh
SMOKE_DETACH=true SMOKE_METADATA_FILE=/tmp/paperclip-smoke.env PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
Notes:
- Persistent data is mounted at
./data/docker-onboard-smokeby default. - Container runtime user id defaults to your local
id -uso the mounted data dir stays writable while avoiding root runtime. - Smoke script defaults to
authenticated/privatemode soHOST=0.0.0.0can be exposed to the host. - Smoke script defaults host port to
3131to avoid conflicts with local Paperclip on3100. - Smoke script also defaults
PAPERCLIP_PUBLIC_URLtohttp://localhost:<HOST_PORT>so bootstrap invite URLs and auth callbacks use the reachable host port instead of the container's internal3100. - In authenticated mode, the smoke script defaults
SMOKE_AUTO_BOOTSTRAP=trueand drives the real bootstrap path automatically: it signs up a real user, runspaperclipai auth bootstrap-ceoinside 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+Cafter validation. - Set
SMOKE_DETACH=trueto leave the container running for automation and optionally write shell-ready metadata toSMOKE_METADATA_FILE. - Set
SMOKE_CONTAINER_NAMEto 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/<container name>.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.shadjusts the containernodeuser UID/GID at startup to match the values passed viaUSER_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).