https://github.com/yorch/ccpod
Run Claude Code inside Docker containers with portable, composable configuration
https://github.com/yorch/ccpod
claude claude-ai claude-code
Last synced: about 2 months ago
JSON representation
Run Claude Code inside Docker containers with portable, composable configuration
- Host: GitHub
- URL: https://github.com/yorch/ccpod
- Owner: yorch
- Created: 2026-05-01T01:30:52.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-06T13:30:59.000Z (about 2 months ago)
- Last Synced: 2026-05-07T21:58:41.600Z (about 2 months ago)
- Topics: claude, claude-ai, claude-code
- Language: TypeScript
- Homepage: https://ccpod.brnby.com/
- Size: 439 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# ccpod
Run Claude Code inside Docker containers with portable, composable configuration.
**Why:** Your Claude environment (settings, plugins, skills, CLAUDE.md, MCPs) is usually locked to one machine. ccpod wraps it in a **profile** — a versioned config that travels with you, your team, or your open-source project. Every `ccpod run` drops you into a clean, reproducible Claude session.
Works with Docker, Podman, OrbStack, and Colima — auto-detected.
📖 **Docs:** https://ccpod.brnby.com
---
## Install
```sh
# macOS / Linux — one-liner installer
curl -fsSL https://ccpod.brnby.com/install.sh | sh
# Or build from source (requires Bun)
bun run build # outputs dist/ccpod
```
The installer places `ccpod` in `/usr/local/bin` by default. Override with `CCPOD_INSTALL_DIR=~/.local/bin` or pin a version with `CCPOD_VERSION=v0.2.0`.
---
## Quick start
```sh
ccpod init # first-run wizard: detect runtime, set up auth, create default profile
ccpod run # interactive Claude session in the current directory
```
The project directory is mounted at `/workspace` inside the container. Claude runs there.
---
## Profiles
A profile defines the complete Claude environment for a session: Docker image, auth, settings, plugins, CLAUDE.md, SSH config, network policy, and sidecar services.
Profiles live at `~/.ccpod/profiles//profile.yml`.
### Profile config (`~/.ccpod/profiles//profile.yml`)
```yaml
name: personal
description: My personal Claude environment
config:
source: local # "local" | "git"
path: ~/.my-claude-config # local path to your ~/.claude config dir
# For git-based configs:
# source: git
# repo: https://github.com/org/claude-config
# sync: daily # "always" | "daily" | "pin"
# ref: main
image:
use: ghcr.io/yorch/ccpod:latest # pre-built image with Claude Code
# use: build # build locally using dockerfile:
# dockerfile: ./Dockerfile
auth:
type: api-key # "api-key" | "oauth"
keyEnv: ANTHROPIC_API_KEY # env var to read key from
# keyFile: /run/secrets/api-key
state: ephemeral # "ephemeral" (default) | "persistent"
ssh:
agentForward: true # forward SSH_AUTH_SOCK
mountSshDir: false # mount ~/.ssh read-only
network:
policy: full # "full" (default) | "restricted"
allow: [] # domains/IPs to allow in restricted mode
ports:
list:
- "3000:3000" # host:container
autoDetectMcp: true # expose HTTP/SSE MCP ports from .mcp.json
plugins:
- mcp-server-brave-search # delta-installed on first run; skipped if already present
claudeArgs: # extra flags passed to claude on every run
- "--dangerously-skip-permissions"
env:
- DATABASE_URL # host env vars to forward into container
services: # optional sidecar containers
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: dev
volumes:
- ccpod-pg-data:/var/lib/postgresql/data
```
### Project config (`.ccpod.yml` in project root)
Overrides the profile for a specific project. ccpod walks up from `$PWD` to find it.
```yaml
profile: personal # which profile to use
merge: deep # "deep" (default) | "override"
config:
claudeMd: append # "append" (default) | "override"
network:
policy: restricted
allow:
- api.github.com
- registry.npmjs.org
ports:
list:
- "4000:4000"
env:
- STRIPE_SECRET_KEY
```
**Merge strategies:**
| Asset | Default behavior |
|-------|-----------------|
| `settings.json` | Deep merge; project wins on conflicts |
| `CLAUDE.md` | Profile content first, project appended (or `override` to replace) |
| plugins | Union — project adds, cannot remove profile plugins (unless `merge: override`) |
| `services` | Merged by key; project adds sidecars |
| `env` | Union of both lists |
---
## CLI reference
```
ccpod run Interactive Claude session
ccpod run "fix lint errors" Headless: inline prompt
ccpod run --file prompt.txt Headless: prompt from file (exit with container code)
ccpod run --profile Use a specific profile
ccpod run --env KEY=VALUE Pass/override env var for this run
ccpod run --rebuild Force image rebuild or repull
ccpod run --no-state Force ephemeral state for this run
ccpod init First-run setup wizard
ccpod init --profile Create a named profile
ccpod profile create
ccpod profile list
ccpod profile update Force-pull git-based config
ccpod profile delete
ccpod plugins list [--profile ]
ccpod plugins update [--profile ] Flush and reinstall all plugins
ccpod image init [--profile ] Download Dockerfile for local customization
ccpod image init --from Use a custom Dockerfile URL
ccpod image build [--profile ] Build local Dockerfile image
ccpod image build --apply Also update profile image.use to the built tag
ccpod image pull [--profile ] Pull latest base or declared image
ccpod ps List running ccpod containers
ccpod ps --all Include stopped containers
ccpod down Stop Claude container + sidecars for $PWD
ccpod state clear [--profile ] Delete state directory (resets projects/todos)
ccpod config get Get a global config value
ccpod config set Set a global config value
ccpod config show Print resolved merged config
ccpod config validate Validate .ccpod.yml
ccpod update Update ccpod to the latest release
```
---
## State persistence
| Mode | History & memory | Survives restart |
|------|-----------------|-----------------|
| `ephemeral` (default) | tmpfs — wiped on exit | No |
| `persistent` | `~/.ccpod/state//` on host | Yes |
Switch for a single run: `ccpod run --no-state`
Reset: `ccpod state clear [profile]` — deletes `~/.ccpod/state//`
---
## Storage layout
```
~/.ccpod/
config.yml # global ccpod config (autoCheckUpdates, etc.)
profiles//
profile.yml
.ccpod-sync-lock # timestamp of last git sync
config/ # cloned Claude config (only if source: git)
credentials// # auth tokens — persist across container restarts
state// # projects, todos, statsig (persistent mode only)
Docker named volumes (managed by ccpod):
ccpod-plugins- # installed Claude plugins
```
---
## Global config
`~/.ccpod/config.yml` controls ccpod's own behaviour (not the container).
| Key | Default | Description |
|-----|---------|-------------|
| `autoCheckUpdates` | `true` | Check for new ccpod releases on startup |
```sh
ccpod config set autoCheckUpdates false # disable startup update checks
ccpod config set autoCheckUpdates true # re-enable
```
---
## MCP auto-detection
If `.mcp.json` exists at the project root and `ports.autoDetectMcp: true` (default), ccpod reads HTTP/SSE MCP entries and automatically exposes their ports. No manual port config needed for most MCP setups.
> **Note:** stdio-based MCP servers require their runtime binaries inside the container. `npx`-based MCPs work automatically since the base image includes Node.js. Other runtimes need a custom `dockerfile:`.
---
## Shared team profile
Point your team at a shared git repo:
```yaml
# ~/.ccpod/profiles/team/profile.yml
name: team
config:
source: git
repo: https://github.com/your-org/claude-config
sync: daily
ref: main
```
Everyone gets the same CLAUDE.md, settings, and plugins. Updates flow in daily (or on demand with `ccpod profile update team`).
---
## Base image
The official base image is `ghcr.io/yorch/ccpod` — built from `docker/Dockerfile` and published automatically on every push to `main` (`:main`, `:latest`) and on version tags (`:1.2.3`, `:1.2`).
To use a custom image, set `image.use` in your profile. To build locally from a Dockerfile, set `image.use: build` and `image.dockerfile: ./Dockerfile`.