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
- Go to
https://dev.nuclide.systems→ log in via Pocket-ID - Create a workspace from a template (admin must create templates first)
- 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_workspacescoder_create_workspacecoder_execute_command← sandbox code executioncoder_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)
- Go to
https://git.nuclide.systems→ complete installation wizard - Set admin account, confirm DB settings (pre-filled from env)
- 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
Recommended repos to create
| 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).