Self-Hosting Remote VSCode with Cloudflare Tunnel and Authentik SSO

Published: (March 2, 2026 at 04:47 PM EST)
8 min read
Source: Dev.to

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

ComponentPurpose
code‑server (by Coder)VS Code in the browser, packaged as a Docker image by LinuxServer.io
Cloudflare TunnelOutbound‑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_request to 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 -d after changing env vars.
docker restart re‑uses the original environment from the container’s creation, while docker compose up -d re‑reads the compose file and the .env.

Nginx Proxy Manager Configuration

  1. Create a proxy host for code.yourdomain.com

    • Forward Hostname: your server’s local IP (not localhost)
    • Forward Port: 8484
  2. WebSockets Support: ON (required for code‑server)

  3. Force SSL: OFF – Cloudflare terminates TLS at the edge and sends plain HTTP to NPM. Enabling Force SSL creates an infinite redirect loop.

  4. 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.

  1. Reload Nginx after editing the config:
docker exec nginx-proxy-manager nginx -s reload

Common Pitfalls & Fixes

SymptomCauseFix
Credentials don’t update after editing composeRan docker restart instead of docker compose up -dUse docker compose up -d
Password with & breaks composeYAML treats & as an anchorMove secrets to a .env file and reference ${VAR}
Force‑SSL redirect loopNPM forces HTTPS while Cloudflare already sends HTTPDisable Force‑SSL in NPM
Extensions missingcode‑server uses the Open VSX Registry, not Microsoft MarketplaceInstall 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-chat and ms-python.vscode-pylance are 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:

  1. OAuth login – uses your Claude Pro/Max subscription.
  2. 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

  1. Deploy code-server via Docker Compose.
  2. Expose it through Cloudflare Tunnel → Nginx Proxy Manager → Authentik.
  3. Configure NPM with Authentik’s auth_request blocks and disable Force‑SSL.
  4. Install needed extensions (Open VSX only).
  5. Add a startup script to install Node.js and Claude Code CLI.
  6. 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
  • /config is 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.

  1. Create a Portainer access token

    • Portainer → Account Settings → Access Tokens → Add access token
    • Name it something like claude-code-server.
  2. Store the token in the container’s .env file

    PORTAINER_URL=https://portainer.yourdomain.com
    PORTAINER_TOKEN=your-token-here
  3. 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 :ro flag 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 /config volume.

Obsidian Vault

  • Mounted as a workspace folder.
  • Notes edited in the browser sync to iCloud instantly.

TL;DR Checklist

  • Set ANTHROPIC_API_KEY to an OAuth token (sk-ant‑oat…).
  • Install gh to /config/bin (no sudo).
  • Prefer Portainer API over Docker socket; store PORTAINER_URL & PORTAINER_TOKEN in .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.

0 views
Back to Blog

Related posts

Read more »

Google Gemini Writing Challenge

What I Built - Where Gemini fit in - Used Gemini’s multimodal capabilities to let users upload screenshots of notes, diagrams, or code snippets. - Gemini gener...