263 lines
9.0 KiB
Markdown
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`).
|