389 lines
17 KiB
Markdown
389 lines
17 KiB
Markdown
# 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) .
|
|
```
|
|
|
|
## Cloud image addresses
|
|
|
|
The Docker workflow publishes the managed deployment image for Linux AMD64.
|
|
`Cloud readiness` starts `Docker cloud` on each master push independently of the
|
|
multi-platform self-hosted build. Different commits use separate concurrency groups and existing
|
|
GitHub-hosted runners, so an older production or cloud build does not hold the
|
|
new commit in a workflow queue. Available GitHub runner capacity still applies.
|
|
Release tags and manual `Docker` dispatches call the same cloud build workflow.
|
|
|
|
Each commit exports to its own `buildcache-cloud-<FULL_SHA>` registry tag.
|
|
Builds import the current commit and nine first-parent ancestors, plus the
|
|
legacy `buildcache-cloud` fallback. This preserves reusable layers without
|
|
letting concurrent builds overwrite one shared cache manifest. Retain recent
|
|
cache tags if registry cleanup is configured; deleting them makes builds colder.
|
|
|
|
Cloud CI skips SDK and cache cleanup when both the Docker data filesystem and
|
|
the checkout filesystem have at least 64 GiB available. Below that conservative
|
|
headroom threshold, or when the measurement fails, it retains the existing
|
|
cleanup. The threshold selects the fast path; it is not a new minimum disk
|
|
requirement for local builds or smaller runners.
|
|
|
|
After the pushed image passes its Sentry and orphan-reaping checks, the workflow verifies its
|
|
commit label and platform and adds `ghcr.io/paperclipai/paperclip:sha-<full-commit-sha>-cloud`.
|
|
This address lets commit-based deployment tooling reuse the normal build.
|
|
Existing short-SHA and release tags remain available.
|
|
|
|
The full-SHA tag identifies the source commit. It does not certify that source
|
|
tests passed or that a compatible database migrator is available. Deployment
|
|
tooling must still check those prerequisites and pin the resolved image digest;
|
|
a rebuild of the same source can update the tag's digest.
|
|
|
|
The separate [cloud readiness check](cloud-build-readiness.md) combines source
|
|
verification, successful cloud image checks, and exact-source migrator
|
|
availability. It runs outside the full npm release's concurrency queue.
|
|
|
|
## 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 <<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
|
|
```
|
|
|
|
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.
|
|
|
|
## 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
|
|
- onboard/run banners and startup logs are visible in your terminal
|
|
|
|
Build + run:
|
|
|
|
```sh
|
|
./scripts/docker-onboard-smoke.sh
|
|
```
|
|
|
|
Open: `http://localhost:3131` (default smoke host port)
|
|
|
|
Useful overrides:
|
|
|
|
```sh
|
|
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-smoke` by default.
|
|
- Container runtime user id defaults to your local `id -u` so the mounted data dir stays writable while avoiding root runtime.
|
|
- 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`.
|
|
- 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`.
|
|
- 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/<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.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. A pinned `cargo-chef` generates a dependency recipe in
|
|
`runner-plan`. The separate `runner-deps` stage compiles that recipe with the
|
|
package-owned Rust compiler. Both the dependency build and the real binary use
|
|
the release profile and locked Cargo dependencies. The recipe stage never
|
|
modifies source in the checkout.
|
|
|
|
Changes to Rust source or embedded protocol inputs rebuild the real binary but
|
|
can reuse compiled dependencies when the recipe is unchanged. Dependency
|
|
manifests, the Cargo lockfile, target metadata, or compiler changes invalidate
|
|
the relevant cache. Ordinary server or UI changes can reuse the entire native
|
|
build through the existing registry cache (`mode=max`). Each platform gets its
|
|
own native build; no cross-architecture binary is reused. No additional GitHub
|
|
Actions cache is created. A cold build also installs the recipe generator and
|
|
compiles dependencies, so the savings apply after those layers are available.
|
|
|
|
Cloud builds import one registry cache: the first available full-SHA cache in
|
|
the current commit's ten-entry first-parent ancestry, with the legacy cache
|
|
as a final fallback. Each build still exports its own SHA cache with
|
|
`mode=max`. In fresh-builder checks, importing several historical manifests
|
|
missed native layers that a single matching manifest reused. The selector
|
|
inspects metadata after Docker login, stops at the first available cache, and
|
|
permits a cold build if no cache can be read.
|
|
|
|
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`.
|
|
The check runs `bash scripts/check-docker-runner-cache.sh` against a disposable
|
|
copy of tracked source and the actual Docker ignore rules. It compiles a baseline
|
|
and exports a local cache, removes that builder, changes a Rust metadata constant,
|
|
and rebuilds on a fresh builder using only the exported cache. It requires a
|
|
cached dependency build, an unchanged dependency recipe, and changed metadata
|
|
from the real binary. It also verifies that a dependency declaration change
|
|
alters the recipe. The probe exports small metadata results instead of importing
|
|
a large test image into the Docker daemon. Temporary builders and cache files
|
|
are removed afterward. It catches missing embedded inputs before the post-merge
|
|
build. It uses a GitHub-hosted runner with read-only repository access and never
|
|
publishes images or registry caches. Allow up to 20 minutes for its cold build and
|
|
source rebuild.
|