I wanted to have a way to [relatively] safely use claude in a dangerously-skip-permissions way across my projects, on my workstation, and to access my projects from any device at any time in a persistent manner.
An nvidia and pytorch-enabled, containerized, assumed self-hosted worskpace, with multiple projects. Session persistence via tmux.
- VS Code in your phone or laptop browser via code-server, with server-side persistence
- Tmux enabled tmux automatically as default terminal, with per-project named sessions
- Claude Code via CLI in the integrated terminal, containerized
- Git configured via proper ssh mounts
- GPU access for PyTorch / CUDA work
- HTTPS via Tailscale so you can access it securely from anywhere
- Docker with Compose
- NVIDIA GPU + drivers (for GPU access)
- Tailscale account
By default, ~/projects is mounted into the container. To use a different folder:
export PROJECTS_DIR=/path/to/your/projectsThe folder is mounted at the same absolute path inside the container so that Claude Code's chat history (which is indexed by project path) stays linked correctly.
./install_tailscale.shThis installs Tailscale (if not already installed) and generates TLS certificates in /etc/tailscale/certs/. These are auto-detected by the container at startup.
export CODE_SERVER_PASSWORD="something-secure"./start_docker.shThis builds the image (if needed) and starts the container. Subsequent runs reuse cached layers.
Open https://<your-tailscale-hostname>:8080 in your phone or laptop browser to access the full projects folder, then use the vs code interface to open a specific project.
Find your hostname with tailscale status — it will be something like myhost.tail1234.ts.net.
./stop_docker.shLog in from inside a code-server terminal (or via ./connect_to_docker.sh):
claude loginIt will print an OAuth URL — open it in any browser. Because the container uses host networking, the callback reaches it directly. The credential persists in your host's ~/.claude/.
OAuth refresh tokens are single-use. When multiple concurrent Claude sessions share
the same ~/.claude/.credentials.json, they race to refresh the token — one session
wins, the rest get 401 errors and force re-login. This is a known upstream bug with several open issues (#37678, #36911).
Workaround 1 - use long-lived auth token. Export before starting docker:
export CLAUDE_CODE_OAUTH_TOKEN="..." # recommended: long-lived subscription token (run `claude setup-token` to generate)Workaround 2 — use an API key instead of OAuth:
If you have access to the Claude Console, set ANTHROPIC_API_KEY
in start_docker.sh. This bypasses OAuth entirely and has no refresh race. Note: this uses
API billing, not your subscription quota.
export ANTHROPIC_API_KEY="sk-ant-..."Terminals run as a non-root coder user, so --dangerously-skip-permissions works:
claude --dangerously-skip-permissionsThis gives Claude full autonomy — no prompts for file edits, shell commands, web searches, etc.
Your host's ~/.claude and ~/.claude.json are bind-mounted into the container, so existing chat history and auth carry over. Claude indexes sessions by absolute project path, which is why the projects folder is mounted at the same path inside the container.
Note: Don't run Claude on the same project from host and container simultaneously — this can cause file contention.
code-server runs server-side, so closing your browser doesn't stop running processes. When you reconnect, your terminals and any running Claude Code session are still there.
Terminals auto-attach to project-specific tmux sessions (named after the project folder). This means long-running Claude sessions survive even if code-server restarts — as long as the container stays up.
Git config and SSH keys are mounted read-only from the host. Push, pull, and clone work out of the box — no additional setup needed inside the container.
./stop_docker.sh
docker compose build --no-cache
./start_docker.shVS Code settings and Claude auth persist across rebuilds (stored in Docker volumes and host bind-mounts).
docker compose logs -f