Skip to content

Docker CLI

The painapple docker subcommand group runs the bridge in a Docker or Podman container instead of directly on the host — a Python port of the repo's painapple-docker.sh wrapper, sharing the same config.

Pull-only

This CLI fetches the prebuilt image (wrotek/painapple-code) from Docker Hub — there is intentionally no build subcommand. Building from source (and the personalize/devcontainer paths) requires a repo checkout and ./painapple-docker.sh build, which reads the same config as this CLI.

Bare invocation

painapple docker with no subcommand shows status if a config exists, or launches the interactive setup wizard on first run.

Subcommands

Command Aliases Description
setup Interactive setup wizard (workspace, Claude credentials, network). Safe to rerun — edits config in place.
list profiles, ls List every deployment (the default one + named profiles) with host-bind address, container name, and running state
quick [-d] [--cwd PATH] Zero-config start: current directory (or --cwd) as the workspace, defaults for everything else
pull [TAG] [--source REF] Fetch a prebuilt image and tag it for up. Default: wrotek/painapple-code:latest
up [-d] [WS] [--flag …] start Start the bridge. -d = detached (durable: --restart unless-stopped); foreground runs are ephemeral (--rm). Extra --flag value pairs are one-off config overrides; a positional path overrides the workspace.
stop Stop the running container
down rm Stop and remove the container
restart [--flag …] reload Recreate the container (down + up -d)
logs log Follow container logs
shell sh, exec Open a shell inside the container
claude-login login Run claude login inside the container
password show-password, url, token Show the bridge login URL(s) + password (+ TLS fingerprint when applicable)
extract [DEST] export Copy the data volume to a host directory (default ./painapple-data-export). No-op with a hint if the data volume is already a bind mount.
status ps Show the resolved config and running state
config … Manage config non-interactively (see below)
help --help, -h Full command reference

config subcommands

Command Description
config show Print the raw docker.yaml (falls back to wrapper.conf if no YAML yet)
config edit Open docker.yaml in $EDITOR (default vi)
config path Print the config file path
config get [KEY …] Print one key, or all keys when none given
config set --flag value … Set keys and save (config --flag value works too)

Config keys (each also accepts a --kebab-case flag, e.g. LISTEN_HOST--listen-host):

Group Keys
Runtime RUNTIME (docker | podman, empty = auto-detect), RUNTIME_FLAGS
Workspace WORKSPACE, WORKSPACE_MODE (project | parent | multi), CLAUDE_HOME, CLAUDE_JSON
Storage DATA_VOLUME (host dir or named volume for sessions, logs, DuckDB), CONFIG_VOLUME
Container IMAGE, CONTAINER
Network PORT, LISTEN_HOST, TLS_MODE (auto | on | off)
UI INSTANCE_NAME (max 12 chars), ACCENT (preset name or #RRGGBB)

LISTEN_HOST is the host interface the container publishes on (-p LISTEN_HOST:PORT:8765), not the container's internal bind (always 0.0.0.0:8765). 0.0.0.0 exposes the bridge on every host interface — reachable from your LAN (phones, other machines); 127.0.0.1 (or localhost) keeps it to the local machine only. status and list both show this as the host bind.

DEVCONTAINER_PATH / DOCKERFILE_PATH also exist but are only used by ./painapple-docker.sh build.

Profiles — multiple sandboxes

Every command accepts --profile NAME (anywhere in the arguments) to target an independent deployment — its own container, port, data volume, and bridge config. One sandbox per project is the intended use:

painapple docker --profile myapp setup    # configure a second sandbox
painapple docker --profile myapp up -d    # runs alongside the default one
painapple docker profiles                 # list them all

A named profile's config lives at ~/.painapple-code/docker-profiles/NAME/docker.yaml, with collision-free defaults derived from the name: container painapple-code-NAME, data volume painapple-data-NAME, bridge config ~/.config/painapple-code/docker-NAME. CLAUDE_HOME still defaults to the shared isolated directory, so one claude login serves every sandbox.

The flag-less invocation is the default profile — the classic ~/.painapple-code/docker.yaml, and the only deployment painapple-docker.sh knows about (named profiles are CLI/desktop-app territory). PAINAPPLE_DOCKER_PROFILE=NAME sets the profile via the environment when the flag is absent.

The macOS desktop app's launcher drives the same profiles: sandboxes added in its setup wizard appear in profiles, and terminal-made profiles show up as instances in the launcher.

The top-level painapple list shows these sandboxes together with any local (non-container) server processes — the whole machine at a glance.

Configuration files & sync

The canonical store is ~/.painapple-code/docker.yaml. The bash wrapper's wrapper.conf (in the same directory) is kept in two-way sync so a repo checkout and a pip install can manage the same container:

  • Load: whichever file is newer wins (wrapper.conf newer than docker.yaml → the script-side config is used).
  • Save: both files are written — wrapper.conf first, then docker.yaml, so the YAML ends up newest.

The setup wizard (setup) is a step-by-step TUI with back navigation, pre-filled editable path prompts, and a final review screen you can jump back from. It covers the runtime, workspace layout, Claude credential mounts, storage volumes, network binding, and instance branding.

Environment overrides

Variable Purpose
RUNTIME Override the container runtime for this run (docker or podman)
RUNTIME_FLAGS Extra global flags passed to the runtime binary on every invocation
PAINAPPLE_HOME Move the config (docker.yaml, wrapper.conf) and isolated Claude home elsewhere (default ~/.painapple-code)
PAINAPPLE_DOCKER_PROFILE Default profile when --profile isn't given
NO_COLOR Disable colored output

Runtime auto-detection

When RUNTIME is unset, the CLI prefers docker (the more common default) and falls back to podman; it errors if neither is on PATH. Podman gets --userns=keep-id applied automatically so bind mounts stay writable, and SELinux-enforcing hosts get :Z-labeled mounts.

Typical first run

painapple docker setup     # interactive wizard
painapple docker pull      # fetch the prebuilt image
painapple docker up -d     # start detached
painapple docker password  # login URL + password

Or skip the wizard entirely:

cd ~/my-project
painapple docker quick -d