Complete guide to setting up and running Codetainer on Fly.io.
- Fly.io account with the
flyctlCLI installed - A dedicated GitHub robot account for Claude (e.g.
my-org-claude-bot). Create a standard GitHub account for this purpose — Claude will commit and open PRs as this identity. Add it as a collaborator to the repo you want Claude to work in. - A GitHub Personal Access Token created on the robot account, scoped to the target repo
- A Claude Code OAuth token generated via
claude setup-token
# Install flyctl
curl -L https://fly.io/install.sh | sh
# Log in
fly auth loginFly.io SSH uses WireGuard tunneling. You need to set this up once per machine you'll connect from.
# Create a WireGuard peer configuration
fly wireguard create
# This outputs a WireGuard config file. Import it into your WireGuard client:
# - macOS: WireGuard app from the Mac App Store → Import Tunnel
# - Linux: sudo cp <config>.conf /etc/wireguard/ && sudo wg-quick up <config>
# - Windows: WireGuard app → Import Tunnel
# Verify the tunnel is working
fly wireguard statusImportant: The WireGuard tunnel must be active whenever you use
fly ssh console. If SSH hangs or times out, check that your WireGuard tunnel is connected.
fly apps create <your-app-name>fly secrets set \
GH_PAT=<your-github-pat> \
CLAUDE_CODE_OAUTH_TOKEN=<your-oauth-token> \
-a <your-app-name>See Configuration for the full secrets reference — what each token is, how to create them, and fine-grained vs. classic token guidance.
Pick the Fly.io region closest to you for the --region flag (e.g. sjc, iad, lhr).
Option A: Prebuilt image (fastest)
fly machine run ghcr.io/perezd/codetainer:latest \
--app <your-app-name> \
--region <your-region> \
--restart no \
--autostart=false \
--vm-memory 1024 \
--vm-size shared-cpu-1x \
--env GIT_USER_NAME="my-robot" \
--env GIT_USER_EMAIL="my-robot@users.noreply.github.com" \
--env REPO_URL="https://github.com/your-org/your-repo"To give Claude an immediate task, add an initialization prompt:
fly machine run ghcr.io/perezd/codetainer:latest \
--app <your-app-name> \
--region <your-region> \
--restart no \
--autostart=false \
--vm-memory 1024 \
--vm-size shared-cpu-1x \
--env GIT_USER_NAME="my-robot" \
--env GIT_USER_EMAIL="my-robot@users.noreply.github.com" \
--env REPO_URL="https://github.com/your-org/your-repo" \
--env CLAUDE_PROMPT="https://github.com/your-org/your-repo/issues/42"Claude will begin working on the prompt as soon as the container is ready, before you SSH in. When you connect, you'll attach to the in-progress session.
Option B: Build from Dockerfile (customizable)
If you want to customize the image (e.g. change installed tools or network allowlists), clone the repo and build directly:
git clone https://github.com/perezd/codetainer.git
cd codetainer
fly machine run . --dockerfile Dockerfile \
--app <your-app-name> \
--region <your-region> \
--restart no \
--autostart=false \
--vm-memory 1024 \
--vm-size shared-cpu-1x \
--env GIT_USER_NAME="my-robot" \
--env GIT_USER_EMAIL="my-robot@users.noreply.github.com" \
--env REPO_URL="https://github.com/your-org/your-repo"This builds the image remotely on Fly.io's builders and deploys it in one step. The first build takes a few minutes; subsequent builds are cached.
# Claude starts automatically at boot — SSH attaches to the running session
fly ssh console -a <your-app-name>
# If Claude is still initializing, you'll see boot progress until it's readyClaude Code launches automatically in a tmux session. The bottom pane is a shell for running commands directly.
The session has two panes:
- Top (80%): Claude Code
- Bottom (20%): A bash shell in the working directory
Switch panes with Ctrl-b ↓ / Ctrl-b ↑ or click with the mouse.
| Size | Memory | Use Case |
|---|---|---|
shared-cpu-1x / 1GB |
Minimum | Small repos, light tasks |
shared-cpu-1x / 2GB |
Recommended | General development |
shared-cpu-2x / 4GB |
Heavy | Large repos, parallel builds |
fly machine run ghcr.io/perezd/codetainer:latest \
--vm-memory 2048 \
--vm-size shared-cpu-2x \
...# List machines to find the ID
fly machine list -a <your-app-name>
# Graceful stop
fly machine stop <machine-id> -a <your-app-name>The machine is configured with --restart no and --autostart=false, so it stays stopped until you explicitly run a new one.
Check that your WireGuard tunnel is active:
fly wireguard statusIf it's disconnected, bring it back up through your WireGuard client.
If your network blocks UDP (common on corporate networks, captive portals, or some ISPs), WireGuard tunnels will fail silently. Switch to WebSocket-based tunneling:
fly wireguard websockets enableThis wraps WireGuard traffic in a WebSocket over TCP/443, which passes through most firewalls. To revert:
fly wireguard websockets disable- Verify
REPO_URLis an HTTPS URL (not SSH) - Verify the
GH_PAThas Contents read access to the repository - Check the entrypoint logs:
fly logs -a <your-app-name>
The superpowers plugin is installed on first SSH login. If it fails, check:
- Network connectivity (the container needs to reach github.com)
statuscommand to see if CoreDNS is running and no unexpected drops
The container builds tmux 3.6a from source for synchronized output support. If you still see rendering artifacts:
- Ensure your local terminal supports true color (
echo $COLORTERMshould showtruecolor) - Try resizing your terminal window after connecting
- Ghostty, iTerm2, and Kitty work best; macOS Terminal.app has limited support
flyctl is not authenticated by default. To authenticate:
# In the terminal pane or via ! in Claude Code
! fly auth loginThe token is stored in memory (tmpfs) and lost on restart.