Self-Hosting Remote VSCode with Cloudflare Tunnel and Authentik SSO
Source: Dev.to
Overview
Working remotely on lab projects usually means a VPN or SSH keys on every device. code‑server fixes that – it gives you a full VS Code experience in a browser tab, locked behind Google SSO, running 24/7 on a Mac Mini.
Components
| Component | Purpose |
|---|---|
| code‑server (by Coder) | VS Code in the browser, packaged as a Docker image by LinuxServer.io |
| Cloudflare Tunnel | Outbound‑only tunnel – no open ports on the router |
| Authentik (by Authentik Security) | Self‑hosted SSO with Google OAuth2 |
| Nginx Proxy Manager (by jc21) | Reverse proxy that enforces forward‑auth on every request |
SSH works, but requires a client and keys on every device, and you lose the full editor experience.
code‑server gives you extensions, an integrated terminal, and Claude Code in any modern browser. Once it’s running and proxied, it works on iPad just as well as a laptop.
Request Flow
Browser → Cloudflare Tunnel Edge → Nginx Proxy Manager → Authentik outpost check
↓ (if authenticated)
code‑server
- Nginx Proxy Manager uses
auth_requestto check every request against Authentik’s embedded outpost. - If you’re not authenticated, you land on the Authentik login page – in this case a Google OAuth2 prompt.
Docker Compose for code‑server
services:
code-server:
image: lscr.io/linuxserver/code-server:latest
container_name: code-server
environment:
- PUID=501
- PGID=20
- TZ=America/Chicago
- PASSWORD=${CODE_SERVER_PASSWORD}
- SUDO_PASSWORD=${CODE_SERVER_PASSWORD}
- DEFAULT_WORKSPACE=/config/workspace
volumes:
- /your/config:/config
- /your/projects:/config/workspace/Projects
ports:
- 8484:8443
restart: unless-stopped
Secrets
Create a .env file next to the compose file:
CODE_SERVER_PASSWORD=your-password-here
chmod 600 .env # only your user can read it
Important: Always (re)run
docker compose up -dafter changing env vars.
docker restartre‑uses the original environment from the container’s creation, whiledocker compose up -dre‑reads the compose file and the.env.
Nginx Proxy Manager Configuration
-
Create a proxy host for
code.yourdomain.com- Forward Hostname: your server’s local IP (not
localhost) - Forward Port:
8484
- Forward Hostname: your server’s local IP (not
-
WebSockets Support: ON (required for code‑server)
-
Force SSL: OFF – Cloudflare terminates TLS at the edge and sends plain HTTP to NPM. Enabling Force SSL creates an infinite redirect loop.
-
Patch the generated NPM config (add Authentik blocks before the main
location /block):
auth_request /outpost.goauthentik.io/auth/nginx;
error_page 401 = @goauthentik_proxy_signin;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
location /outpost.goauthentik.io {
proxy_pass http://your-server-ip:9010/outpost.goauthentik.io;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location @goauthentik_proxy_signin {
internal;
return 302 /outpost.goauthentik.io/start?rd=https://$http_host$request_uri;
}
The rd=https://… parameter redirects you back to code‑server after a successful login. Without it, Authentik would send you to its own dashboard.
- Reload Nginx after editing the config:
docker exec nginx-proxy-manager nginx -s reload
Common Pitfalls & Fixes
| Symptom | Cause | Fix |
|---|---|---|
| Credentials don’t update after editing compose | Ran docker restart instead of docker compose up -d | Use docker compose up -d |
Password with & breaks compose | YAML treats & as an anchor | Move secrets to a .env file and reference ${VAR} |
| Force‑SSL redirect loop | NPM forces HTTPS while Cloudflare already sends HTTP | Disable Force‑SSL in NPM |
| Extensions missing | code‑server uses the Open VSX Registry, not Microsoft Marketplace | Install only extensions available on Open VSX (see list below) |
Installing Extensions
Most extensions are available on the Open VSX Registry. The following work for me:
for ext in \
anthropic.claude-code \
llvm-vs-code-extensions.lldb-dap \
mechatroner.rainbow-csv \
ms-azuretools.vscode-containers \
ms-python.debugpy \
ms-python.python \
swiftlang.swift-vscode \
tomoki1207.pdf; do
docker exec -u abc code-server \
/app/code-server/bin/code-server --install-extension "$ext"
done
Note:
github.copilot-chatandms-python.vscode-pylanceare not on Open VSX and cannot be installed.
Adding Claude Code (Node.js required)
The LinuxServer.io code-server image does not include Node.js, which Claude Code needs for its CLI agent.
Startup script (config/custom-cont-init.d/install-claude.sh)
#!/bin/bash
# Install Node.js if missing
if ! command -v node &> /dev/null; then
apt-get update -qq && apt-get install -y -qq nodejs npm > /dev/null 2>&1
fi
# Install Claude CLI if missing
if ! command -v claude &> /dev/null; then
npm install -g @anthropic-ai/claude-code > /dev/null 2>&1
fi
LinuxServer.io images automatically execute anything in custom-cont-init.d/ at container start, so Node and the Claude CLI persist across docker compose up -d cycles.
Claude Code Authentication Gotcha
Claude Code supports two auth methods:
- OAuth login – uses your Claude Pro/Max subscription.
- API key – pay‑as‑you‑go billing against your Anthropic API account.
In a headless container the OAuth browser flow fails because the callback cannot reach the container.
Work‑around (macOS)
If you’re already authenticated on a desktop machine, the OAuth token lives in the system keychain:
security find-generic-password -s "Claude Code-credentials" -w
The command returns a JSON blob containing an accessToken field (e.g., sk-an…). Export that token to the container or set it as an environment variable for Claude Code.
Summary
- Deploy
code-servervia Docker Compose. - Expose it through Cloudflare Tunnel → Nginx Proxy Manager → Authentik.
- Configure NPM with Authentik’s
auth_requestblocks and disable Force‑SSL. - Install needed extensions (Open VSX only).
- Add a startup script to install Node.js and Claude Code CLI.
- Handle Claude Code authentication via keychain export (macOS) or API key.
You now have a fully‑featured VS Code environment accessible from any modern browser (including iPads), secured by Google SSO, and running continuously on your Mac Mini. Happy coding!
Setting Up Your Anthropic OAuth Token
# .env file (in the container)
ANTHROPIC_API_KEY=sk-ant-oat01-your-token-here
- OAuth tokens (
sk-ant‑oat…) route through your subscription (not pay‑as‑you‑go). - API tokens (
sk-ant‑api…) bill against your API account per token. - If you generate a key from console.anthropic.com, you receive an API key and will be charged per token.
- The OAuth token stored in the keychain uses your existing plan.
Running Claude Code in the Container
Out‑of‑the‑box Claude Code can:
- Read and write files in the workspace.
It cannot (by default):
- Run Docker commands.
- Push to GitHub.
- Use any custom skills you’ve built.
Why Not the Naïve Fixes?
- Mounting the Docker socket → gives root on the host (container escape risk).
- Adding SSH keys / installing tools with
sudo→ introduces real security risk.
Installing the GitHub CLI Without sudo
# Get the latest GH version
GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest \
| grep '"tag_name"' | cut -d'"' -f4 | sed 's/v//')
# Download the tarball
curl -sL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_arm64.tar.gz" \
-o /tmp/gh.tar.gz
# Extract and install to a writable directory
mkdir -p /config/bin
tar -xz -f /tmp/gh.tar.gz -C /tmp
cp /tmp/gh_${GH_VERSION}_linux_arm64/bin/gh /config/bin/gh
# Add to PATH for future sessions
echo 'export PATH="/config/bin:$PATH"' >> /config/.bashrc
/configis a persistent volume, so the binary survives container recreations.- Authenticate with a fine‑grained personal access token (PAT) scoped only to the repos Claude needs. This limits blast radius if the container is compromised.
Using Portainer Instead of the Docker Socket
Mounting the Docker socket is equivalent to giving the container root on the host.
A safer alternative is to let Claude talk to Portainer’s REST API.
-
Create a Portainer access token
- Portainer → Account Settings → Access Tokens → Add access token
- Name it something like
claude-code-server.
-
Store the token in the container’s
.envfilePORTAINER_URL=https://portainer.yourdomain.com PORTAINER_TOKEN=your-token-here -
Test the connection from inside the container
curl -s -H "X-API-Key: $PORTAINER_TOKEN" \ $PORTAINER_URL/api/endpoints
Claude can now create containers, inspect services, pull images, etc., without any elevated host access. The token is scoped, auditable, and revocable.
Adding Custom Claude Code Skills (Slash Commands)
Custom skills live in ~/.claude/commands/ on the host. Inside the container this maps to /config/.claude/commands/, which doesn’t exist by default.
Fix – Volume Mount in docker‑compose.yml
volumes:
- /your/config:/config
- /your/projects:/config/workspace/Projects
- ~/.claude/commands:/config/.claude/commands:ro # read‑only mount
- The
:roflag makes the mount read‑only – Claude can use the skills but cannot modify them from inside the container. - Restart the container; the skills appear in the next session.
Agent Definitions & Vault Access
Agent definitions stored as vault notes are already accessible because the vault is mounted as a workspace folder. No extra setup needed.
Persistent Development Environments
Authenticated VS Code
- URL:
https://code.yourdomain.com - Works on iPad, iPhone, any laptop.
- Sessions, extensions, settings, and workspace survive image updates because they live in the
/configvolume.
Obsidian Vault
- Mounted as a workspace folder.
- Notes edited in the browser sync to iCloud instantly.
TL;DR Checklist
- Set
ANTHROPIC_API_KEYto an OAuth token (sk-ant‑oat…). - Install
ghto/config/bin(nosudo). - Prefer Portainer API over Docker socket; store
PORTAINER_URL&PORTAINER_TOKENin.env. - Mount custom skills read‑only via
~/.claude/commands:/config/.claude/commands:ro. - Leverage persistent volumes (
/config, vault, projects) for VS Code and Obsidian.
Following these steps gives Claude Code the ability to interact with external services securely while keeping the container environment clean and reproducible.