Install with Docker / Podman¶
The recommended way to run pAInapple Code is in a container — it gives you the isolation the security notes call for, and the image bundles everything the bridge needs.
The image ships Python 3.13, Node 20, the @anthropic-ai/claude-code CLI, git, and a baseline dev toolkit (ripgrep, fd, jq, tmux, vim, and more). Bridge state persists in named volumes; your project and an isolated Claude CLI home are bind-mounted from the host. The Dockerfile is OCI-compliant, so it works with Docker, Podman, nerdctl, or any other OCI runtime.
Option A — built-in docker CLI (pipx, no clone needed)¶
The pip package ships a painapple docker command group with an interactive TUI setup — arrow-key menus, a browsable directory picker (type to filter, Left / Right to climb or enter folders), back navigation on every step, and a final review screen that jumps back into any section.
pipx install painapple-code
painapple docker # first run opens the setup wizard
painapple docker pull # fetch wrotek/painapple-code:latest (pull v1.0.0 to pin)
painapple docker up -d # start the bridge detached
In a hurry?
painapple docker quick skips setup entirely and starts the bridge against the current directory with sane defaults.
Other subcommands: stop, down, restart, logs, shell, claude-login, password (reveal the login URL), extract, status, config (non-interactive --flag value overrides). Run painapple docker help for the full list.
The setup wizard configures an isolated .claude home by default — the container doesn't share state with your host CLI — and offers to seed it from your host login once.
Docker and Podman are auto-detected; override with RUNTIME=docker or RUNTIME=podman if both are installed. This CLI is pull-only — building the image from source needs a repo checkout (Option C below; both tools share the same config, so mixing them works).
Option B — raw docker run (one-liner, no clone needed)¶
docker run -d --name painapple-code \
-p 127.0.0.1:8765:8765 \
-v "$PWD:/workspace" \
-v "$HOME/.painapple-code/.claude:/home/app/.claude" \
-v painapple-data:/data \
wrotek/painapple-code:latest
docker logs painapple-code 2>&1 | grep -E 'http(s)?://' | head -1 # bootstrap URL
Image tags:
| Tag | Meaning |
|---|---|
:latest |
Newest stable release |
:vX.Y.Z |
Pinned to a specific release |
:edge |
Manual builds off main |
Published image and UIDs (Linux)
The published image bakes in USER_UID=1000. On Linux with rootful Docker and a host UID other than 1000, bind-mount writes may land owned by 1000:1000. Build locally (Option C) when UIDs need to match exactly.
Option C — build from source with painapple-docker.sh (clone the repo)¶
The Bash wrapper does everything Option A does and adds local image builds — including personalized builds layered from your own devcontainer.json or Dockerfile.
git clone https://github.com/wrotek/painapple-code.git
cd painapple-code
./painapple-docker.sh setup # interactive: workspace, claude credentials, port, accent
./painapple-docker.sh build # build the image locally (~2-3 min, once)
./painapple-docker.sh up -d # start the bridge detached
The wrapper auto-detects the OCI runtime (force with RUNTIME=docker / RUNTIME=podman), passes your USER_UID/USER_GID build args automatically so bind mounts stay writable, and auto-applies the Podman-specific flags (--userns=keep-id, SELinux :Z). If you don't want to build, swap build for pull — it retags the fetched image so the rest of the wrapper finds it.
To layer your own tooling on top of the base image:
# Layer Dev Container Features from a devcontainer.json
./painapple-docker.sh build --devcontainer ~/my-project/.devcontainer
# Or append your project's existing Dockerfile
./painapple-docker.sh build --dockerfile ~/my-project/Dockerfile
Open http://localhost:8765/ in a browser. The first run logs a bootstrap URL with the auth password embedded as ?tkn=… — open that link once and the cookie keeps you logged in. See First run & login.
Manual Compose / Podman (no wrapper)¶
docker compose build --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g)
WORKSPACE=/absolute/path/to/your/project docker compose up
The WORKSPACE env var is required — compose refuses to start without it. For convenience, drop it into a .env file next to docker-compose.yml:
By default the compose file mounts ~/.painapple-code/claude-home/ as the container's .claude. To seed it with your existing login:
Podman is daemonless, rootless-by-default, and CLI-compatible with Docker:
podman build --build-arg USER_UID=$(id -u) -t painapple-code:latest .
podman run --rm -it --userns=keep-id \
-p 8765:8765 \
-v painapple-data:/data \
-v "$HOME/.painapple-code/claude-home:/home/app/.claude:Z" \
-v "/absolute/path/to/your/project:/workspace:Z" \
painapple-code:latest
Podman-specific flags:
--userns=keep-id— maps your host UID directly into the container. Without it, rootless Podman remaps UIDs through/etc/subuidand bind-mounted files appear asnobody. Combined with the matchingUSER_UIDbuild arg, your host user owns files inside the container.:Zmount suffix — applies a private SELinux label so the container can read/write the bind mount. Required on Fedora/RHEL/CentOS/Rocky; a harmless no-op on Debian/Ubuntu/Arch. Use:z(lowercase) if the same volume is shared between containers.
Volumes¶
| Mount target in container | Purpose |
|---|---|
/data |
Bridge state (sessions, shadow DB, logs, presets, uploads) via PAINAPPLE_CODE_HOME=/data. Back up this one volume to back up everything. |
/home/app/.config/painapple-code |
Auth config — the generated password. Persist it so down/up keeps the same login. |
/home/app/.claude |
Container-local Claude CLI state (OAuth, history). Defaults to an isolated host path so the container never writes into your host's ~/.claude. Mount $HOME/.claude instead to share state with your host CLI, or drop the mount and set ANTHROPIC_API_KEY for headless deploys. |
/workspace |
Required. Your project directory — where Claude reads and edits files. Mount a single repo or a parent directory of many. |
The workspace mount is mandatory
The entrypoint checks that a real host directory is mounted at /workspace and exits with a configuration error (exit code 78) if it isn't. Compose refuses to start with WORKSPACE unset. This catches the common "I forgot to mount my project" mistake.
Authenticating the in-container Claude CLI¶
The claude subprocess inside the container needs to authenticate to Anthropic. Three paths:
- Isolated
.claudehome (default) — seed it once withcp ~/.claude/.credentials.json <claude-home>/, or runpainapple docker claude-login/./painapple-docker.sh claude-loginto log in inside the container. - Share the host's
~/.claude— mount it directly. Easiest, but the container can mutate your host state. - API key — set
ANTHROPIC_API_KEYand remove the.claudemount. Cleanest for headless servers and CI.
Next step¶
Continue to First run & login to get the bootstrap URL and open your first session.