Files

2666 lines
66 KiB
HTML

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Single source of truth for the homelab">
<link rel="canonical" href="http://192.168.1.8:13080/services/dev-environment/">
<link rel="prev" href="../homelab-architecture/">
<link rel="next" href="../mcp-gateway/">
<link rel="icon" href="../../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.6">
<title>Dev environment (Coder + Gitea) - nuclide.systems docs</title>
<link rel="stylesheet" href="../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="black" data-md-color-accent="blue">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#dev-environment-ct-104-devnuclidesystems" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../.." title="nuclide.systems docs" class="md-header__button md-logo" aria-label="nuclide.systems docs" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
nuclide.systems docs
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Dev environment (Coder + Gitea)
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="(prefers-color-scheme: dark)" data-md-color-scheme="slate" data-md-color-primary="black" data-md-color-accent="blue" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
<input class="md-option" data-md-color-media="(prefers-color-scheme: light)" data-md-color-scheme="default" data-md-color-primary="black" data-md-color-accent="blue" aria-hidden="true" type="radio" name="__palette" id="__palette_1">
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item">
<a href="../../CHANGELOG/" class="md-tabs__link">
Changelog
</a>
</li>
<li class="md-tabs__item">
<a href="../../RESUME/" class="md-tabs__link">
Resume
</a>
</li>
<li class="md-tabs__item">
<a href="../../ct-inventory/" class="md-tabs__link">
CT inventory
</a>
</li>
<li class="md-tabs__item">
<a href="../../infra/proxmox-state/" class="md-tabs__link">
Infra
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../homelab-architecture/" class="md-tabs__link">
Services
</a>
</li>
<li class="md-tabs__item">
<a href="../../security/data-leak-audit-comparison/" class="md-tabs__link">
Security & audits
</a>
</li>
<li class="md-tabs__item">
<a href="../../ideas/stack-ideas/" class="md-tabs__link">
Ideas
</a>
</li>
<li class="md-tabs__item">
<a href="../../history/traefik-migration/" class="md-tabs__link">
History
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../.." title="nuclide.systems docs" class="md-nav__button md-logo" aria-label="nuclide.systems docs" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
nuclide.systems docs
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../CHANGELOG/" class="md-nav__link">
<span class="md-ellipsis">
Changelog
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../RESUME/" class="md-nav__link">
<span class="md-ellipsis">
Resume
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../ct-inventory/" class="md-nav__link">
<span class="md-ellipsis">
CT inventory
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_5" >
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="0">
<span class="md-ellipsis">
Infra
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Infra
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../infra/proxmox-state/" class="md-nav__link">
<span class="md-ellipsis">
Proxmox state
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/portmap/" class="md-nav__link">
<span class="md-ellipsis">
Port map
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/storage/" class="md-nav__link">
<span class="md-ellipsis">
Storage
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/volumes/" class="md-nav__link">
<span class="md-ellipsis">
Volumes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/docker-networks/" class="md-nav__link">
<span class="md-ellipsis">
Docker networks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/connection-hosts/" class="md-nav__link">
<span class="md-ellipsis">
Connection hosts
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/proxmox-memory-audit/" class="md-nav__link">
<span class="md-ellipsis">
Memory audit
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_6" checked>
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
Services
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../homelab-architecture/" class="md-nav__link">
<span class="md-ellipsis">
Homelab architecture
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Dev environment (Coder + Gitea)
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Dev environment (Coder + Gitea)
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#services" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#coder-dev-workspaces" class="md-nav__link">
<span class="md-ellipsis">
Coder — Dev Workspaces
</span>
</a>
<nav class="md-nav" aria-label="Coder — Dev Workspaces">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#what-it-is" class="md-nav__link">
<span class="md-ellipsis">
What it is
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#first-time-setup" class="md-nav__link">
<span class="md-ellipsis">
First-time setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#vs-code-connection" class="md-nav__link">
<span class="md-ellipsis">
VS Code connection
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#claude-code-inside-a-workspace" class="md-nav__link">
<span class="md-ellipsis">
Claude Code inside a workspace
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#coder-mcp-ai-agent-sandbox-execution" class="md-nav__link">
<span class="md-ellipsis">
Coder MCP (AI agent sandbox execution)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#creating-workspace-templates" class="md-nav__link">
<span class="md-ellipsis">
Creating workspace templates
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#gitea-internal-repos" class="md-nav__link">
<span class="md-ellipsis">
Gitea — Internal Repos
</span>
</a>
<nav class="md-nav" aria-label="Gitea — Internal Repos">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#what-it-is_1" class="md-nav__link">
<span class="md-ellipsis">
What it is
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#first-time-setup-admin" class="md-nav__link">
<span class="md-ellipsis">
First-time setup (admin)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ssh-access" class="md-nav__link">
<span class="md-ellipsis">
SSH access
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#recommended-repos-to-create" class="md-nav__link">
<span class="md-ellipsis">
Recommended repos to create
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#storage-layout-unas" class="md-nav__link">
<span class="md-ellipsis">
Storage layout (UNAS)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-104-specs-current-host" class="md-nav__link">
<span class="md-ellipsis">
CT 104 specs (current host)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#oidc-clients-in-pocket-id" class="md-nav__link">
<span class="md-ellipsis">
OIDC clients in Pocket-ID
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#auth-lockdown-sso-only" class="md-nav__link">
<span class="md-ellipsis">
Auth lockdown (SSO-only)
</span>
</a>
<nav class="md-nav" aria-label="Auth lockdown (SSO-only)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#compose-env-flags" class="md-nav__link">
<span class="md-ellipsis">
Compose env flags
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pocket-id-client-secret-pitfall-important" class="md-nav__link">
<span class="md-ellipsis">
Pocket-ID client secret pitfall (important)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#break-glass-recovery-if-sso-is-broken" class="md-nav__link">
<span class="md-ellipsis">
Break-glass recovery (if SSO is broken)
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../mcp-gateway/" class="md-nav__link">
<span class="md-ellipsis">
MCP gateway
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../mcp-servers/" class="md-nav__link">
<span class="md-ellipsis">
MCP servers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../pocket-id/" class="md-nav__link">
<span class="md-ellipsis">
Pocket-ID (OIDC)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../backrest/" class="md-nav__link">
<span class="md-ellipsis">
Backrest (backups)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../databases/" class="md-nav__link">
<span class="md-ellipsis">
Databases
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../secrets-manager/" class="md-nav__link">
<span class="md-ellipsis">
Secrets manager
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../comfyui/" class="md-nav__link">
<span class="md-ellipsis">
ComfyUI
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../zoraxy/" class="md-nav__link">
<span class="md-ellipsis">
Zoraxy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../adguard-dns/" class="md-nav__link">
<span class="md-ellipsis">
AdGuard DNS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../cloud-gpu/" class="md-nav__link">
<span class="md-ellipsis">
Cloud GPU
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../arcane/" class="md-nav__link">
<span class="md-ellipsis">
Arcane
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../doc-ingestion/" class="md-nav__link">
<span class="md-ellipsis">
Doc ingestion pipeline
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../llm-benchmark/" class="md-nav__link">
<span class="md-ellipsis">
LLM benchmark
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_7" >
<label class="md-nav__link" for="__nav_7" id="__nav_7_label" tabindex="0">
<span class="md-ellipsis">
Security & audits
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_7_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_7">
<span class="md-nav__icon md-icon"></span>
Security & audits
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../security/data-leak-audit-comparison/" class="md-nav__link">
<span class="md-ellipsis">
Comparison
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/data-leak-audit-2026-05-20-tr004-cloud-sandbox/" class="md-nav__link">
<span class="md-ellipsis">
2026-05-20 · cloud-sandbox breach
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/data-leak-audit-2026-05-21-tr004-artifacts/" class="md-nav__link">
<span class="md-ellipsis">
2026-05-21 · artifacts (clean)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/audit-claude-code-meta/" class="md-nav__link">
<span class="md-ellipsis">
Self-audit (Claude Code)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/transcripts/audit-chat-2026-05-21/" class="md-nav__link">
<span class="md-ellipsis">
Transcript (audit session)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_8" >
<label class="md-nav__link" for="__nav_8" id="__nav_8_label" tabindex="0">
<span class="md-ellipsis">
Ideas
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_8_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_8">
<span class="md-nav__icon md-icon"></span>
Ideas
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../ideas/stack-ideas/" class="md-nav__link">
<span class="md-ellipsis">
Stack ideas
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_9" >
<label class="md-nav__link" for="__nav_9" id="__nav_9_label" tabindex="0">
<span class="md-ellipsis">
History
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_9_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_9">
<span class="md-nav__icon md-icon"></span>
History
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../history/traefik-migration/" class="md-nav__link">
<span class="md-ellipsis">
Traefik (abandoned 2026-05-16)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../history/traefik-migration-docker-labels/" class="md-nav__link">
<span class="md-ellipsis">
Traefik labels (abandoned)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../history/mcp-gateway-requirements/" class="md-nav__link">
<span class="md-ellipsis">
MCP gateway requirements (superseded)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../history/scrubbing-list-2026-05-17/" class="md-nav__link">
<span class="md-ellipsis">
Scrubbing list (2026-05-17)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../history/case-study/" class="md-nav__link">
<span class="md-ellipsis">
Case study
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#services" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#coder-dev-workspaces" class="md-nav__link">
<span class="md-ellipsis">
Coder — Dev Workspaces
</span>
</a>
<nav class="md-nav" aria-label="Coder — Dev Workspaces">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#what-it-is" class="md-nav__link">
<span class="md-ellipsis">
What it is
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#first-time-setup" class="md-nav__link">
<span class="md-ellipsis">
First-time setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#vs-code-connection" class="md-nav__link">
<span class="md-ellipsis">
VS Code connection
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#claude-code-inside-a-workspace" class="md-nav__link">
<span class="md-ellipsis">
Claude Code inside a workspace
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#coder-mcp-ai-agent-sandbox-execution" class="md-nav__link">
<span class="md-ellipsis">
Coder MCP (AI agent sandbox execution)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#creating-workspace-templates" class="md-nav__link">
<span class="md-ellipsis">
Creating workspace templates
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#gitea-internal-repos" class="md-nav__link">
<span class="md-ellipsis">
Gitea — Internal Repos
</span>
</a>
<nav class="md-nav" aria-label="Gitea — Internal Repos">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#what-it-is_1" class="md-nav__link">
<span class="md-ellipsis">
What it is
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#first-time-setup-admin" class="md-nav__link">
<span class="md-ellipsis">
First-time setup (admin)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ssh-access" class="md-nav__link">
<span class="md-ellipsis">
SSH access
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#recommended-repos-to-create" class="md-nav__link">
<span class="md-ellipsis">
Recommended repos to create
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#storage-layout-unas" class="md-nav__link">
<span class="md-ellipsis">
Storage layout (UNAS)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ct-104-specs-current-host" class="md-nav__link">
<span class="md-ellipsis">
CT 104 specs (current host)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#oidc-clients-in-pocket-id" class="md-nav__link">
<span class="md-ellipsis">
OIDC clients in Pocket-ID
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#auth-lockdown-sso-only" class="md-nav__link">
<span class="md-ellipsis">
Auth lockdown (SSO-only)
</span>
</a>
<nav class="md-nav" aria-label="Auth lockdown (SSO-only)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#compose-env-flags" class="md-nav__link">
<span class="md-ellipsis">
Compose env flags
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pocket-id-client-secret-pitfall-important" class="md-nav__link">
<span class="md-ellipsis">
Pocket-ID client secret pitfall (important)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#break-glass-recovery-if-sso-is-broken" class="md-nav__link">
<span class="md-ellipsis">
Break-glass recovery (if SSO is broken)
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="dev-environment-ct-104-devnuclidesystems">Dev Environment — CT 104 (dev.nuclide.systems)<a class="headerlink" href="#dev-environment-ct-104-devnuclidesystems" title="Permanent link">&para;</a></h1>
<p><strong>Migrated from CT 111 to CT 104 on 2026-05-26.</strong> CT 111 ("dev") is decommissioned; LXC pending removal.</p>
<p>CT 104 hosts the self-hosted development platform: <strong>Coder</strong> (dev workspaces) + <strong>Gitea</strong> (internal repos).</p>
<h2 id="services">Services<a class="headerlink" href="#services" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Service</th>
<th>URL</th>
<th>Port</th>
</tr>
</thead>
<tbody>
<tr>
<td>Coder</td>
<td>https://dev.nuclide.systems</td>
<td>7080</td>
</tr>
<tr>
<td>Gitea</td>
<td>https://git.nuclide.systems</td>
<td>3000</td>
</tr>
</tbody>
</table>
<p>Both use <strong>Pocket-ID OIDC</strong> (<code>https://id.nuclide.systems</code>) for SSO.</p>
<hr />
<h2 id="coder-dev-workspaces">Coder — Dev Workspaces<a class="headerlink" href="#coder-dev-workspaces" title="Permanent link">&para;</a></h2>
<h3 id="what-it-is">What it is<a class="headerlink" href="#what-it-is" title="Permanent link">&para;</a></h3>
<p>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 (<code>/mnt/pve/unas/services/coder/</code>)
- Intel Arc GPU renderD128 available
- VS Code Server (browser or desktop SSH tunnel)</p>
<h3 id="first-time-setup">First-time setup<a class="headerlink" href="#first-time-setup" title="Permanent link">&para;</a></h3>
<ol>
<li>Go to <code>https://dev.nuclide.systems</code> → log in via Pocket-ID</li>
<li>Create a workspace from a template (admin must create templates first)</li>
<li>Connect via VS Code: install <strong>Coder extension</strong> → sign in → open workspace</li>
</ol>
<h3 id="vs-code-connection">VS Code connection<a class="headerlink" href="#vs-code-connection" title="Permanent link">&para;</a></h3>
<p><div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="c1"># Install Coder CLI on your local machine</span>
<a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a>curl<span class="w"> </span>-fsSL<span class="w"> </span>https://coder.com/install.sh<span class="w"> </span><span class="p">|</span><span class="w"> </span>sh
<a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a>
<a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a><span class="c1"># Authenticate</span>
<a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a>coder<span class="w"> </span>login<span class="w"> </span>https://dev.nuclide.systems
<a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a>
<a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a><span class="c1"># Open workspace in VS Code</span>
<a id="__codelineno-0-8" name="__codelineno-0-8" href="#__codelineno-0-8"></a>coder<span class="w"> </span>open<span class="w"> </span>&lt;workspace-name&gt;
</code></pre></div>
Or use the Coder VS Code extension directly from the marketplace (<code>coder.coder-remote</code>).</p>
<h3 id="claude-code-inside-a-workspace">Claude Code inside a workspace<a class="headerlink" href="#claude-code-inside-a-workspace" title="Permanent link">&para;</a></h3>
<p><div class="highlight"><pre><span></span><code><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="c1"># Inside the workspace terminal</span>
<a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a>npm<span class="w"> </span>install<span class="w"> </span>-g<span class="w"> </span>@anthropic/claude-code
<a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a>claude
</code></pre></div>
Claude Code runs inside the workspace container — same environment, same files, same GPU.</p>
<h3 id="coder-mcp-ai-agent-sandbox-execution">Coder MCP (AI agent sandbox execution)<a class="headerlink" href="#coder-mcp-ai-agent-sandbox-execution" title="Permanent link">&para;</a></h3>
<p>Add to Claude Code's MCP config (<code>~/.claude/claude_desktop_config.json</code> or via <code>/mcp add</code>):
<div class="highlight"><pre><span></span><code><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a><span class="p">{</span>
<a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a><span class="w"> </span><span class="nt">&quot;mcpServers&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<a id="__codelineno-2-3" name="__codelineno-2-3" href="#__codelineno-2-3"></a><span class="w"> </span><span class="nt">&quot;coder&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<a id="__codelineno-2-4" name="__codelineno-2-4" href="#__codelineno-2-4"></a><span class="w"> </span><span class="nt">&quot;command&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;coder&quot;</span><span class="p">,</span>
<a id="__codelineno-2-5" name="__codelineno-2-5" href="#__codelineno-2-5"></a><span class="w"> </span><span class="nt">&quot;args&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&quot;mcp&quot;</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;server&quot;</span><span class="p">],</span>
<a id="__codelineno-2-6" name="__codelineno-2-6" href="#__codelineno-2-6"></a><span class="w"> </span><span class="nt">&quot;env&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<a id="__codelineno-2-7" name="__codelineno-2-7" href="#__codelineno-2-7"></a><span class="w"> </span><span class="nt">&quot;CODER_URL&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;https://dev.nuclide.systems&quot;</span><span class="p">,</span>
<a id="__codelineno-2-8" name="__codelineno-2-8" href="#__codelineno-2-8"></a><span class="w"> </span><span class="nt">&quot;CODER_TOKEN&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;&lt;your-api-token&gt;&quot;</span>
<a id="__codelineno-2-9" name="__codelineno-2-9" href="#__codelineno-2-9"></a><span class="w"> </span><span class="p">}</span>
<a id="__codelineno-2-10" name="__codelineno-2-10" href="#__codelineno-2-10"></a><span class="w"> </span><span class="p">}</span>
<a id="__codelineno-2-11" name="__codelineno-2-11" href="#__codelineno-2-11"></a><span class="w"> </span><span class="p">}</span>
<a id="__codelineno-2-12" name="__codelineno-2-12" href="#__codelineno-2-12"></a><span class="p">}</span>
</code></pre></div>
Claude can then create workspaces, execute code, and read output via MCP tools:
- <code>coder_list_workspaces</code>
- <code>coder_create_workspace</code>
- <code>coder_execute_command</code> ← sandbox code execution
- <code>coder_start_workspace</code> / <code>coder_stop_workspace</code></p>
<p>Get your API token: <code>coder tokens create</code></p>
<h3 id="creating-workspace-templates">Creating workspace templates<a class="headerlink" href="#creating-workspace-templates" title="Permanent link">&para;</a></h3>
<p>Templates are Terraform configs stored in Gitea. Basic Docker template:
<div class="highlight"><pre><span></span><code><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a>coder<span class="w"> </span>templates<span class="w"> </span>push<span class="w"> </span>&lt;template-name&gt;<span class="w"> </span>--directory<span class="w"> </span>./template/
</code></pre></div></p>
<hr />
<h2 id="gitea-internal-repos">Gitea — Internal Repos<a class="headerlink" href="#gitea-internal-repos" title="Permanent link">&para;</a></h2>
<h3 id="what-it-is_1">What it is<a class="headerlink" href="#what-it-is_1" title="Permanent link">&para;</a></h3>
<p>Self-hosted Git for internal infrastructure: compose files, CT configs, dotfiles, Coder templates.
<strong>Not</strong> the primary remote for Claude Code collaboration — use GitHub for that.</p>
<h3 id="first-time-setup-admin">First-time setup (admin)<a class="headerlink" href="#first-time-setup-admin" title="Permanent link">&para;</a></h3>
<ol>
<li>Go to <code>https://git.nuclide.systems</code> → complete installation wizard</li>
<li>Set admin account, confirm DB settings (pre-filled from env)</li>
<li>Add OIDC provider: Admin → Site Administration → Authentication Sources</li>
<li>Auth type: OAuth2</li>
<li>Provider: OpenID Connect</li>
<li>Discovery URL: <code>https://id.nuclide.systems/.well-known/openid-configuration</code></li>
<li>Client ID/Secret: create a new client in Pocket-ID for Gitea</li>
</ol>
<h3 id="ssh-access">SSH access<a class="headerlink" href="#ssh-access" title="Permanent link">&para;</a></h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a><span class="c1"># Gitea SSH runs on port 222</span>
<a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a>git<span class="w"> </span>clone<span class="w"> </span>ssh://git@git.nuclide.systems:222/&lt;user&gt;/&lt;repo&gt;.git
<a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a>
<a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="c1"># Or add to ~/.ssh/config:</span>
<a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a>Host<span class="w"> </span>git.nuclide.systems
<a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a><span class="w"> </span>Port<span class="w"> </span><span class="m">222</span>
<a id="__codelineno-4-7" name="__codelineno-4-7" href="#__codelineno-4-7"></a><span class="w"> </span>IdentityFile<span class="w"> </span>~/.ssh/id_ed25519
</code></pre></div>
<h3 id="recommended-repos-to-create">Recommended repos to create<a class="headerlink" href="#recommended-repos-to-create" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Repo</th>
<th>Contents</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>infra/proxmox</code></td>
<td><code>/etc/pve/</code> snapshots, CT configs</td>
</tr>
<tr>
<td><code>infra/stacks</code></td>
<td>Compose files from CT 104/101/111</td>
</tr>
<tr>
<td><code>infra/docs</code></td>
<td>Mirror of <code>/docs/</code> on Proxmox host</td>
</tr>
<tr>
<td><code>dev/templates</code></td>
<td>Coder workspace Terraform templates</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="storage-layout-unas">Storage layout (UNAS)<a class="headerlink" href="#storage-layout-unas" title="Permanent link">&para;</a></h2>
<div class="highlight"><pre><span></span><code><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a>/mnt/pve/unas/services/
<a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a>├── coder/ # Coder workspace home dirs (persistent)
<a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a>└── gitea/ # Gitea repos + data
</code></pre></div>
<hr />
<h2 id="ct-104-specs-current-host">CT 104 specs (current host)<a class="headerlink" href="#ct-104-specs-current-host" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>IP</td>
<td>192.168.1.40</td>
</tr>
<tr>
<td>Cores</td>
<td>16</td>
</tr>
<tr>
<td>RAM</td>
<td>48GB</td>
</tr>
<tr>
<td>Rootfs</td>
<td>200GB local-zfs</td>
</tr>
<tr>
<td>UNAS</td>
<td>/mnt/pve/unas (mp0) — coder + gitea data paths unchanged</td>
</tr>
<tr>
<td>GPU</td>
<td>renderD128 (Intel Arc Xe, idmapped)</td>
</tr>
</tbody>
</table>
<p>Compose files at <code>/opt/stacks/coder/</code> and <code>/opt/stacks/gitea/</code> on CT 104.
Gitea uses Redis (<code>gitea-redis</code>) for queue/cache/session — required because CT 104 uses idmapped NFS which doesn't support LevelDB file locks.
Act-runner at <code>/opt/stacks/act-runner/</code> — runner name <code>ct104-runner</code>.</p>
<hr />
<h2 id="oidc-clients-in-pocket-id">OIDC clients in Pocket-ID<a class="headerlink" href="#oidc-clients-in-pocket-id" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Client</th>
<th>Callback URL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Coder</td>
<td><code>https://dev.nuclide.systems/api/v2/users/oidc/callback</code></td>
</tr>
<tr>
<td>Gitea</td>
<td>Add via Gitea admin UI (see above)</td>
</tr>
</tbody>
</table>
<p>To create new OIDC clients programmatically, see <code>/docs/proxmox-optimizations.md</code> § OIDC client creation via SQLite.</p>
<hr />
<h2 id="auth-lockdown-sso-only">Auth lockdown (SSO-only)<a class="headerlink" href="#auth-lockdown-sso-only" title="Permanent link">&para;</a></h2>
<p>Both Gitea and Coder are locked to <strong>Pocket-ID OIDC only</strong>. Local password and GitHub login are disabled. Passkey/WebAuthn login <em>to Gitea</em> remains available because it's tied to OIDC accounts, not to a separate password.</p>
<h3 id="compose-env-flags">Compose env flags<a class="headerlink" href="#compose-env-flags" title="Permanent link">&para;</a></h3>
<p><strong>Coder</strong> (<code>/opt/stacks/coder/compose.yaml</code>):
<div class="highlight"><pre><span></span><code><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a><span class="nt">CODER_OIDC_ISSUER_URL</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;https://id.nuclide.systems&quot;</span>
<a id="__codelineno-6-2" name="__codelineno-6-2" href="#__codelineno-6-2"></a><span class="nt">CODER_OIDC_CLIENT_ID</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;${CODER_OIDC_CLIENT_ID}&quot;</span>
<a id="__codelineno-6-3" name="__codelineno-6-3" href="#__codelineno-6-3"></a><span class="nt">CODER_OIDC_CLIENT_SECRET</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;${CODER_OIDC_CLIENT_SECRET}&quot;</span>
<a id="__codelineno-6-4" name="__codelineno-6-4" href="#__codelineno-6-4"></a><span class="nt">CODER_OIDC_ALLOW_SIGNUPS</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;true&quot;</span>
<a id="__codelineno-6-5" name="__codelineno-6-5" href="#__codelineno-6-5"></a><span class="nt">CODER_OIDC_EMAIL_DOMAIN</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;nucli.de&quot;</span>
<a id="__codelineno-6-6" name="__codelineno-6-6" href="#__codelineno-6-6"></a><span class="nt">CODER_DISABLE_PASSWORD_AUTH</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;true&quot;</span>
<a id="__codelineno-6-7" name="__codelineno-6-7" href="#__codelineno-6-7"></a><span class="nt">CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;false&quot;</span>
</code></pre></div>
Note: the bundled "GitHub external auth provider" log line is for <em>workspaces</em> cloning from GitHub, not for login — leaving it on is fine.</p>
<p><strong>Gitea</strong> (<code>/opt/stacks/gitea/compose.yaml</code>):
<div class="highlight"><pre><span></span><code><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a><span class="nt">GITEA__oauth2__ENABLED</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;true&quot;</span>
<a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a><span class="nt">GITEA__openid__ENABLE_OPENID_SIGNIN</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;false&quot;</span><span class="w"> </span><span class="c1"># legacy OpenID 2.0 button off</span>
<a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a><span class="nt">GITEA__openid__ENABLE_OPENID_SIGNUP</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;false&quot;</span>
<a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a><span class="nt">GITEA__service__ENABLE_PASSWORD_SIGNIN_FORM</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;false&quot;</span><span class="w"> </span><span class="c1"># local username/password form off</span>
<a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a><span class="nt">GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;true&quot;</span>
<a id="__codelineno-7-6" name="__codelineno-7-6" href="#__codelineno-7-6"></a><span class="nt">GITEA__oauth2_client__ACCOUNT_LINKING</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;auto&quot;</span>
<a id="__codelineno-7-7" name="__codelineno-7-7" href="#__codelineno-7-7"></a><span class="nt">GITEA__oauth2_client__USERNAME</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;preferred_username&quot;</span>
<a id="__codelineno-7-8" name="__codelineno-7-8" href="#__codelineno-7-8"></a><span class="nt">GITEA__oauth2_client__UPDATE_AVATAR</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;true&quot;</span>
</code></pre></div>
Note: <code>ENABLE_OPENID_SIGNIN</code> lives in <code>[openid]</code>, <em>not</em> <code>[service]</code>. Putting it under <code>GITEA__service__</code> is a no-op and leaves the legacy button visible.</p>
<p>Gitea login source (DB row in <code>login_source</code>):
- <code>name = "pocket-id"</code> (case-sensitive — becomes part of the callback URL)
- <code>Scopes = ["openid","profile","email"]</code>
- <code>two_factor_policy = "skip"</code> (Pocket-ID passkey already enforces 2FA)</p>
<h3 id="pocket-id-client-secret-pitfall-important">Pocket-ID client secret pitfall (important)<a class="headerlink" href="#pocket-id-client-secret-pitfall-important" title="Permanent link">&para;</a></h3>
<p>Pocket-ID 2.7.0 verifies client secrets with <strong>bcrypt</strong> (<code>bcrypt.CompareHashAndPassword</code>). The stored <code>oidc_clients.secret</code> column must be a 60-char bcrypt hash like <code>$2a$10$...</code> or <code>$2b$10$...</code>.</p>
<p>A raw 64-char hex SHA-256 in that column <strong>silently fails every token exchange</strong> with <code>invalid client secret</code>. The Gitea and Coder clients on this host were initially provisioned that way and had to be regenerated.</p>
<p>To programmatically add or rotate an OIDC client secret in Pocket-ID:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a><span class="c1"># On Proxmox host (has python3-bcrypt installed)</span>
<a id="__codelineno-8-2" name="__codelineno-8-2" href="#__codelineno-8-2"></a>python3<span class="w"> </span><span class="s">&lt;&lt;&#39;EOF&#39;</span>
<a id="__codelineno-8-3" name="__codelineno-8-3" href="#__codelineno-8-3"></a><span class="s">import bcrypt, secrets, string</span>
<a id="__codelineno-8-4" name="__codelineno-8-4" href="#__codelineno-8-4"></a><span class="s">alphabet = string.ascii_letters + string.digits</span>
<a id="__codelineno-8-5" name="__codelineno-8-5" href="#__codelineno-8-5"></a><span class="s">plain = &quot;&quot;.join(secrets.choice(alphabet) for _ in range(40))</span>
<a id="__codelineno-8-6" name="__codelineno-8-6" href="#__codelineno-8-6"></a><span class="s">hashed = bcrypt.hashpw(plain.encode(), bcrypt.gensalt(rounds=10)).decode()</span>
<a id="__codelineno-8-7" name="__codelineno-8-7" href="#__codelineno-8-7"></a><span class="s">print(&quot;PLAINTEXT (give to client app):&quot;, plain)</span>
<a id="__codelineno-8-8" name="__codelineno-8-8" href="#__codelineno-8-8"></a><span class="s">print(&quot;HASH (store in oidc_clients.secret):&quot;, hashed)</span>
<a id="__codelineno-8-9" name="__codelineno-8-9" href="#__codelineno-8-9"></a><span class="s">EOF</span>
</code></pre></div>
<p>Always push the hash to Pocket-ID via a tmp file (<code>pct push 110 ...</code>) — never inline the bcrypt hash in a shell command, the <code>$</code> chars get expanded.</p>
<p>To verify a client's secret is the right format:
<div class="highlight"><pre><span></span><code><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a>pct<span class="w"> </span><span class="nb">exec</span><span class="w"> </span><span class="m">110</span><span class="w"> </span>--<span class="w"> </span>sqlite3<span class="w"> </span>/opt/stacks/pocketid/data/pocket-id.db<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-9-2" name="__codelineno-9-2" href="#__codelineno-9-2"></a><span class="w"> </span><span class="s2">&quot;SELECT name, length(secret), substr(secret,1,7) FROM oidc_clients;&quot;</span>
<a id="__codelineno-9-3" name="__codelineno-9-3" href="#__codelineno-9-3"></a><span class="c1"># Working clients: length=60, prefix &quot;$2a$10$&quot; or &quot;$2b$10$&quot;</span>
<a id="__codelineno-9-4" name="__codelineno-9-4" href="#__codelineno-9-4"></a><span class="c1"># Broken clients : length=64, prefix is hex (e.g. &quot;1180664&quot;)</span>
</code></pre></div></p>
<h3 id="break-glass-recovery-if-sso-is-broken">Break-glass recovery (if SSO is broken)<a class="headerlink" href="#break-glass-recovery-if-sso-is-broken" title="Permanent link">&para;</a></h3>
<p>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:</p>
<p><strong>Gitea — re-enable local login + reset password:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a><span class="c1"># 1. Temporarily put the form back</span>
<a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a>ssh<span class="w"> </span>nuc
<a id="__codelineno-10-3" name="__codelineno-10-3" href="#__codelineno-10-3"></a>python3<span class="w"> </span>-c<span class="w"> </span><span class="s2">&quot;</span>
<a id="__codelineno-10-4" name="__codelineno-10-4" href="#__codelineno-10-4"></a><span class="s2">p=&#39;/rpool/data/subvol-111-disk-0/opt/stacks/gitea/compose.yaml&#39;</span>
<a id="__codelineno-10-5" name="__codelineno-10-5" href="#__codelineno-10-5"></a><span class="s2">s=open(p).read().replace(&#39;ENABLE_PASSWORD_SIGNIN_FORM: \&quot;false\&quot;&#39;,&#39;ENABLE_PASSWORD_SIGNIN_FORM: \&quot;true\&quot;&#39;)</span>
<a id="__codelineno-10-6" name="__codelineno-10-6" href="#__codelineno-10-6"></a><span class="s2">open(p,&#39;w&#39;).write(s)&quot;</span>
<a id="__codelineno-10-7" name="__codelineno-10-7" href="#__codelineno-10-7"></a>pct<span class="w"> </span><span class="nb">exec</span><span class="w"> </span><span class="m">111</span><span class="w"> </span>--<span class="w"> </span>bash<span class="w"> </span>-c<span class="w"> </span><span class="s1">&#39;cd /opt/stacks/gitea &amp;&amp; docker compose up -d --force-recreate gitea&#39;</span>
<a id="__codelineno-10-8" name="__codelineno-10-8" href="#__codelineno-10-8"></a>
<a id="__codelineno-10-9" name="__codelineno-10-9" href="#__codelineno-10-9"></a><span class="c1"># 2. Reset admin password</span>
<a id="__codelineno-10-10" name="__codelineno-10-10" href="#__codelineno-10-10"></a>pct<span class="w"> </span><span class="nb">exec</span><span class="w"> </span><span class="m">111</span><span class="w"> </span>--<span class="w"> </span>docker<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>-u<span class="w"> </span>git<span class="w"> </span>gitea<span class="w"> </span>gitea<span class="w"> </span>-c<span class="w"> </span>/data/gitea/conf/app.ini<span class="w"> </span>admin<span class="w"> </span>user<span class="w"> </span>change-password<span class="w"> </span>-u<span class="w"> </span>fkrebs<span class="w"> </span>-p<span class="w"> </span><span class="s1">&#39;temp-strong-pass&#39;</span>
</code></pre></div></p>
<p><strong>Coder — flip user back to password login + re-enable:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-11-1" name="__codelineno-11-1" href="#__codelineno-11-1"></a>ssh<span class="w"> </span>nuc
<a id="__codelineno-11-2" name="__codelineno-11-2" href="#__codelineno-11-2"></a><span class="c1"># 1. Flip login_type back</span>
<a id="__codelineno-11-3" name="__codelineno-11-3" href="#__codelineno-11-3"></a>pct<span class="w"> </span><span class="nb">exec</span><span class="w"> </span><span class="m">111</span><span class="w"> </span>--<span class="w"> </span>docker<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>coder-db<span class="w"> </span>psql<span class="w"> </span>-U<span class="w"> </span>coder<span class="w"> </span>-d<span class="w"> </span>coder<span class="w"> </span>-c<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-11-4" name="__codelineno-11-4" href="#__codelineno-11-4"></a><span class="w"> </span><span class="s2">&quot;UPDATE users SET login_type=&#39;password&#39; WHERE username=&#39;fkrebs&#39;;&quot;</span>
<a id="__codelineno-11-5" name="__codelineno-11-5" href="#__codelineno-11-5"></a>
<a id="__codelineno-11-6" name="__codelineno-11-6" href="#__codelineno-11-6"></a><span class="c1"># 2. Re-enable password auth in compose</span>
<a id="__codelineno-11-7" name="__codelineno-11-7" href="#__codelineno-11-7"></a>python3<span class="w"> </span>-c<span class="w"> </span><span class="s2">&quot;</span>
<a id="__codelineno-11-8" name="__codelineno-11-8" href="#__codelineno-11-8"></a><span class="s2">p=&#39;/rpool/data/subvol-111-disk-0/opt/stacks/coder/compose.yaml&#39;</span>
<a id="__codelineno-11-9" name="__codelineno-11-9" href="#__codelineno-11-9"></a><span class="s2">s=open(p).read().replace(&#39;CODER_DISABLE_PASSWORD_AUTH: \&quot;true\&quot;&#39;,&#39;CODER_DISABLE_PASSWORD_AUTH: \&quot;false\&quot;&#39;)</span>
<a id="__codelineno-11-10" name="__codelineno-11-10" href="#__codelineno-11-10"></a><span class="s2">open(p,&#39;w&#39;).write(s)&quot;</span>
<a id="__codelineno-11-11" name="__codelineno-11-11" href="#__codelineno-11-11"></a>pct<span class="w"> </span><span class="nb">exec</span><span class="w"> </span><span class="m">111</span><span class="w"> </span>--<span class="w"> </span>bash<span class="w"> </span>-c<span class="w"> </span><span class="s1">&#39;cd /opt/stacks/coder &amp;&amp; docker compose up -d --force-recreate coder&#39;</span>
<a id="__codelineno-11-12" name="__codelineno-11-12" href="#__codelineno-11-12"></a>
<a id="__codelineno-11-13" name="__codelineno-11-13" href="#__codelineno-11-13"></a><span class="c1"># 3. Reset password (interactive)</span>
<a id="__codelineno-11-14" name="__codelineno-11-14" href="#__codelineno-11-14"></a>pct<span class="w"> </span><span class="nb">exec</span><span class="w"> </span><span class="m">111</span><span class="w"> </span>--<span class="w"> </span>docker<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>-it<span class="w"> </span>coder<span class="w"> </span>coder<span class="w"> </span>reset-password<span class="w"> </span>fkrebs
</code></pre></div></p>
<p><strong>Pocket-ID — bootstrap a one-time access token if you're locked out of <em>Pocket-ID itself</em>:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-12-1" name="__codelineno-12-1" href="#__codelineno-12-1"></a>pct<span class="w"> </span><span class="nb">exec</span><span class="w"> </span><span class="m">110</span><span class="w"> </span>--<span class="w"> </span>docker<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>pocket-id<span class="w"> </span>pocket-id-cli<span class="w"> </span>one-time-access-token<span class="w"> </span><span class="se">\</span>
<a id="__codelineno-12-2" name="__codelineno-12-2" href="#__codelineno-12-2"></a><span class="w"> </span>--email<span class="w"> </span>fkrebs@nucli.de<span class="w"> </span>--duration<span class="w"> </span>1h
<a id="__codelineno-12-3" name="__codelineno-12-3" href="#__codelineno-12-3"></a><span class="c1"># Open the printed URL in a browser to log in once and register a new passkey.</span>
</code></pre></div></p>
<p>After SSO is fixed: undo each of the above steps (re-disable password auth, flip <code>login_type</code> back to <code>oidc</code>).</p>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "../..", "features": ["navigation.tabs", "navigation.sections", "navigation.expand", "navigation.top", "search.highlight", "search.suggest", "content.code.copy"], "search": "../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../assets/javascripts/bundle.79ae519e.min.js"></script>
</body>
</html>