init: Coder compose + templates + act-runner
This commit is contained in:
@@ -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:
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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/<user>/<workspace>/` 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`.
|
||||
@@ -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 <<CFG
|
||||
bind-addr: 127.0.0.1:13337
|
||||
auth: none
|
||||
cert: false
|
||||
CFG
|
||||
code-server --disable-telemetry >/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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user