chore(docker): improve base image and organize docker files

- Add wget, ripgrep, python3, and GitHub CLI (gh) to base image
- Add OPENCODE_ALLOW_ALL_MODELS=true to production ENV
- Move compose files, onboard-smoke Dockerfile to docker/
- Move entrypoint script to scripts/docker-entrypoint.sh
- Add Podman Quadlet unit files (pod, app, db containers)
- Add docker/README.md with build, compose, and quadlet docs
- Add scripts/docker-build-test.sh for local build validation
- Update all doc references for new file locations
- Keep main Dockerfile at project root (no .dockerignore changes needed)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Cody (Radius Red)
2026-04-01 11:06:37 +00:00
parent ebc6888e7d
commit 420cd4fd8d
15 changed files with 249 additions and 27 deletions

View File

@@ -2,10 +2,17 @@ FROM node:lts-trixie-slim AS base
ARG USER_UID=1000
ARG USER_GID=1000
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git gosu \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable
&& apt-get install -y --no-install-recommends ca-certificates gosu curl git wget ripgrep python3 \
&& mkdir -p -m 755 /etc/apt/keyrings \
&& wget -nv -O/etc/apt/keyrings/githubcli-archive-keyring.gpg https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& echo "20e0125d6f6e077a9ad46f03371bc26d90b04939fb95170f5a1905099cc6bcc0 /etc/apt/keyrings/githubcli-archive-keyring.gpg" | sha256sum -c - \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& mkdir -p -m 755 /etc/apt/sources.list.d \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/* \
&& corepack enable
# Modify the existing node user/group to have the specified UID/GID to match host user
RUN usermod -u $USER_UID --non-unique node \
@@ -51,7 +58,7 @@ RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/cod
&& mkdir -p /paperclip \
&& chown node:node /paperclip
COPY docker-entrypoint.sh /usr/local/bin/
COPY scripts/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENV NODE_ENV=production \
@@ -65,7 +72,8 @@ ENV NODE_ENV=production \
USER_GID=${USER_GID} \
PAPERCLIP_CONFIG=/paperclip/instances/default/config.json \
PAPERCLIP_DEPLOYMENT_MODE=authenticated \
PAPERCLIP_DEPLOYMENT_EXPOSURE=private
PAPERCLIP_DEPLOYMENT_EXPOSURE=private \
OPENCODE_ALLOW_ALL_MODELS=true
VOLUME ["/paperclip"]
EXPOSE 3100

View File

@@ -97,7 +97,7 @@ docker run --name paperclip \
Or use Compose:
```sh
docker compose -f docker-compose.quickstart.yml up --build
docker compose -f docker/docker-compose.quickstart.yml up --build
```
See `doc/DOCKER.md` for API key wiring (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`) and persistence details.

View File

@@ -2,6 +2,28 @@
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 |
```sh
docker build -t paperclip-local \
--build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) .
```
## One-liner (build + run)
```sh
@@ -10,6 +32,7 @@ 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
```
@@ -25,10 +48,15 @@ Data persistence:
All persisted under your bind mount (`./data/docker-paperclip` in the example above).
## Compose Quickstart
## Docker Compose
### Quickstart (embedded SQLite)
Single container, no external database. Data persists via a bind mount.
```sh
docker compose -f docker-compose.quickstart.yml up --build
BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
docker compose -f docker/docker-compose.quickstart.yml up --build
```
Defaults:
@@ -39,11 +67,36 @@ Defaults:
Optional overrides:
```sh
PAPERCLIP_PORT=3200 PAPERCLIP_DATA_DIR=./data/pc docker compose -f docker-compose.quickstart.yml up --build
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:
@@ -93,11 +146,71 @@ Notes:
- Without API keys, the app still runs normally.
- Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites.
## Untrusted PR Review Container
## Podman Quadlet (systemd)
If you want a separate Docker environment for reviewing untrusted pull requests with `codex` or `claude`, use the dedicated review workflow in `doc/UNTRUSTED-PR-REVIEW.md`.
The `docker/quadlet/` directory contains unit files to run Paperclip + PostgreSQL as systemd services via Podman Quadlet.
That setup keeps CLI auth state in Docker volumes instead of your host home directory and uses a separate scratch workspace for PR checkouts and preview runs.
| 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)
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)
@@ -133,4 +246,9 @@ Notes:
- 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`.
- The image definition is in `Dockerfile.onboard-smoke`.
- 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).

View File

@@ -16,14 +16,14 @@ By default this workflow does **not** mount your host repo checkout, your host h
## Files
- `docker/untrusted-review/Dockerfile`
- `docker-compose.untrusted-review.yml`
- `docker/docker-compose.untrusted-review.yml`
- `review-checkout-pr` inside the container
## Build and start a shell
```sh
docker compose -f docker-compose.untrusted-review.yml build
docker compose -f docker-compose.untrusted-review.yml run --rm --service-ports review
docker compose -f docker/docker-compose.untrusted-review.yml build
docker compose -f docker/docker-compose.untrusted-review.yml run --rm --service-ports review
```
That opens an interactive shell in the review container with:
@@ -47,7 +47,7 @@ claude login
If you prefer API-key auth instead of CLI login, pass keys through Compose env:
```sh
OPENAI_API_KEY=... ANTHROPIC_API_KEY=... docker compose -f docker-compose.untrusted-review.yml run --rm review
OPENAI_API_KEY=... ANTHROPIC_API_KEY=... docker compose -f docker/docker-compose.untrusted-review.yml run --rm review
```
## Check out a PR safely
@@ -117,7 +117,7 @@ Notes:
Remove the review container volumes when you want a clean environment:
```sh
docker compose -f docker-compose.untrusted-review.yml down -v
docker compose -f docker/docker-compose.untrusted-review.yml down -v
```
That deletes:

View File

@@ -1,7 +1,7 @@
services:
paperclip:
build:
context: .
context: ..
dockerfile: Dockerfile
ports:
- "${PAPERCLIP_PORT:-3100}:3100"
@@ -15,4 +15,4 @@ services:
PAPERCLIP_PUBLIC_URL: "${PAPERCLIP_PUBLIC_URL:-http://localhost:3100}"
BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET must be set}"
volumes:
- "${PAPERCLIP_DATA_DIR:-./data/docker-paperclip}:/paperclip"
- "${PAPERCLIP_DATA_DIR:-../data/docker-paperclip}:/paperclip"

View File

@@ -1,7 +1,7 @@
services:
review:
build:
context: .
context: ..
dockerfile: docker/untrusted-review/Dockerfile
init: true
tty: true

View File

@@ -16,7 +16,9 @@ services:
- pgdata:/var/lib/postgresql/data
server:
build: .
build:
context: ..
dockerfile: Dockerfile
ports:
- "3100:3100"
environment:

View File

@@ -0,0 +1,20 @@
[Unit]
Description=PostgreSQL for Paperclip
[Container]
Image=docker.io/library/postgres:17-alpine
ContainerName=paperclip-db
Pod=paperclip.pod
Volume=paperclip-pgdata:/var/lib/postgresql/data
EnvironmentFile=%h/.config/containers/systemd/paperclip.env
HealthCmd=pg_isready -U $POSTGRES_USER -d $POSTGRES_DB -h localhost || exit 1
HealthInterval=15s
HealthTimeout=5s
HealthRetries=5
[Service]
Restart=on-failure
TimeoutStartSec=60
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,23 @@
[Unit]
Description=Paperclip AI Agent Orchestrator
Requires=paperclip-db.service
After=paperclip-db.service
[Container]
Image=paperclip-local
ContainerName=paperclip
Pod=paperclip.pod
Volume=%h/.local/share/paperclip:/paperclip:Z
Environment=HOST=0.0.0.0
Environment=PAPERCLIP_HOME=/paperclip
Environment=PAPERCLIP_DEPLOYMENT_MODE=authenticated
Environment=PAPERCLIP_DEPLOYMENT_EXPOSURE=private
Environment=PAPERCLIP_PUBLIC_URL=http://localhost:3100
EnvironmentFile=%h/.config/containers/systemd/paperclip.env
[Service]
Restart=on-failure
TimeoutStartSec=120
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,3 @@
[Pod]
PodName=paperclip
PublishPort=3100:3100

View File

@@ -8,7 +8,7 @@ Run Paperclip in Docker without installing Node or pnpm locally.
## Compose Quickstart (Recommended)
```sh
docker compose -f docker-compose.quickstart.yml up --build
docker compose -f docker/docker-compose.quickstart.yml up --build
```
Open [http://localhost:3100](http://localhost:3100).
@@ -21,10 +21,12 @@ Defaults:
Override with environment variables:
```sh
PAPERCLIP_PORT=3200 PAPERCLIP_DATA_DIR=./data/pc \
docker compose -f docker-compose.quickstart.yml up --build
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.
## Manual Docker Build
```sh

46
scripts/docker-build-test.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Verify the Docker image builds successfully.
# Skips gracefully when docker/podman is not available.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# Detect container runtime
if command -v docker >/dev/null 2>&1; then
RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then
RUNTIME=podman
else
echo "SKIP: neither docker nor podman found — skipping build test"
exit 0
fi
# Verify the daemon is reachable (docker may be installed but not running)
if ! "$RUNTIME" info >/dev/null 2>&1; then
echo "SKIP: $RUNTIME is installed but not running — skipping build test"
exit 0
fi
IMAGE_TAG="paperclip-build-test:$$"
trap '"$RUNTIME" rmi "$IMAGE_TAG" >/dev/null 2>&1 || true' EXIT
echo "==> Testing Docker build with $RUNTIME"
"$RUNTIME" build \
-f "$REPO_ROOT/Dockerfile" \
-t "$IMAGE_TAG" \
--target production \
"$REPO_ROOT"
echo "==> Verifying key binaries in image"
"$RUNTIME" run --rm "$IMAGE_TAG" sh -c '
set -e
node --version
git --version
gh --version
rg --version
python3 --version
curl --version | head -1
claude --version 2>/dev/null || echo "claude CLI not found (OK in minimal builds)"
'
echo "PASS: Docker build test succeeded"

View File

@@ -242,7 +242,7 @@ echo "==> Building onboard smoke image"
docker build \
--build-arg PAPERCLIPAI_VERSION="$PAPERCLIPAI_VERSION" \
--build-arg HOST_UID="$HOST_UID" \
-f "$REPO_ROOT/Dockerfile.onboard-smoke" \
-f "$REPO_ROOT/docker/Dockerfile.onboard-smoke" \
-t "$IMAGE_NAME" \
"$REPO_ROOT"