Files
docs/stacks/docs/dev-environment.md
T

9.0 KiB

Dev Environment — CT 111 (dev.nuclide.systems)

CT 111 hosts the self-hosted development platform: Coder (dev workspaces) + Gitea (internal repos).

Services

Service URL Port
Coder https://dev.nuclide.systems 7080
Gitea https://git.nuclide.systems 3000

Both use Pocket-ID OIDC (https://id.nuclide.systems) for SSO.


Coder — Dev Workspaces

What it is

Coder provisions isolated Docker-based dev environments (workspaces) on CT 111. Each workspace has:

  • A full Linux environment with your tools
  • Persistent home dir on UNAS (/mnt/pve/unas/services/coder/)
  • Intel Arc GPU renderD128 available
  • VS Code Server (browser or desktop SSH tunnel)

First-time setup

  1. Go to https://dev.nuclide.systems → log in via Pocket-ID
  2. Create a workspace from a template (admin must create templates first)
  3. Connect via VS Code: install Coder extension → sign in → open workspace

VS Code connection

# Install Coder CLI on your local machine
curl -fsSL https://coder.com/install.sh | sh

# Authenticate
coder login https://dev.nuclide.systems

# Open workspace in VS Code
coder open <workspace-name>

Or use the Coder VS Code extension directly from the marketplace (coder.coder-remote).

Claude Code inside a workspace

# Inside the workspace terminal
npm install -g @anthropic/claude-code
claude

Claude Code runs inside the workspace container — same environment, same files, same GPU.

Coder MCP (AI agent sandbox execution)

Add to Claude Code's MCP config (~/.claude/claude_desktop_config.json or via /mcp add):

{
  "mcpServers": {
    "coder": {
      "command": "coder",
      "args": ["mcp", "server"],
      "env": {
        "CODER_URL": "https://dev.nuclide.systems",
        "CODER_TOKEN": "<your-api-token>"
      }
    }
  }
}

Claude can then create workspaces, execute code, and read output via MCP tools:

  • coder_list_workspaces
  • coder_create_workspace
  • coder_execute_command ← sandbox code execution
  • coder_start_workspace / coder_stop_workspace

Get your API token: coder tokens create

Creating workspace templates

Templates are Terraform configs stored in Gitea. Basic Docker template:

coder templates push <template-name> --directory ./template/

Gitea — Internal Repos

What it is

Self-hosted Git for internal infrastructure: compose files, CT configs, dotfiles, Coder templates. Not the primary remote for Claude Code collaboration — use GitHub for that.

First-time setup (admin)

  1. Go to https://git.nuclide.systems → complete installation wizard
  2. Set admin account, confirm DB settings (pre-filled from env)
  3. Add OIDC provider: Admin → Site Administration → Authentication Sources
    • Auth type: OAuth2
    • Provider: OpenID Connect
    • Discovery URL: https://id.nuclide.systems/.well-known/openid-configuration
    • Client ID/Secret: create a new client in Pocket-ID for Gitea

SSH access

# Gitea SSH runs on port 222
git clone ssh://git@git.nuclide.systems:222/<user>/<repo>.git

# Or add to ~/.ssh/config:
Host git.nuclide.systems
    Port 222
    IdentityFile ~/.ssh/id_ed25519
Repo Contents
infra/proxmox /etc/pve/ snapshots, CT configs
infra/stacks Compose files from CT 104/101/111
infra/docs Mirror of /docs/ on Proxmox host
dev/templates Coder workspace Terraform templates

Storage layout (UNAS)

/mnt/pve/unas/services/
├── coder/          # Coder workspace home dirs (persistent)
└── gitea/          # Gitea repos + data

CT 111 specs

IP 192.168.1.42
Cores 12
RAM 32GB
Rootfs 60GB local-zfs (Docker image cache)
UNAS /mnt/pve/unas (mp0)
GPU renderD128 (Intel Arc Xe, by-path)

OIDC clients in Pocket-ID

Client Callback URL
Coder https://dev.nuclide.systems/api/v2/users/oidc/callback
Gitea Add via Gitea admin UI (see above)

To create new OIDC clients programmatically, see /docs/proxmox-optimizations.md § OIDC client creation via SQLite.


Auth lockdown (SSO-only)

Both Gitea and Coder are locked to Pocket-ID OIDC only. Local password and GitHub login are disabled. Passkey/WebAuthn login to Gitea remains available because it's tied to OIDC accounts, not to a separate password.

Compose env flags

Coder (/opt/stacks/coder/compose.yaml):

CODER_OIDC_ISSUER_URL: "https://id.nuclide.systems"
CODER_OIDC_CLIENT_ID: "${CODER_OIDC_CLIENT_ID}"
CODER_OIDC_CLIENT_SECRET: "${CODER_OIDC_CLIENT_SECRET}"
CODER_OIDC_ALLOW_SIGNUPS: "true"
CODER_OIDC_EMAIL_DOMAIN: "nucli.de"
CODER_DISABLE_PASSWORD_AUTH: "true"
CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE: "false"

Note: the bundled "GitHub external auth provider" log line is for workspaces cloning from GitHub, not for login — leaving it on is fine.

Gitea (/opt/stacks/gitea/compose.yaml):

GITEA__oauth2__ENABLED: "true"
GITEA__openid__ENABLE_OPENID_SIGNIN: "false"        # legacy OpenID 2.0 button off
GITEA__openid__ENABLE_OPENID_SIGNUP: "false"
GITEA__service__ENABLE_PASSWORD_SIGNIN_FORM: "false" # local username/password form off
GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION: "true"
GITEA__oauth2_client__ACCOUNT_LINKING: "auto"
GITEA__oauth2_client__USERNAME: "preferred_username"
GITEA__oauth2_client__UPDATE_AVATAR: "true"

Note: ENABLE_OPENID_SIGNIN lives in [openid], not [service]. Putting it under GITEA__service__ is a no-op and leaves the legacy button visible.

Gitea login source (DB row in login_source):

  • name = "pocket-id" (case-sensitive — becomes part of the callback URL)
  • Scopes = ["openid","profile","email"]
  • two_factor_policy = "skip" (Pocket-ID passkey already enforces 2FA)

Pocket-ID client secret pitfall (important)

Pocket-ID 2.7.0 verifies client secrets with bcrypt (bcrypt.CompareHashAndPassword). The stored oidc_clients.secret column must be a 60-char bcrypt hash like $2a$10$... or $2b$10$....

A raw 64-char hex SHA-256 in that column silently fails every token exchange with invalid client secret. The Gitea and Coder clients on this host were initially provisioned that way and had to be regenerated.

To programmatically add or rotate an OIDC client secret in Pocket-ID:

# On Proxmox host (has python3-bcrypt installed)
python3 <<'EOF'
import bcrypt, secrets, string
alphabet = string.ascii_letters + string.digits
plain = "".join(secrets.choice(alphabet) for _ in range(40))
hashed = bcrypt.hashpw(plain.encode(), bcrypt.gensalt(rounds=10)).decode()
print("PLAINTEXT (give to client app):", plain)
print("HASH (store in oidc_clients.secret):", hashed)
EOF

Always push the hash to Pocket-ID via a tmp file (pct push 110 ...) — never inline the bcrypt hash in a shell command, the $ chars get expanded.

To verify a client's secret is the right format:

pct exec 110 -- sqlite3 /opt/stacks/pocketid/data/pocket-id.db \
  "SELECT name, length(secret), substr(secret,1,7) FROM oidc_clients;"
# Working clients: length=60, prefix "$2a$10$" or "$2b$10$"
# Broken clients : length=64, prefix is hex (e.g. "1180664")

Break-glass recovery (if SSO is broken)

You have host SSH access, so you're never locked out — but the UI will be unusable until you re-enable a local login path. From the Proxmox host:

Gitea — re-enable local login + reset password:

# 1. Temporarily put the form back
ssh nuc
python3 -c "
p='/rpool/data/subvol-111-disk-0/opt/stacks/gitea/compose.yaml'
s=open(p).read().replace('ENABLE_PASSWORD_SIGNIN_FORM: \"false\"','ENABLE_PASSWORD_SIGNIN_FORM: \"true\"')
open(p,'w').write(s)"
pct exec 111 -- bash -c 'cd /opt/stacks/gitea && docker compose up -d --force-recreate gitea'

# 2. Reset admin password
pct exec 111 -- docker exec -u git gitea gitea -c /data/gitea/conf/app.ini admin user change-password -u fkrebs -p 'temp-strong-pass'

Coder — flip user back to password login + re-enable:

ssh nuc
# 1. Flip login_type back
pct exec 111 -- docker exec coder-db psql -U coder -d coder -c \
  "UPDATE users SET login_type='password' WHERE username='fkrebs';"

# 2. Re-enable password auth in compose
python3 -c "
p='/rpool/data/subvol-111-disk-0/opt/stacks/coder/compose.yaml'
s=open(p).read().replace('CODER_DISABLE_PASSWORD_AUTH: \"true\"','CODER_DISABLE_PASSWORD_AUTH: \"false\"')
open(p,'w').write(s)"
pct exec 111 -- bash -c 'cd /opt/stacks/coder && docker compose up -d --force-recreate coder'

# 3. Reset password (interactive)
pct exec 111 -- docker exec -it coder coder reset-password fkrebs

Pocket-ID — bootstrap a one-time access token if you're locked out of Pocket-ID itself:

pct exec 110 -- docker exec pocket-id pocket-id-cli one-time-access-token \
  --email fkrebs@nucli.de --duration 1h
# Open the printed URL in a browser to log in once and register a new passkey.

After SSO is fixed: undo each of the above steps (re-disable password auth, flip login_type back to oidc).