Add a UI API client contract test for company-scoped subscription credentials and document BYO subscription caveats in the Docker guide.
10 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) \
-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) \
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).
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.
Bring Your Own Subscription
Paperclip's subscription-credential flow is per user, not a company-shared secret store. Each employee links their own official Claude or Codex subscription material from the profile/settings UI, and Paperclip stores the material encrypted at rest while only showing redacted metadata back to the board.
Keep these caveats in mind when you run a public deployment:
- Do not ask one employee to paste another employee's subscription token or
auth.json/.credentials.jsonfile. - Do not treat the linked credential as a shared service account.
- Runtime refreshes are written back to the same encrypted per-user record, so the stored copy stays authoritative after a CLI session rotates material.
- Live-provider checks still depend on the employee's local CLI login state or subscription account access; automated tests cover the storage and redaction paths, but they do not replace a real host-side login.
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) 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. - 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).