From 16708d3a396537e8a6e85f88bad03e0f1d8df63d Mon Sep 17 00:00:00 2001 From: fkrebs Date: Sat, 23 May 2026 09:46:51 +0200 Subject: [PATCH] init: Coder compose + templates + act-runner --- .gitignore | 5 + README.md | 11 +- act-runner/compose.yaml | 17 +++ coder/compose.yaml | 27 ++++ coder/templates/mcp-sandbox/Dockerfile | 39 ++++++ coder/templates/mcp-sandbox/README.md | 31 +++++ coder/templates/mcp-sandbox/main.tf | 72 ++++++++++ coder/templates/python-uv/README.md | 34 +++++ coder/templates/python-uv/main.tf | 174 +++++++++++++++++++++++++ 9 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 act-runner/compose.yaml create mode 100644 coder/compose.yaml create mode 100644 coder/templates/mcp-sandbox/Dockerfile create mode 100644 coder/templates/mcp-sandbox/README.md create mode 100644 coder/templates/mcp-sandbox/main.tf create mode 100644 coder/templates/python-uv/README.md create mode 100644 coder/templates/python-uv/main.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abd7b61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +.env.* +!.env.example +coder/.env +act-runner/.env diff --git a/README.md b/README.md index 997e97b..5d2241a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ # ct111-dev -Coder templates + act-runner configs (CT 111) \ No newline at end of file +Coder + Gitea act-runner configs for CT 111 (192.168.1.42). + +Auto-synced daily at 03:15. + +| Service | Stack | Port | +|---|---|---| +| Coder | /opt/stacks/coder/ | https://dev.nuclide.systems | +| act-runner | /opt/stacks/act-runner/ | (no port, polls Gitea) | + +Gitea app.ini is at `/mnt/pve/unas/services/gitea/gitea/conf/app.ini` (UNAS NFS mount). diff --git a/act-runner/compose.yaml b/act-runner/compose.yaml new file mode 100644 index 0000000..9cb919e --- /dev/null +++ b/act-runner/compose.yaml @@ -0,0 +1,17 @@ +name: act-runner + +services: + act-runner: + image: gitea/act_runner:latest + container_name: act-runner + restart: unless-stopped + environment: + GITEA_INSTANCE_URL: "https://git.nuclide.systems" + GITEA_RUNNER_REGISTRATION_TOKEN: "${ACT_TOKEN}" + GITEA_RUNNER_NAME: "ct111-runner" + GITEA_RUNNER_LABELS: "docker,ubuntu-latest:docker://node:20-bookworm,debian:docker://debian:bookworm" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./data:/data + group_add: + - "991" # docker socket gid in CT 111 diff --git a/coder/compose.yaml b/coder/compose.yaml new file mode 100644 index 0000000..faf051b --- /dev/null +++ b/coder/compose.yaml @@ -0,0 +1,27 @@ +services: + coder: + image: ghcr.io/coder/coder:latest + container_name: coder + restart: unless-stopped + ports: + - "7080:7080" + environment: + CODER_HTTP_ADDRESS: "0.0.0.0:7080" + CODER_ACCESS_URL: "https://dev.nuclide.systems" + CODER_WILDCARD_ACCESS_URL: "*.dev.nuclide.systems" + CODER_PG_CONNECTION_URL: "postgresql://coder:${CODER_DB_PASSWORD}@192.168.1.6:5432/coder?sslmode=disable" + # OIDC via Pocket-ID + 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" + # Disable the bundled GitHub login provider (separate from OIDC) + CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE: "false" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /mnt/pve/unas/services/coder:/home/coder + group_add: + - "991" # docker socket gid inside CT 111 +volumes: diff --git a/coder/templates/mcp-sandbox/Dockerfile b/coder/templates/mcp-sandbox/Dockerfile new file mode 100644 index 0000000..25ea012 --- /dev/null +++ b/coder/templates/mcp-sandbox/Dockerfile @@ -0,0 +1,39 @@ +FROM ghcr.io/astral-sh/uv:bookworm-slim + +ARG PY_VERSION=3.13 + +# minimal OS deps: external network tools + ca-certs + a shell-friendly setup +RUN apt-get update -qq \ + && apt-get install -y --no-install-recommends \ + curl wget ca-certificates sudo git openssh-client \ + bash less procps file \ + && rm -rf /var/lib/apt/lists/* + +# install pinned-but-current Python via uv (rebuild image to bump) +RUN uv python install --default ${PY_VERSION} + +# create a managed venv and put it on PATH so `python`/`pip` are the sandbox's +ENV VIRTUAL_ENV=/opt/venv \ + PATH=/opt/venv/bin:/root/.local/bin:${PATH} +RUN uv venv --python ${PY_VERSION} /opt/venv + +# sci/plotting stack — installed once into /opt/venv +RUN uv pip install --no-cache \ + numpy \ + pandas \ + matplotlib \ + plotly \ + seaborn \ + scipy \ + scikit-learn \ + requests \ + httpx \ + ipython \ + && python -c "import numpy, pandas, matplotlib, scipy, sklearn, plotly, seaborn; print('sci stack OK')" + +# non-root sandbox user +RUN useradd -m -s /bin/bash -u 1000 coder \ + && echo "coder ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder + +USER coder +WORKDIR /home/coder diff --git a/coder/templates/mcp-sandbox/README.md b/coder/templates/mcp-sandbox/README.md new file mode 100644 index 0000000..ca14412 --- /dev/null +++ b/coder/templates/mcp-sandbox/README.md @@ -0,0 +1,31 @@ +# mcp-sandbox + +Ephemeral, minimalist Python sandbox for MCP-driven code execution (plotting, quick analysis, exfil-style external fetches with `curl`/`wget`/`httpx`). + +## What's baked in +- Debian slim + uv + Python 3.13 (rebuild image to bump) +- Sci stack: numpy, pandas, matplotlib, plotly, seaborn, scipy, scikit-learn +- HTTP: requests, httpx, curl, wget +- Tools: git, ssh, ipython, sudo +- Non-root user `coder` (uid 1000) with passwordless sudo + +## Why "baked in" vs install-at-startup +MCP spawns short-lived workspaces. Installing deps every time burns ~2 min and ~300 MB of network. Bake them, spin in ~3 s. + +## Build the image (run on CT 111) +```bash +cd /opt/stacks/coder/templates/mcp-sandbox +docker build -t coder-mcp-sandbox:latest . +# bump Python: docker build --build-arg PY_VERSION=3.14 -t coder-mcp-sandbox:latest . +``` + +## Push the template to Coder +```bash +coder templates push mcp-sandbox -d /opt/stacks/coder/templates/mcp-sandbox +``` + +## Ephemerality +`$HOME` is **not** persisted. Each workspace start gets fresh state. Plots written to `~/plots/` vanish on stop — copy via `coder cp` or upload elsewhere from inside the workspace if you need to keep them. + +## GPU +`/dev/dri/renderD128` is mounted. Useful if matplotlib offloads to GPU or if you decide to add `torch[cpu]` → `torch` with Arc support later. diff --git a/coder/templates/mcp-sandbox/main.tf b/coder/templates/mcp-sandbox/main.tf new file mode 100644 index 0000000..280c2d1 --- /dev/null +++ b/coder/templates/mcp-sandbox/main.tf @@ -0,0 +1,72 @@ +terraform { + required_providers { + coder = { source = "coder/coder" } + docker = { source = "kreuzwerker/docker" } + } +} + +provider "docker" {} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + username = data.coder_workspace_owner.me.name +} + +resource "coder_agent" "main" { + arch = "amd64" + os = "linux" + startup_script = <<-EOT + set -e + mkdir -p ~/sandbox ~/plots + echo "ready: $(python --version) | sci stack pre-baked" + EOT + + metadata { + display_name = "CPU" + key = "cpu" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + metadata { + display_name = "RAM" + key = "ram" + script = "coder stat mem" + interval = 10 + timeout = 1 + } +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = "coder-mcp-sandbox:latest" # built locally; do not pull + name = "coder-${local.username}-${lower(data.coder_workspace.me.name)}-sbx" + hostname = data.coder_workspace.me.name + + entrypoint = ["sh", "-c", coder_agent.main.init_script] + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + "OPENAI_API_KEY=sk-tapirnase", + "OPENAI_BASE_URL=http://192.168.1.40:14000/v1", + "OPENAI_MODEL=qwen3-coder-30b-a3b-instruct", + "AIDER_MODEL=qwen3-coder-30b-a3b-instruct", + "AIDER_WEAK_MODEL=mistral-small-latest", + "EMBEDDING_MODEL=text-embedding-3-large", + "MPLBACKEND=Agg", + ] + + # ephemeral home — fresh state every spin + # (drop the `volumes` block entirely to keep tmpfs-like behavior) + + devices { host_path = "/dev/dri/renderD128" } + + memory = 4096 + cpu_shares = 512 + + host { + host = "host.docker.internal" + ip = "host-gateway" + } +} diff --git a/coder/templates/python-uv/README.md b/coder/templates/python-uv/README.md new file mode 100644 index 0000000..5eca97c --- /dev/null +++ b/coder/templates/python-uv/README.md @@ -0,0 +1,34 @@ +# python-uv (minimal uv-based dev workspace) + +One template, intended for a **single workspace** per user (work + home projects coexist in `~/work/` and `~/home/`; `uv` gives each project its own venv). + +## What's in the workspace +- Base: `ghcr.io/astral-sh/uv:bookworm-slim` +- Python: latest stable via `uv python install` (tracks Astral's release channel) +- code-server (browser VS Code) +- git, curl, nano, openssh-client +- Intel Arc renderD128 mounted at `/dev/dri/renderD128` + +## Persistent storage +`$HOME` is bind-mounted from `/mnt/pve/unas/services/coder///` on UNAS. +Workspace stop/rebuild is safe — files persist. + +## Per-project pattern +```bash +cd ~/work/myproj +uv init # if new +uv add fastapi uvicorn # adds to pyproject + locks +uv run python main.py +``` + +## Push template to Coder +From a host with the Coder CLI configured: + +```bash +coder templates push python-uv -d /opt/stacks/coder/templates/python-uv +``` + +Then in the Coder UI: **Workspaces → Create Workspace → python-uv → name it `dev`**. + +## Optional: dotfiles +On workspace creation you can pass a Git URL (e.g. `https://git.nuclide.systems/fkrebs/dotfiles.git`). Coder clones it and runs `install.sh`. diff --git a/coder/templates/python-uv/main.tf b/coder/templates/python-uv/main.tf new file mode 100644 index 0000000..a034e2c --- /dev/null +++ b/coder/templates/python-uv/main.tf @@ -0,0 +1,174 @@ +terraform { + required_providers { + coder = { source = "coder/coder" } + docker = { source = "kreuzwerker/docker" } + } +} + +provider "docker" {} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + username = data.coder_workspace_owner.me.name + home_dir = "/home/${local.username}" + home_host_path = "/mnt/pve/unas/services/coder/${local.username}/${data.coder_workspace.me.name}" +} + +resource "coder_agent" "main" { + arch = "amd64" + os = "linux" + startup_script = <<-EOT + set -e + # --- minimal deps (uv image is slim) --- + if ! command -v git >/dev/null; then + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends git curl ca-certificates sudo openssh-client nano + fi + + # --- latest stable Python via uv (tracks current release channel) --- + uv python install --default + uv python pin "$(uv python find | xargs -n1 basename | head -1)" 2>/dev/null || true + echo "Python: $(uv run python --version)" + + # --- code-server (browser VS Code) --- + if ! command -v code-server >/dev/null; then + curl -fsSL https://code-server.dev/install.sh | sh + fi + mkdir -p ~/.config/code-server + cat > ~/.config/code-server/config.yaml </tmp/code-server.log 2>&1 & + + # --- opencode CLI (uses LiteLLM + MCP gateway) --- + if ! command -v opencode >/dev/null 2>&1; then + curl -fsSL https://opencode.ai/install | sh >/dev/null 2>&1 || true + fi + + # --- Claude Code CLI (routed via LiteLLM ANTHROPIC_BASE_URL) --- + if ! command -v claude >/dev/null 2>&1; then + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - >/dev/null + sudo apt-get install -y --no-install-recommends nodejs >/dev/null + sudo npm install -g @anthropic-ai/claude-code >/dev/null + fi + + # --- workspace scaffold (idempotent) --- + mkdir -p ~/work ~/home + [ -f ~/.bashrc ] || echo 'export PATH=$HOME/.local/bin:$PATH' >> ~/.bashrc + + # --- dotfiles (optional; user sets via UI param) --- + if [ -n "${data.coder_parameter.dotfiles_uri.value}" ]; then + coder dotfiles -y "${data.coder_parameter.dotfiles_uri.value}" || true + fi + EOT + + metadata { + display_name = "CPU" + key = "0_cpu" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + metadata { + display_name = "RAM" + key = "1_ram" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + metadata { + display_name = "Disk" + key = "2_disk" + script = "coder stat disk" + interval = 60 + timeout = 1 + } + metadata { + display_name = "Python" + key = "3_python" + script = "uv run python --version 2>/dev/null || echo n/a" + interval = 60 + timeout = 5 + } +} + +data "coder_parameter" "dotfiles_uri" { + name = "dotfiles_uri" + display_name = "Dotfiles repo (optional)" + description = "Git URL of a dotfiles repo. Coder will clone it and run install.sh on workspace start." + type = "string" + default = "https://git.nuclide.systems/fkrebs/dotfiles.git" + mutable = true +} + +resource "coder_app" "code-server" { + agent_id = coder_agent.main.id + slug = "code-server" + display_name = "VS Code" + url = "http://localhost:13337" + icon = "/icon/code.svg" + subdomain = false + share = "owner" + healthcheck { + url = "http://localhost:13337/healthz" + interval = 5 + threshold = 6 + } +} + +resource "docker_image" "workspace" { + name = "ghcr.io/astral-sh/uv:bookworm-slim" +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = docker_image.workspace.name + name = "coder-${local.username}-${lower(data.coder_workspace.me.name)}" + hostname = data.coder_workspace.me.name + + # The agent's init script bootstraps everything. + entrypoint = ["sh", "-c", <<-EOT + apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates sudo procps >/dev/null 2>&1 + ${coder_agent.main.init_script} + EOT + ] + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + "OPENAI_API_KEY=sk-tapirnase", + "OPENAI_BASE_URL=http://192.168.1.40:14000/v1", + "OPENAI_MODEL=qwen3-coder-30b-a3b-instruct", + "AIDER_MODEL=qwen3-coder-30b-a3b-instruct", + "AIDER_WEAK_MODEL=mistral-small-latest", + "EMBEDDING_MODEL=text-embedding-3-large", + "ANTHROPIC_BASE_URL=http://192.168.1.40:14000", + "ANTHROPIC_API_KEY=sk-tapirnase", + "CHAT_ARTIFACTS_ENDPOINT=https://s3.nuclide.systems", + "CHAT_ARTIFACTS_ACCESS_KEY=GK50bfcfcb7e05fb8421193ab0", + "CHAT_ARTIFACTS_SECRET_KEY=fa139e0422c0140331a69be4053eb7fb0d04bd64cd33790714099481ba19e0bf", + "CHAT_ARTIFACTS_BUCKET=chat-artifacts", + "MCP_GATEWAY_TOKEN=JxxCqKw32XoN4LOHunDikS6u1RpS7R5ythzaqADPuIA", + ] + + # Persistent home backed by UNAS. + volumes { + container_path = local.home_dir + host_path = local.home_host_path + read_only = false + } + + # Intel Arc GPU. + devices { host_path = "/dev/dri/renderD128" } + + # Sane resource caps; tune later. + memory = 8192 + cpu_shares = 1024 + + host { + host = "host.docker.internal" + ip = "host-gateway" + } +}