Files
docs/services/dev-environment.md
T

263 lines
9.0 KiB
Markdown

# 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
```bash
# 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
```bash
# 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`):
```json
{
"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:
```bash
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
```bash
# 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`):
```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`):
```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:
```bash
# 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:
```bash
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:**
```bash
# 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:**
```bash
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*:**
```bash
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`).