2026-02-26 10:32:33 -06:00
# Docker Quickstart
Run Paperclip in Docker without installing Node or pnpm locally.
2026-04-01 11:06:37 +00:00
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 |
```sh
docker build -t paperclip-local \
--build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) .
```
2026-02-26 10:32:33 -06:00
## 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 \
2026-04-01 11:06:37 +00:00
-e BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
2026-02-26 10:32:33 -06:00
-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).
2026-04-01 11:06:37 +00:00
## Docker Compose
### Quickstart (embedded SQLite)
Single container, no external database. Data persists via a bind mount.
2026-02-26 10:32:33 -06:00
```sh
2026-04-01 11:06:37 +00:00
BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
docker compose -f docker/docker-compose.quickstart.yml up --build
2026-02-26 10:32:33 -06:00
```
Defaults:
- host port: `3100`
- persistent data dir: `./data/docker-paperclip`
Optional overrides:
```sh
2026-04-01 11:06:37 +00:00
PAPERCLIP_PORT=3200 PAPERCLIP_DATA_DIR=../data/pc \
docker compose -f docker/docker-compose.quickstart.yml up --build
2026-02-26 10:32:33 -06:00
```
2026-04-01 11:06:37 +00:00
**Note:** `PAPERCLIP_DATA_DIR` is resolved relative to the compose file (`docker/` ), so `../data/pc` maps to `data/pc` in the project root.
2026-03-05 18:33:49 -03:00
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.
2026-04-01 11:06:37 +00:00
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
```
2026-03-05 17:55:34 -03:00
## 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)
[codex] Add private browser first-admin claim flow (#6755)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Fresh self-hosted deployments need an operator path before any
invite exists.
> - Umbrel installs are private LAN deployments, so a one-time browser
claim is appropriate only when the deployment is private and unclaimed.
> - Public deployments and installs with active invites must keep the
existing invite-only model so admin creation is not exposed broadly.
> - GitHub PR #2927 established the useful direction, but it needed to
be adapted onto current `master` rather than merged as-is.
> - This pull request adds that adapted private-only claim flow across
server, UI, docs, and regression coverage.
> - The benefit is that a fresh private Umbrel-style install can be
claimed from the browser without weakening public deployment access.
## What Changed
- Added a first-admin claim service and access route support for
one-time admin claim eligibility on private unclaimed deployments.
- Updated the bootstrap/access UI so eligible private installs show a
setup claim path, while public and invited deployments keep invite-first
behavior.
- Added a bootstrap-pending setup UX lab covering claim, invite, public,
and signed-in access states.
- Updated deployment and local development docs for authenticated
private/public behavior and the Umbrel-style claim path.
- Added server and UI regression tests for private claim, public
no-claim, active invite fallback, existing board/no-access flows, and
health exposure reporting.
- Stabilized PR handoff verification by serializing the aggregate server
Vitest workspace run, forcing `NODE_ENV=test`, and relaxing the
heartbeat batching test around legitimate recovery follow-up runs.
## Verification
- `pnpm -r typecheck`
- `pnpm build`
- `pnpm vitest --run
server/src/__tests__/heartbeat-comment-wake-batching.test.ts`
- `pnpm vitest --run
server/src/__tests__/health-dev-server-token.test.ts`
- `pnpm test:run`
- QA validation: PAP-10115 passed browser validation with screenshots
for private fresh install claim, active invite versus claim conflict,
public invite-only/claim-absent behavior, existing invite fallback, and
normal board/no-access flows.
- GitHub closeout: issue #2579 and PR #2927 were updated with the
accepted direction: adapt the implementation, do not direct-merge #2927
as-is.
## Risks
- The claim endpoint must remain private-only and one-time; a regression
here could expose admin creation on public deployments.
- Existing invite behavior must remain intact for public deployments and
installs that already have an active invite.
- The stable Vitest harness now serializes the aggregate server
workspace group; this is slower, but it avoids DB-backed suite
collisions under root workspace mode.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected - check the roadmap
first. See `CONTRIBUTING.md`.
>
> ROADMAP.md checked: this is a scoped deployment bootstrap/access fix
and does not duplicate a listed roadmap project.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` for product
engineering, implementation, and verification, with tool-enabled local
code execution. Paperclip QA browser validation was performed in
PAP-10115 by the assigned QA agent; exact adapter model metadata for
that QA run is not exposed in this PR context.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-27 21:15:01 -10:00
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
```
2026-03-05 17:55:34 -03:00
Granular overrides remain available if needed (`PAPERCLIP_AUTH_PUBLIC_BASE_URL` , `BETTER_AUTH_URL` , `BETTER_AUTH_TRUSTED_ORIGINS` , `PAPERCLIP_ALLOWED_HOSTNAMES` ).
2026-03-05 18:33:49 -03:00
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).
2026-02-26 10:32:33 -06:00
## 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.
2026-03-04 10:15:11 -06:00
2026-04-01 11:06:37 +00:00
## 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/
```
2026-03-15 14:18:56 -05:00
2026-04-01 11:06:37 +00:00
3. Create a secrets env file (keep out of version control):
2026-03-15 14:18:56 -05:00
2026-04-01 11:06:37 +00:00
```sh
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
```
4. Create the data directory and start:
```sh
mkdir -p ~/.local/share/paperclip
systemctl --user daemon-reload
systemctl --user start paperclip-pod
```
### Quadlet management
```sh
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's `After=` 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 in `journalctl --user -u paperclip` while PostgreSQL initialises — this is expected and resolves automatically via `Restart=on-failure` .
- Containers in a pod share `localhost` , so Paperclip reaches Postgres at `127.0.0.1:5432` .
- PostgreSQL data persists in the `paperclip-pgdata` named volume.
- Paperclip data persists at `~/.local/share/paperclip` .
- For rootful quadlet deployment, remove `%h` prefixes and use absolute paths.
2026-03-15 14:18:56 -05:00
2026-03-04 10:15:11 -06:00
## 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 --yes` completes
- the server binds to `0.0.0.0:3100` so host access works
2026-03-04 10:48:36 -06:00
- onboard/run banners and startup logs are visible in your terminal
2026-03-04 10:15:11 -06:00
Build + run:
```sh
./scripts/docker-onboard-smoke.sh
```
2026-03-04 10:48:36 -06:00
Open: `http://localhost:3131` (default smoke host port)
2026-03-04 10:15:11 -06:00
Useful overrides:
```sh
HOST_PORT=3200 PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
2026-03-04 10:42:07 -06:00
PAPERCLIP_DEPLOYMENT_MODE=authenticated PAPERCLIP_DEPLOYMENT_EXPOSURE=private ./scripts/docker-onboard-smoke.sh
2026-03-18 07:59:32 -05:00
SMOKE_DETACH=true SMOKE_METADATA_FILE=/tmp/paperclip-smoke.env PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
2026-03-04 10:15:11 -06:00
```
Notes:
- Persistent data is mounted at `./data/docker-onboard-smoke` by default.
2026-03-04 10:42:07 -06:00
- Container runtime user id defaults to your local `id -u` so the mounted data dir stays writable while avoiding root runtime.
2026-03-04 10:48:36 -06:00
- Smoke script defaults to `authenticated/private` mode so `HOST=0.0.0.0` can be exposed to the host.
- Smoke script defaults host port to `3131` to avoid conflicts with local Paperclip on `3100` .
2026-03-09 14:41:00 -05:00
- Smoke script also defaults `PAPERCLIP_PUBLIC_URL` to `http://localhost:<HOST_PORT>` so bootstrap invite URLs and auth callbacks use the reachable host port instead of the container's internal `3100` .
2026-03-09 15:30:08 -05:00
- 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.
2026-03-04 10:48:36 -06:00
- Run the script in the foreground to watch the onboarding flow; stop with `Ctrl+C` after validation.
2026-03-18 07:59:32 -05:00
- Set `SMOKE_DETACH=true` to leave the container running for automation and optionally write shell-ready metadata to `SMOKE_METADATA_FILE` .
2026-04-01 11:06:37 +00:00
- 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).