## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Core publishes standard images and source verification for downstream services. > - Managed services can now compose private images from the signed standard image. > - Core still builds a second public cloud image on every master push and release. > - That duplicate producer consumes build capacity and retains an obsolete readiness contract. > - This pull request retires recurring cloud publication while preserving the standard producer and rollback artifacts. ## Linked Issues or Issue Description Refs #13797 and #13789. Related: #12856 changes image dependency packaging; it does not retire this producer. **What existing behavior does this improve?** Core's recurring Docker publication and Cloud readiness workflow. **Current behavior** Master pushes call the legacy cloud publisher from Cloud readiness. Release tags and manual Docker runs call it too. Canary promotion also requires the legacy image. **Proposed behavior** Publish standard Core images and retain `Cloud source verified v1`. Let downstream services build their managed image. Keep explicit commit previews and existing images available. ## What Changed - Remove `docker-cloud.yml`, its master and release callers, and its unused cache selector. - Remove the legacy image/migrator wait and `Cloud deployable v1` job. Keep the full source verification workflow and exact source-proof name. - Make canary promotion inspect and promote the standard image only. - Preserve signed standard-image publication, direct migrator publication, and explicit `release.yml` previews. The preview path still uses the Dockerfile `cloud` target. - Update workflow, preview, build-stamp, and packaging tests. Exercise the promotion shell with mocked registry commands, including missing-image and missing-tag cases. - Document frozen legacy aliases, consumer requirements, preview compatibility, and rollback retention. ## Verification - All 377 workflow tests pass: `node --test .github/scripts/tests/*.test.mjs`. - All 129 release-registry tests pass: `pnpm test:release-registry`. - Focused source-proof, standard-image, preview, and workflow tests pass: 256 tests. - Focused image packaging/build-stamp tests pass: 16 tests. - Actionlint passes on all three changed workflow files. `git diff --check` passes. - Full local `pnpm build` and `pnpm -r typecheck` pass. - The policy follow-up updates an old assertion that required the removed readiness job. All 37 source-proof/release-workflow tests pass locally. - Full local `pnpm test:run` did not complete successfully while the Mac ran out of disk space. No full-suite pass is claimed. Removed 1.2 GiB of generated Cargo output from this isolated worktree with `cargo clean`. GitHub CI passed on the final head: 52 successful checks and 2 optional skips. - Fresh Greptile review for `4f5fe1951f0bd7f7739cf6655d395ff78f1ed944`: **5/5**, successful current-head check, zero review threads. - September 23 refresh: the unchanged PR head merges cleanly with current master `db8f8fe5b73a2697684a30261b0d306a9c631aba`. In an isolated temporary worktree, all 377 workflow tests and 29 release/preview tests pass on the combined tree. `git diff --cached --check` passes. - Refreshed Actionlint workflow validation passes with ShellCheck disabled. Full Actionlint reports the same 10 existing ShellCheck diagnostics as master, with no added diagnostics. No source changes or new PR commits were needed. - The full local build/typecheck and current-head Linux CI results above remain the verification for the unchanged PR head. They were not rerun for this metadata-only refresh. No image publication or tenant deployment was initiated for this refresh. ## Risks **Deployment prerequisite satisfied (September 23):** The combined cleanup release is deployed to staging and production, and production Support is verified. Active managed-fleet automation uses standard-image composition. Explicit immutable previews remain supported by the retained preview publisher. This PR is ready for maintainer review; keep auto-merge disabled and wait for explicit merge authorization. - A consumer still selecting `Cloud deployable v1` will stop advancing at the last legacy-ready commit. Confirm active automatic consumers use the standard-image composition contract before merge. - Legacy cloud release-channel aliases stop advancing. Standard self-hosted aliases continue. - This PR deletes no registry images, cache tags, migrators, credentials, or runner infrastructure. Existing immutable releases remain usable for rollback. - Explicit legacy previews remain for commit-specific operator deployments. Retiring that compatibility path requires a separate consumer migration. - These changes affect CI publication, not database schema or application behavior. ## Model Used OpenAI Codex, GPT-6. The runtime does not expose a more specific model identifier or context-window size. Used repository inspection, reasoning, code editing, shell tools, and test execution. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
16 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 |
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.
docker build -t paperclip-local \
--build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) .
Standard images and downstream composition
The Docker workflow publishes the standard production target for Linux AMD64
and ARM64. Canonical master pushes also publish
ghcr.io/paperclipai/paperclip:sha-<FULL_SHA> and a GitHub/Sigstore attestation
for its immutable multi-platform digest. Downstream services can compose their
own images from this public base without rebuilding Core.
The legacy recurring public -cloud publisher is retired. Master pushes,
release tags, and manual Docker dispatches no longer build that variant.
Existing -cloud tags and digests remain in the registry for rollback; their
release-channel aliases no longer advance. This change deletes no images,
cache tags, or migrator artifacts.
The cloud Dockerfile target remains available for explicit
preview builds. Those requests still publish a
full-SHA -cloud tag when needed. They do not advance a release channel or
replace downstream private composition.
A published image alone does not prove source tests or migration compatibility. Downstream deployment tooling must verify source proof, the standard image attestation, the exact-source migrator, and its own composed image before rollout. Resolve immutable digests instead of deploying mutable tags.
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).
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.