diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b7c61de..0000000 --- a/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM codercom/enterprise-base:ubuntu - -ARG GO_VERSION=1.22.5 -ARG GO_ARCH=amd64 - -USER root - -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get upgrade -y && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - curl wget git sudo zip unzip jq build-essential ca-certificates && \ - rm -rf /var/lib/apt/lists/* - -RUN curl -fsSL https://get.docker.com | sh - -RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tar.gz && \ - tar -C /usr/local -xzf /tmp/go.tar.gz && \ - rm /tmp/go.tar.gz - -USER coder - -RUN curl -fsSL https://bun.sh/install | bash - -RUN curl -s "https://get.sdkman.io" | bash && \ - bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && sdk install maven" - -RUN curl -fsSL https://opencode.ai/install | bash - -ENV PATH="/usr/local/go/bin:/home/coder/go/bin:/home/coder/.opencode/bin:/home/coder/.bun/bin:${PATH}" diff --git a/README.md b/README.md index 6435e87..acc7377 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,63 @@ ---- -display_name: Docker Workspaces with GPU & DooD -description: Provision Docker-based Coder workspaces with selectable GPU support, Docker-out-of-Docker, Go, Bun, and Opencode. -icon: /icon/docker.svg -maintainer_github: sirblob -verified: false -tags: [docker, container, gpu, golang, bun, sdkman, opencode, ollama] ---- +# Coder Templates -# Feature-Rich Docker Workspaces +Docker-based [Coder](https://coder.com/docs) workspace templates for the homelab, organized by development stack. Every template shares the same infrastructure and differs only in the toolchain baked into its image. -Provision powerful Docker containers as [Coder workspaces](https://coder.com/docs/workspaces) with this template. It features selectable GPU passthrough, Docker-out-of-Docker (DooD), and pre-installs essential development tools via a custom Dockerfile. +## Templates + +| Template | Stack | +| --- | --- | +| [dev-web](dev-web) | Node.js, Bun, Python, Claude Code | +| [dev-backend](dev-backend) | Go, Node.js, Bun, Opencode, Claude Code | +| [dev-rust](dev-rust) | Rust, Python, Claude Code | +| [dev-c](dev-c) | C, C++, Claude Code | +| [dev-java](dev-java) | Java, Maven, SDKMAN!, Claude Code | +| [dev-robots](dev-robots) | ROS 2, Python, C++, Claude Code | + +## Shared features + +- **Selectable GPU support**: a `GPU Assignment` parameter attaches GPU 0, GPU 1, or both. GPU 0 is the default because GPU 1 is shared with the Ollama and Immich stacks. +- **Docker-out-of-Docker**: the host's `/var/run/docker.sock` is mounted, so workspaces can build and run containers. +- **Ollama integration**: `OLLAMA_HOST` points at `http://host.docker.internal:11434`. +- **Persistent home**: `/home/coder` is mapped to a host directory and survives restarts. +- **Telemetry**: CPU, RAM, disk, load average, swap, and GPU utilization in the Coder UI. +- **IDE support**: code-server and JetBrains Toolbox. +- **Gitea CLI**: `tea` is preinstalled for working with `git.sirblob.co` from the workspace. +- **Patched on start**: the agent runs `apt-get update` and `apt-get upgrade` each time a workspace starts. + +## Layout + +Each template directory is self-contained and can be pushed to Coder on its own: + +``` +dev-/ + main.tf Providers, data sources, and shared locals + parameters.tf Workspace parameters + agent.tf Coder agent, startup script, and metadata + apps.tf code-server and JetBrains modules + container.tf Docker image build and container resource + Dockerfile Workspace image + README.md Template documentation +``` + +## Pushing a template + +```sh +coder templates push dev-web -d ./dev-web +``` ## Prerequisites -### Infrastructure +The host needs the NVIDIA container toolkit for GPU passthrough. Coder itself runs as a container and creates workspaces through the host's Docker socket, so its service needs the socket mounted and the host `docker` group added: -The host you run Coder on must have a running Docker socket, the NVIDIA container runtime installed, and the `coder` user added to the Docker group: - -```sh -sudo adduser coder docker -sudo systemctl restart coder -sudo -u coder docker ps +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +group_add: + - "987" ``` -## Features & Architecture +Workspaces get the same GID through the `docker_group_id` template variable, which defaults to `987`. Confirm the host GID with `getent group docker` and override the variable if it differs. -This template provisions the following resources and features: +## Adding tools to a template -- **Base Image**: Custom image built from `codercom/enterprise-base:ubuntu`. -- **Selectable GPU Support**: A `GPU Assignment` parameter chooses which GPU(s) to attach. The default is **GPU 0**, because GPU 1 is shared with the `ollama` and `immich` stacks; pick `GPU 1 only` or `Both GPUs` when you need them. -- **Docker-out-of-Docker (DooD)**: Mounts the host's `/var/run/docker.sock` so you can build and run containers from inside your workspace. -- **Ollama Integration**: `OLLAMA_HOST` is preset to `http://host.docker.internal:11434`, so tools in the workspace talk to the homelab Ollama service out of the box. -- **Pre-baked Tools**: The custom Docker image comes pre-installed with: - - System package updates - - Docker CLI - - Bun - - Go (pinned version, installed from the official tarball) - - SDKMAN! & Maven - - Opencode - - `jq`, `build-essential` -- **Persistent Storage**: Uses a persistent host directory mapped to `/home/coder` so files survive workspace restarts. -- **Rich Telemetry**: Displays CPU, RAM, Disk, Load Average, and GPU utilization directly in the Coder UI. -- **IDE Support**: Native integration with code-server (Browser VS Code) and JetBrains IDEs. - -## Using the homelab services - -### Ollama - -`OLLAMA_HOST` is already exported in the workspace. Verify with: - -```sh -curl "$OLLAMA_HOST/api/tags" -``` - -### Gitea container registry - -With DooD enabled you can build and push images straight to the Gitea registry from inside a workspace: - -```sh -docker login git.sirblob.co -u -p -docker build -t git.sirblob.co//: . -docker push git.sirblob.co//: -``` - -See `projects.md` in the homelab repo for the full registry workflow. - -> **Note** -> While your `/home/coder` directory is persistent, the container's root filesystem is ephemeral. This template builds a custom Docker image to pre-bake tools (Go, Bun, SDKMAN!, Opencode, etc.), ensuring your workspace starts instantly while keeping the tools available. +The container's root filesystem is ephemeral and `/home/coder` is a mounted volume, so anything installed into the home directory at build time is shadowed at runtime. Install tools under `/opt` or `/usr/local` and add them to `PATH` at the end of the Dockerfile. diff --git a/dev-backend/Dockerfile b/dev-backend/Dockerfile new file mode 100644 index 0000000..480ed17 --- /dev/null +++ b/dev-backend/Dockerfile @@ -0,0 +1,53 @@ +FROM codercom/enterprise-base:ubuntu + +ARG DEBIAN_FRONTEND=noninteractive +ARG TEA_VERSION= +ARG TEA_ARCH=amd64 +ARG NODE_VERSION=22 +ARG GO_ARCH=amd64 + +USER root + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y \ + build-essential ca-certificates curl git jq sudo unzip wget zip && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://get.docker.com | sh + +RUN tea_version="${TEA_VERSION}" && \ + if [ -z "$tea_version" ]; then \ + tea_version="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | jq -r .tag_name)"; \ + fi && \ + tea_version="${tea_version#v}" && \ + curl -fsSL "https://dl.gitea.com/tea/${tea_version}/tea-${tea_version}-linux-${TEA_ARCH}" \ + -o /usr/local/bin/tea && \ + chmod 755 /usr/local/bin/tea + +RUN GO_RELEASE="$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n 1)" && \ + curl -fsSL "https://go.dev/dl/${GO_RELEASE}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tar.gz && \ + tar -C /usr/local -xzf /tmp/go.tar.gz && \ + rm /tmp/go.tar.gz + +RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* + +ENV BUN_INSTALL=/opt/bun +RUN curl -fsSL https://bun.sh/install | bash && \ + chown -R coder:coder ${BUN_INSTALL} + +ENV OPENCODE_INSTALL_DIR=/opt/opencode +RUN curl -fsSL https://opencode.ai/install | bash && \ + chown -R coder:coder ${OPENCODE_INSTALL_DIR} + +ENV CLAUDE_HOME=/opt/claude +RUN mkdir -p ${CLAUDE_HOME} && \ + env HOME=${CLAUDE_HOME} bash -c 'curl -fsSL https://claude.ai/install.sh | bash' && \ + ln -s ${CLAUDE_HOME}/.local/bin/claude /usr/local/bin/claude && \ + chown -R coder:coder ${CLAUDE_HOME} + +USER coder + +ENV PATH="/usr/local/go/bin:/home/coder/go/bin:/opt/bun/bin:/opt/opencode/bin:${PATH}" diff --git a/dev-backend/README.md b/dev-backend/README.md new file mode 100644 index 0000000..9310278 --- /dev/null +++ b/dev-backend/README.md @@ -0,0 +1,88 @@ +--- +display_name: Backend Development Workspace +description: Docker workspace for backend development with Go, Node.js, Bun, Opencode, and Claude Code. +icon: /icon/go.svg +maintainer_github: sirblob +verified: false +tags: [docker, container, gpu, golang, nodejs, bun, opencode, claude, ollama] +--- + +# Backend Development Workspace + +Docker workspace for backend development with Go, Node.js, Bun, Opencode, and Claude Code. + +Provisions a Docker container as a [Coder workspace](https://coder.com/docs/workspaces) with selectable GPU passthrough, Docker-out-of-Docker, and a persistent home directory. + +## Preinstalled tools + +- Go, latest stable release +- Node.js 22 with npm and npx +- Bun +- Opencode +- Claude Code +- Docker CLI for Docker-out-of-Docker +- Gitea CLI (`tea`) +- code-server and JetBrains Toolbox support + +## Parameters + +| Parameter | Description | Default | +| --- | --- | --- | +| `GPU Assignment` | Which GPU(s) to attach: GPU 0, GPU 1, or both. GPU 1 is shared with the Ollama and Immich stacks. | GPU 0 | + +## Prerequisites + +The host needs the NVIDIA container toolkit for GPU passthrough. Coder itself runs as a container and creates workspaces through the host's Docker socket, so its service needs the socket mounted and the host `docker` group added: + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +group_add: + - "987" +``` + +Workspaces get the same GID through the `docker_group_id` template variable, which defaults to `987`. Confirm the host GID with `getent group docker` and override the variable if it differs. + +## Ollama + +`OLLAMA_HOST` is preset to `http://host.docker.internal:11434`. Verify with: + +```sh +curl "$OLLAMA_HOST/api/tags" +``` + +## Gitea + +The `tea` CLI manages repositories, issues, and pull requests from the workspace: + +```sh +tea login add --name sirblob --url https://git.sirblob.co --token +tea repos list +``` + +Docker-out-of-Docker lets you build and push images to the registry: + +```sh +docker login git.sirblob.co -u -p +docker build -t git.sirblob.co//: . +docker push git.sirblob.co//: +``` + +## Updates + +The image is built with the base packages already upgraded, and the agent runs `apt-get update` and `apt-get upgrade` on every workspace start so a long-lived container keeps picking up security updates. + +## Persistence + +`/home/coder` is mapped to a host directory and survives workspace restarts. The rest of the root filesystem is ephemeral, so every preinstalled tool lives under `/opt` or `/usr/local` instead of the home directory. + +## Files + +| File | Contents | +| --- | --- | +| `main.tf` | Providers, data sources, and shared locals | +| `parameters.tf` | Workspace parameters | +| `agent.tf` | Coder agent, startup script, and metadata | +| `apps.tf` | code-server and JetBrains modules | +| `container.tf` | Docker image build and container resource | +| `Dockerfile` | Workspace image | diff --git a/dev-backend/agent.tf b/dev-backend/agent.tf new file mode 100644 index 0000000..18b2d30 --- /dev/null +++ b/dev-backend/agent.tf @@ -0,0 +1,92 @@ +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + + startup_script = <<-EOT + set -e + + sudo DEBIAN_FRONTEND=noninteractive apt-get update || true + sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y || true + + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ + touch ~/.init_done + fi + EOT + + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email + OLLAMA_HOST = local.ollama_host + } + + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "2_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "GPU Usage" + key = "3_gpu_usage" + script = "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | awk '{sum+=$1} END {print sum/NR \"%\"}'" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + script = </dev/null 2>&1 || sudo groupadd --gid "$group_id" "host-$group_id" || true + done + ${replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")} + EOT + ] + + gpus = data.coder_parameter.gpu.value == "all" ? "all" : "device=${data.coder_parameter.gpu.value}" + group_add = [var.docker_group_id] + + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + "NVIDIA_VISIBLE_DEVICES=${data.coder_parameter.gpu.value}", + "NVIDIA_DRIVER_CAPABILITIES=all", + "OLLAMA_HOST=${local.ollama_host}" + ] + + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + volumes { + container_path = "/home/coder" + host_path = local.workspace_home + read_only = false + } + + volumes { + container_path = "/var/run/docker.sock" + host_path = "/var/run/docker.sock" + read_only = false + } + + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} diff --git a/dev-backend/main.tf b/dev-backend/main.tf new file mode 100644 index 0000000..60a8b09 --- /dev/null +++ b/dev-backend/main.tf @@ -0,0 +1,35 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + docker = { + source = "kreuzwerker/docker" + } + } +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +variable "docker_group_id" { + default = "987" + description = "Host GID of the docker group, granted to workspaces for Docker-out-of-Docker" + type = string +} + +provider "docker" { + host = var.docker_socket != "" ? var.docker_socket : null +} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + workspace_home = "/media/blob/ChaiNet/coder/workspaces/coder-${data.coder_workspace.me.id}-home" + ollama_host = "http://host.docker.internal:11434" +} diff --git a/dev-backend/parameters.tf b/dev-backend/parameters.tf new file mode 100644 index 0000000..3b05daa --- /dev/null +++ b/dev-backend/parameters.tf @@ -0,0 +1,21 @@ +data "coder_parameter" "gpu" { + name = "gpu" + display_name = "GPU Assignment" + description = "Which GPU(s) to attach. GPU 1 is shared with Ollama and Immich, so GPU 0 is the default to avoid contention." + default = "0" + mutable = true + icon = "/icon/memory.svg" + + option { + name = "GPU 0 - RTX 3060" + value = "0" + } + option { + name = "GPU 1 - GTX 1660 (shared with Ollama and Immich)" + value = "1" + } + option { + name = "Both GPUs" + value = "all" + } +} diff --git a/dev-c/Dockerfile b/dev-c/Dockerfile new file mode 100644 index 0000000..1ca1efa --- /dev/null +++ b/dev-c/Dockerfile @@ -0,0 +1,38 @@ +FROM codercom/enterprise-base:ubuntu + +ARG DEBIAN_FRONTEND=noninteractive +ARG TEA_VERSION= +ARG TEA_ARCH=amd64 + +USER root + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y \ + build-essential ca-certificates curl git jq pkg-config sudo unzip wget zip && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://get.docker.com | sh + +RUN tea_version="${TEA_VERSION}" && \ + if [ -z "$tea_version" ]; then \ + tea_version="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | jq -r .tag_name)"; \ + fi && \ + tea_version="${tea_version#v}" && \ + curl -fsSL "https://dl.gitea.com/tea/${tea_version}/tea-${tea_version}-linux-${TEA_ARCH}" \ + -o /usr/local/bin/tea && \ + chmod 755 /usr/local/bin/tea + +RUN apt-get update && \ + apt-get install -y \ + autoconf automake ccache clang clang-format clang-tidy clangd cmake \ + gdb libtool lldb ninja-build valgrind && \ + rm -rf /var/lib/apt/lists/* + +ENV CLAUDE_HOME=/opt/claude +RUN mkdir -p ${CLAUDE_HOME} && \ + env HOME=${CLAUDE_HOME} bash -c 'curl -fsSL https://claude.ai/install.sh | bash' && \ + ln -s ${CLAUDE_HOME}/.local/bin/claude /usr/local/bin/claude && \ + chown -R coder:coder ${CLAUDE_HOME} + +USER coder diff --git a/dev-c/README.md b/dev-c/README.md new file mode 100644 index 0000000..afa51e7 --- /dev/null +++ b/dev-c/README.md @@ -0,0 +1,88 @@ +--- +display_name: C and C++ Development Workspace +description: Docker workspace for C and C++ development with GCC, Clang, CMake, and Claude Code. +icon: /icon/cpp.svg +maintainer_github: sirblob +verified: false +tags: [docker, container, gpu, c, cpp, cmake, claude, ollama] +--- + +# C and C++ Development Workspace + +Docker workspace for C and C++ development with GCC, Clang, CMake, and Claude Code. + +Provisions a Docker container as a [Coder workspace](https://coder.com/docs/workspaces) with selectable GPU passthrough, Docker-out-of-Docker, and a persistent home directory. + +## Preinstalled tools + +- GCC and G++ via build-essential +- Clang, clangd, clang-format, and clang-tidy +- CMake, Ninja, Autotools, and ccache +- GDB, LLDB, and Valgrind +- Claude Code +- Docker CLI for Docker-out-of-Docker +- Gitea CLI (`tea`) +- code-server and JetBrains Toolbox support + +## Parameters + +| Parameter | Description | Default | +| --- | --- | --- | +| `GPU Assignment` | Which GPU(s) to attach: GPU 0, GPU 1, or both. GPU 1 is shared with the Ollama and Immich stacks. | GPU 0 | + +## Prerequisites + +The host needs the NVIDIA container toolkit for GPU passthrough. Coder itself runs as a container and creates workspaces through the host's Docker socket, so its service needs the socket mounted and the host `docker` group added: + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +group_add: + - "987" +``` + +Workspaces get the same GID through the `docker_group_id` template variable, which defaults to `987`. Confirm the host GID with `getent group docker` and override the variable if it differs. + +## Ollama + +`OLLAMA_HOST` is preset to `http://host.docker.internal:11434`. Verify with: + +```sh +curl "$OLLAMA_HOST/api/tags" +``` + +## Gitea + +The `tea` CLI manages repositories, issues, and pull requests from the workspace: + +```sh +tea login add --name sirblob --url https://git.sirblob.co --token +tea repos list +``` + +Docker-out-of-Docker lets you build and push images to the registry: + +```sh +docker login git.sirblob.co -u -p +docker build -t git.sirblob.co//: . +docker push git.sirblob.co//: +``` + +## Updates + +The image is built with the base packages already upgraded, and the agent runs `apt-get update` and `apt-get upgrade` on every workspace start so a long-lived container keeps picking up security updates. + +## Persistence + +`/home/coder` is mapped to a host directory and survives workspace restarts. The rest of the root filesystem is ephemeral, so every preinstalled tool lives under `/opt` or `/usr/local` instead of the home directory. + +## Files + +| File | Contents | +| --- | --- | +| `main.tf` | Providers, data sources, and shared locals | +| `parameters.tf` | Workspace parameters | +| `agent.tf` | Coder agent, startup script, and metadata | +| `apps.tf` | code-server and JetBrains modules | +| `container.tf` | Docker image build and container resource | +| `Dockerfile` | Workspace image | diff --git a/dev-c/agent.tf b/dev-c/agent.tf new file mode 100644 index 0000000..18b2d30 --- /dev/null +++ b/dev-c/agent.tf @@ -0,0 +1,92 @@ +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + + startup_script = <<-EOT + set -e + + sudo DEBIAN_FRONTEND=noninteractive apt-get update || true + sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y || true + + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ + touch ~/.init_done + fi + EOT + + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email + OLLAMA_HOST = local.ollama_host + } + + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "2_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "GPU Usage" + key = "3_gpu_usage" + script = "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | awk '{sum+=$1} END {print sum/NR \"%\"}'" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + script = </dev/null 2>&1 || sudo groupadd --gid "$group_id" "host-$group_id" || true + done + ${replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")} + EOT + ] + + gpus = data.coder_parameter.gpu.value == "all" ? "all" : "device=${data.coder_parameter.gpu.value}" + group_add = [var.docker_group_id] + + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + "NVIDIA_VISIBLE_DEVICES=${data.coder_parameter.gpu.value}", + "NVIDIA_DRIVER_CAPABILITIES=all", + "OLLAMA_HOST=${local.ollama_host}" + ] + + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + volumes { + container_path = "/home/coder" + host_path = local.workspace_home + read_only = false + } + + volumes { + container_path = "/var/run/docker.sock" + host_path = "/var/run/docker.sock" + read_only = false + } + + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} diff --git a/dev-c/main.tf b/dev-c/main.tf new file mode 100644 index 0000000..60a8b09 --- /dev/null +++ b/dev-c/main.tf @@ -0,0 +1,35 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + docker = { + source = "kreuzwerker/docker" + } + } +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +variable "docker_group_id" { + default = "987" + description = "Host GID of the docker group, granted to workspaces for Docker-out-of-Docker" + type = string +} + +provider "docker" { + host = var.docker_socket != "" ? var.docker_socket : null +} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + workspace_home = "/media/blob/ChaiNet/coder/workspaces/coder-${data.coder_workspace.me.id}-home" + ollama_host = "http://host.docker.internal:11434" +} diff --git a/dev-c/parameters.tf b/dev-c/parameters.tf new file mode 100644 index 0000000..3b05daa --- /dev/null +++ b/dev-c/parameters.tf @@ -0,0 +1,21 @@ +data "coder_parameter" "gpu" { + name = "gpu" + display_name = "GPU Assignment" + description = "Which GPU(s) to attach. GPU 1 is shared with Ollama and Immich, so GPU 0 is the default to avoid contention." + default = "0" + mutable = true + icon = "/icon/memory.svg" + + option { + name = "GPU 0 - RTX 3060" + value = "0" + } + option { + name = "GPU 1 - GTX 1660 (shared with Ollama and Immich)" + value = "1" + } + option { + name = "Both GPUs" + value = "all" + } +} diff --git a/dev-java/Dockerfile b/dev-java/Dockerfile new file mode 100644 index 0000000..1f5aeaf --- /dev/null +++ b/dev-java/Dockerfile @@ -0,0 +1,49 @@ +FROM codercom/enterprise-base:ubuntu + +ARG DEBIAN_FRONTEND=noninteractive +ARG TEA_VERSION= +ARG TEA_ARCH=amd64 +ARG JAVA_VERSION=21.0.4-tem +ARG MAVEN_VERSION=3.9.9 + +USER root + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y \ + build-essential ca-certificates curl git jq sudo unzip wget zip && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://get.docker.com | sh + +RUN tea_version="${TEA_VERSION}" && \ + if [ -z "$tea_version" ]; then \ + tea_version="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | jq -r .tag_name)"; \ + fi && \ + tea_version="${tea_version#v}" && \ + curl -fsSL "https://dl.gitea.com/tea/${tea_version}/tea-${tea_version}-linux-${TEA_ARCH}" \ + -o /usr/local/bin/tea && \ + chmod 755 /usr/local/bin/tea + +ENV SDKMAN_DIR=/opt/sdkman +RUN curl -fsSL "https://get.sdkman.io?rcupdate=false" | bash && \ + bash -c "source ${SDKMAN_DIR}/bin/sdkman-init.sh && \ + sdk install java ${JAVA_VERSION} && \ + sdk install maven ${MAVEN_VERSION}" && \ + chown -R coder:coder ${SDKMAN_DIR} + +RUN printf '%s\n' \ + 'export SDKMAN_DIR=/opt/sdkman' \ + '[ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ] && . "$SDKMAN_DIR/bin/sdkman-init.sh"' \ + > /etc/profile.d/sdkman.sh + +ENV CLAUDE_HOME=/opt/claude +RUN mkdir -p ${CLAUDE_HOME} && \ + env HOME=${CLAUDE_HOME} bash -c 'curl -fsSL https://claude.ai/install.sh | bash' && \ + ln -s ${CLAUDE_HOME}/.local/bin/claude /usr/local/bin/claude && \ + chown -R coder:coder ${CLAUDE_HOME} + +USER coder + +ENV JAVA_HOME="/opt/sdkman/candidates/java/current" +ENV PATH="/opt/sdkman/candidates/java/current/bin:/opt/sdkman/candidates/maven/current/bin:${PATH}" diff --git a/dev-java/README.md b/dev-java/README.md new file mode 100644 index 0000000..2de0d9c --- /dev/null +++ b/dev-java/README.md @@ -0,0 +1,97 @@ +--- +display_name: Java Development Workspace +description: Docker workspace for Java development with SDKMAN, a Temurin JDK, Maven, and Claude Code. +icon: /icon/java.svg +maintainer_github: sirblob +verified: false +tags: [docker, container, gpu, java, maven, sdkman, claude, ollama] +--- + +# Java Development Workspace + +Docker workspace for Java development with SDKMAN, a Temurin JDK, Maven, and Claude Code. + +Provisions a Docker container as a [Coder workspace](https://coder.com/docs/workspaces) with selectable GPU passthrough, Docker-out-of-Docker, and a persistent home directory. + +## Preinstalled tools + +- SDKMAN! at `/opt/sdkman`, for switching JDK and Maven versions +- Temurin JDK 21 +- Maven 3.9 +- Claude Code +- Docker CLI for Docker-out-of-Docker +- Gitea CLI (`tea`) +- code-server and JetBrains Toolbox support + +## Parameters + +| Parameter | Description | Default | +| --- | --- | --- | +| `GPU Assignment` | Which GPU(s) to attach: GPU 0, GPU 1, or both. GPU 1 is shared with the Ollama and Immich stacks. | GPU 0 | + +## Prerequisites + +The host needs the NVIDIA container toolkit for GPU passthrough. Coder itself runs as a container and creates workspaces through the host's Docker socket, so its service needs the socket mounted and the host `docker` group added: + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +group_add: + - "987" +``` + +Workspaces get the same GID through the `docker_group_id` template variable, which defaults to `987`. Confirm the host GID with `getent group docker` and override the variable if it differs. + +## Ollama + +`OLLAMA_HOST` is preset to `http://host.docker.internal:11434`. Verify with: + +```sh +curl "$OLLAMA_HOST/api/tags" +``` + +## Gitea + +The `tea` CLI manages repositories, issues, and pull requests from the workspace: + +```sh +tea login add --name sirblob --url https://git.sirblob.co --token +tea repos list +``` + +Docker-out-of-Docker lets you build and push images to the registry: + +```sh +docker login git.sirblob.co -u -p +docker build -t git.sirblob.co//: . +docker push git.sirblob.co//: +``` + +## Switching JDK versions + +`/etc/profile.d/sdkman.sh` initialises SDKMAN! for login shells. In a shell that did not pick it up, source it directly: + +```sh +source /opt/sdkman/bin/sdkman-init.sh +sdk list java +sdk install java 17.0.12-tem +``` + +## Updates + +The image is built with the base packages already upgraded, and the agent runs `apt-get update` and `apt-get upgrade` on every workspace start so a long-lived container keeps picking up security updates. + +## Persistence + +`/home/coder` is mapped to a host directory and survives workspace restarts. The rest of the root filesystem is ephemeral, so every preinstalled tool lives under `/opt` or `/usr/local` instead of the home directory. + +## Files + +| File | Contents | +| --- | --- | +| `main.tf` | Providers, data sources, and shared locals | +| `parameters.tf` | Workspace parameters | +| `agent.tf` | Coder agent, startup script, and metadata | +| `apps.tf` | code-server and JetBrains modules | +| `container.tf` | Docker image build and container resource | +| `Dockerfile` | Workspace image | diff --git a/dev-java/agent.tf b/dev-java/agent.tf new file mode 100644 index 0000000..18b2d30 --- /dev/null +++ b/dev-java/agent.tf @@ -0,0 +1,92 @@ +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + + startup_script = <<-EOT + set -e + + sudo DEBIAN_FRONTEND=noninteractive apt-get update || true + sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y || true + + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ + touch ~/.init_done + fi + EOT + + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email + OLLAMA_HOST = local.ollama_host + } + + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "2_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "GPU Usage" + key = "3_gpu_usage" + script = "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | awk '{sum+=$1} END {print sum/NR \"%\"}'" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + script = </dev/null 2>&1 || sudo groupadd --gid "$group_id" "host-$group_id" || true + done + ${replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")} + EOT + ] + + gpus = data.coder_parameter.gpu.value == "all" ? "all" : "device=${data.coder_parameter.gpu.value}" + group_add = [var.docker_group_id] + + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + "NVIDIA_VISIBLE_DEVICES=${data.coder_parameter.gpu.value}", + "NVIDIA_DRIVER_CAPABILITIES=all", + "OLLAMA_HOST=${local.ollama_host}" + ] + + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + volumes { + container_path = "/home/coder" + host_path = local.workspace_home + read_only = false + } + + volumes { + container_path = "/var/run/docker.sock" + host_path = "/var/run/docker.sock" + read_only = false + } + + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} diff --git a/dev-java/main.tf b/dev-java/main.tf new file mode 100644 index 0000000..60a8b09 --- /dev/null +++ b/dev-java/main.tf @@ -0,0 +1,35 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + docker = { + source = "kreuzwerker/docker" + } + } +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +variable "docker_group_id" { + default = "987" + description = "Host GID of the docker group, granted to workspaces for Docker-out-of-Docker" + type = string +} + +provider "docker" { + host = var.docker_socket != "" ? var.docker_socket : null +} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + workspace_home = "/media/blob/ChaiNet/coder/workspaces/coder-${data.coder_workspace.me.id}-home" + ollama_host = "http://host.docker.internal:11434" +} diff --git a/dev-java/parameters.tf b/dev-java/parameters.tf new file mode 100644 index 0000000..3b05daa --- /dev/null +++ b/dev-java/parameters.tf @@ -0,0 +1,21 @@ +data "coder_parameter" "gpu" { + name = "gpu" + display_name = "GPU Assignment" + description = "Which GPU(s) to attach. GPU 1 is shared with Ollama and Immich, so GPU 0 is the default to avoid contention." + default = "0" + mutable = true + icon = "/icon/memory.svg" + + option { + name = "GPU 0 - RTX 3060" + value = "0" + } + option { + name = "GPU 1 - GTX 1660 (shared with Ollama and Immich)" + value = "1" + } + option { + name = "Both GPUs" + value = "all" + } +} diff --git a/dev-robots/Dockerfile b/dev-robots/Dockerfile new file mode 100644 index 0000000..2ad58cf --- /dev/null +++ b/dev-robots/Dockerfile @@ -0,0 +1,79 @@ +FROM codercom/enterprise-base:ubuntu + +ARG DEBIAN_FRONTEND=noninteractive +ARG TEA_VERSION= +ARG TEA_ARCH=amd64 +ARG ROS_DISTRO= + +USER root + +ENV LANG=en_US.UTF-8 + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y \ + build-essential ca-certificates curl git gnupg jq locales pkg-config \ + software-properties-common sudo unzip wget zip && \ + locale-gen en_US en_US.UTF-8 && \ + update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://get.docker.com | sh + +RUN tea_version="${TEA_VERSION}" && \ + if [ -z "$tea_version" ]; then \ + tea_version="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | jq -r .tag_name)"; \ + fi && \ + tea_version="${tea_version#v}" && \ + curl -fsSL "https://dl.gitea.com/tea/${tea_version}/tea-${tea_version}-linux-${TEA_ARCH}" \ + -o /usr/local/bin/tea && \ + chmod 755 /usr/local/bin/tea + +RUN apt-get update && \ + apt-get install -y \ + clang clang-format clangd cmake gdb ninja-build valgrind && \ + rm -rf /var/lib/apt/lists/* + +RUN apt-get update && \ + apt-get install -y python3 python3-dev python3-pip python3-venv && \ + rm -rf /var/lib/apt/lists/* && \ + update-alternatives --install /usr/bin/python python /usr/bin/python3 1 + +RUN . /etc/os-release && \ + if [ -z "${ROS_DISTRO}" ]; then \ + case "${UBUNTU_CODENAME}" in \ + jammy) ROS_DISTRO=humble ;; \ + noble) ROS_DISTRO=jazzy ;; \ + *) echo "No ROS 2 distribution mapped for Ubuntu ${UBUNTU_CODENAME}, set the ROS_DISTRO build argument" >&2 && exit 1 ;; \ + esac; \ + fi && \ + add-apt-repository -y universe && \ + curl -fsSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \ + -o /usr/share/keyrings/ros-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu ${UBUNTU_CODENAME} main" \ + > /etc/apt/sources.list.d/ros2.list && \ + apt-get update && \ + apt-get install -y \ + python3-colcon-common-extensions python3-rosdep python3-vcstool \ + ros-dev-tools "ros-${ROS_DISTRO}-desktop" && \ + rosdep init && \ + rm -rf /var/lib/apt/lists/* + +RUN printf '%s\n' \ + 'for ros_setup in /opt/ros/*/setup.bash; do' \ + ' [ -r "$ros_setup" ] && . "$ros_setup"' \ + 'done' \ + >> /etc/bash.bashrc + +ENV UV_INSTALL_DIR=/opt/uv/bin +RUN curl -LsSf https://astral.sh/uv/install.sh | sh + +ENV CLAUDE_HOME=/opt/claude +RUN mkdir -p ${CLAUDE_HOME} && \ + env HOME=${CLAUDE_HOME} bash -c 'curl -fsSL https://claude.ai/install.sh | bash' && \ + ln -s ${CLAUDE_HOME}/.local/bin/claude /usr/local/bin/claude && \ + chown -R coder:coder ${CLAUDE_HOME} + +USER coder + +ENV PATH="/opt/uv/bin:${PATH}" diff --git a/dev-robots/README.md b/dev-robots/README.md new file mode 100644 index 0000000..a26b7b6 --- /dev/null +++ b/dev-robots/README.md @@ -0,0 +1,97 @@ +--- +display_name: Robotics Development Workspace +description: Docker workspace for robotics development with ROS 2, Python, C++, and Claude Code. +icon: /icon/memory.svg +maintainer_github: sirblob +verified: false +tags: [docker, container, gpu, ros2, robotics, python, cpp, claude, ollama] +--- + +# Robotics Development Workspace + +Docker workspace for robotics development with ROS 2, Python, C++, and Claude Code. + +Provisions a Docker container as a [Coder workspace](https://coder.com/docs/workspaces) with selectable GPU passthrough, Docker-out-of-Docker, and a persistent home directory. + +## Preinstalled tools + +- ROS 2 Desktop, with colcon, rosdep, and vcstool +- Python 3 with pip, venv, and uv +- GCC, Clang, CMake, Ninja, GDB, and Valgrind +- Claude Code +- Docker CLI for Docker-out-of-Docker +- Gitea CLI (`tea`) +- code-server and JetBrains Toolbox support + +## Parameters + +| Parameter | Description | Default | +| --- | --- | --- | +| `GPU Assignment` | Which GPU(s) to attach: GPU 0, GPU 1, or both. GPU 1 is shared with the Ollama and Immich stacks. | GPU 0 | + +## Prerequisites + +The host needs the NVIDIA container toolkit for GPU passthrough. Coder itself runs as a container and creates workspaces through the host's Docker socket, so its service needs the socket mounted and the host `docker` group added: + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +group_add: + - "987" +``` + +Workspaces get the same GID through the `docker_group_id` template variable, which defaults to `987`. Confirm the host GID with `getent group docker` and override the variable if it differs. + +## Ollama + +`OLLAMA_HOST` is preset to `http://host.docker.internal:11434`. Verify with: + +```sh +curl "$OLLAMA_HOST/api/tags" +``` + +## Gitea + +The `tea` CLI manages repositories, issues, and pull requests from the workspace: + +```sh +tea login add --name sirblob --url https://git.sirblob.co --token +tea repos list +``` + +Docker-out-of-Docker lets you build and push images to the registry: + +```sh +docker login git.sirblob.co -u -p +docker build -t git.sirblob.co//: . +docker push git.sirblob.co//: +``` + +## ROS 2 environment + +The ROS 2 distribution is chosen from the base image's Ubuntu release at build time: Jammy gets Humble, Noble gets Jazzy. Override it with the `ROS_DISTRO` build argument. + +`/etc/bash.bashrc` sources the ROS 2 setup script, so every interactive shell has the environment ready. In a non-interactive shell, source it directly: + +```sh +source /opt/ros/*/setup.bash +``` + +## Updates + +The image is built with the base packages already upgraded, and the agent runs `apt-get update` and `apt-get upgrade` on every workspace start so a long-lived container keeps picking up security updates. + +## Persistence + +`/home/coder` is mapped to a host directory and survives workspace restarts. The rest of the root filesystem is ephemeral, so every preinstalled tool lives under `/opt` or `/usr/local` instead of the home directory. + +## Files + +| File | Contents | +| --- | --- | +| `main.tf` | Providers, data sources, and shared locals | +| `parameters.tf` | Workspace parameters | +| `agent.tf` | Coder agent, startup script, and metadata | +| `apps.tf` | code-server and JetBrains modules | +| `container.tf` | Docker image build and container resource | +| `Dockerfile` | Workspace image | diff --git a/dev-robots/agent.tf b/dev-robots/agent.tf new file mode 100644 index 0000000..18b2d30 --- /dev/null +++ b/dev-robots/agent.tf @@ -0,0 +1,92 @@ +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + + startup_script = <<-EOT + set -e + + sudo DEBIAN_FRONTEND=noninteractive apt-get update || true + sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y || true + + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ + touch ~/.init_done + fi + EOT + + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email + OLLAMA_HOST = local.ollama_host + } + + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "2_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "GPU Usage" + key = "3_gpu_usage" + script = "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | awk '{sum+=$1} END {print sum/NR \"%\"}'" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + script = </dev/null 2>&1 || sudo groupadd --gid "$group_id" "host-$group_id" || true + done + ${replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")} + EOT + ] + + gpus = data.coder_parameter.gpu.value == "all" ? "all" : "device=${data.coder_parameter.gpu.value}" + group_add = [var.docker_group_id] + + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + "NVIDIA_VISIBLE_DEVICES=${data.coder_parameter.gpu.value}", + "NVIDIA_DRIVER_CAPABILITIES=all", + "OLLAMA_HOST=${local.ollama_host}" + ] + + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + volumes { + container_path = "/home/coder" + host_path = local.workspace_home + read_only = false + } + + volumes { + container_path = "/var/run/docker.sock" + host_path = "/var/run/docker.sock" + read_only = false + } + + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} diff --git a/dev-robots/main.tf b/dev-robots/main.tf new file mode 100644 index 0000000..60a8b09 --- /dev/null +++ b/dev-robots/main.tf @@ -0,0 +1,35 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + docker = { + source = "kreuzwerker/docker" + } + } +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +variable "docker_group_id" { + default = "987" + description = "Host GID of the docker group, granted to workspaces for Docker-out-of-Docker" + type = string +} + +provider "docker" { + host = var.docker_socket != "" ? var.docker_socket : null +} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + workspace_home = "/media/blob/ChaiNet/coder/workspaces/coder-${data.coder_workspace.me.id}-home" + ollama_host = "http://host.docker.internal:11434" +} diff --git a/dev-robots/parameters.tf b/dev-robots/parameters.tf new file mode 100644 index 0000000..3b05daa --- /dev/null +++ b/dev-robots/parameters.tf @@ -0,0 +1,21 @@ +data "coder_parameter" "gpu" { + name = "gpu" + display_name = "GPU Assignment" + description = "Which GPU(s) to attach. GPU 1 is shared with Ollama and Immich, so GPU 0 is the default to avoid contention." + default = "0" + mutable = true + icon = "/icon/memory.svg" + + option { + name = "GPU 0 - RTX 3060" + value = "0" + } + option { + name = "GPU 1 - GTX 1660 (shared with Ollama and Immich)" + value = "1" + } + option { + name = "Both GPUs" + value = "all" + } +} diff --git a/dev-rust/Dockerfile b/dev-rust/Dockerfile new file mode 100644 index 0000000..36edc25 --- /dev/null +++ b/dev-rust/Dockerfile @@ -0,0 +1,48 @@ +FROM codercom/enterprise-base:ubuntu + +ARG DEBIAN_FRONTEND=noninteractive +ARG TEA_VERSION= +ARG TEA_ARCH=amd64 + +USER root + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y \ + build-essential ca-certificates curl git jq pkg-config sudo unzip wget zip && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://get.docker.com | sh + +RUN tea_version="${TEA_VERSION}" && \ + if [ -z "$tea_version" ]; then \ + tea_version="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | jq -r .tag_name)"; \ + fi && \ + tea_version="${tea_version#v}" && \ + curl -fsSL "https://dl.gitea.com/tea/${tea_version}/tea-${tea_version}-linux-${TEA_ARCH}" \ + -o /usr/local/bin/tea && \ + chmod 755 /usr/local/bin/tea + +RUN apt-get update && \ + apt-get install -y python3 python3-dev python3-pip python3-venv && \ + rm -rf /var/lib/apt/lists/* && \ + update-alternatives --install /usr/bin/python python /usr/bin/python3 1 + +ENV UV_INSTALL_DIR=/opt/uv/bin +RUN curl -LsSf https://astral.sh/uv/install.sh | sh + +ENV RUSTUP_HOME=/opt/rustup +ENV CARGO_HOME=/opt/cargo +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ + sh -s -- -y --no-modify-path --default-toolchain stable --component clippy rustfmt rust-src && \ + chown -R coder:coder ${RUSTUP_HOME} ${CARGO_HOME} + +ENV CLAUDE_HOME=/opt/claude +RUN mkdir -p ${CLAUDE_HOME} && \ + env HOME=${CLAUDE_HOME} bash -c 'curl -fsSL https://claude.ai/install.sh | bash' && \ + ln -s ${CLAUDE_HOME}/.local/bin/claude /usr/local/bin/claude && \ + chown -R coder:coder ${CLAUDE_HOME} + +USER coder + +ENV PATH="/opt/cargo/bin:/opt/uv/bin:${PATH}" diff --git a/dev-rust/README.md b/dev-rust/README.md new file mode 100644 index 0000000..4a602ab --- /dev/null +++ b/dev-rust/README.md @@ -0,0 +1,86 @@ +--- +display_name: Rust Development Workspace +description: Docker workspace for Rust development with rustup, Python, and Claude Code. +icon: /icon/rust.svg +maintainer_github: sirblob +verified: false +tags: [docker, container, gpu, rust, python, claude, ollama] +--- + +# Rust Development Workspace + +Docker workspace for Rust development with rustup, Python, and Claude Code. + +Provisions a Docker container as a [Coder workspace](https://coder.com/docs/workspaces) with selectable GPU passthrough, Docker-out-of-Docker, and a persistent home directory. + +## Preinstalled tools + +- Rust stable via rustup, with cargo, clippy, rustfmt, and rust-src +- Python 3 with pip, venv, and uv +- Claude Code +- Docker CLI for Docker-out-of-Docker +- Gitea CLI (`tea`) +- code-server and JetBrains Toolbox support + +## Parameters + +| Parameter | Description | Default | +| --- | --- | --- | +| `GPU Assignment` | Which GPU(s) to attach: GPU 0, GPU 1, or both. GPU 1 is shared with the Ollama and Immich stacks. | GPU 0 | + +## Prerequisites + +The host needs the NVIDIA container toolkit for GPU passthrough. Coder itself runs as a container and creates workspaces through the host's Docker socket, so its service needs the socket mounted and the host `docker` group added: + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +group_add: + - "987" +``` + +Workspaces get the same GID through the `docker_group_id` template variable, which defaults to `987`. Confirm the host GID with `getent group docker` and override the variable if it differs. + +## Ollama + +`OLLAMA_HOST` is preset to `http://host.docker.internal:11434`. Verify with: + +```sh +curl "$OLLAMA_HOST/api/tags" +``` + +## Gitea + +The `tea` CLI manages repositories, issues, and pull requests from the workspace: + +```sh +tea login add --name sirblob --url https://git.sirblob.co --token +tea repos list +``` + +Docker-out-of-Docker lets you build and push images to the registry: + +```sh +docker login git.sirblob.co -u -p +docker build -t git.sirblob.co//: . +docker push git.sirblob.co//: +``` + +## Updates + +The image is built with the base packages already upgraded, and the agent runs `apt-get update` and `apt-get upgrade` on every workspace start so a long-lived container keeps picking up security updates. + +## Persistence + +`/home/coder` is mapped to a host directory and survives workspace restarts. The rest of the root filesystem is ephemeral, so every preinstalled tool lives under `/opt` or `/usr/local` instead of the home directory. + +## Files + +| File | Contents | +| --- | --- | +| `main.tf` | Providers, data sources, and shared locals | +| `parameters.tf` | Workspace parameters | +| `agent.tf` | Coder agent, startup script, and metadata | +| `apps.tf` | code-server and JetBrains modules | +| `container.tf` | Docker image build and container resource | +| `Dockerfile` | Workspace image | diff --git a/dev-rust/agent.tf b/dev-rust/agent.tf new file mode 100644 index 0000000..18b2d30 --- /dev/null +++ b/dev-rust/agent.tf @@ -0,0 +1,92 @@ +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + + startup_script = <<-EOT + set -e + + sudo DEBIAN_FRONTEND=noninteractive apt-get update || true + sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y || true + + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ + touch ~/.init_done + fi + EOT + + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email + OLLAMA_HOST = local.ollama_host + } + + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "2_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "GPU Usage" + key = "3_gpu_usage" + script = "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | awk '{sum+=$1} END {print sum/NR \"%\"}'" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + script = </dev/null 2>&1 || sudo groupadd --gid "$group_id" "host-$group_id" || true + done + ${replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")} + EOT + ] + + gpus = data.coder_parameter.gpu.value == "all" ? "all" : "device=${data.coder_parameter.gpu.value}" + group_add = [var.docker_group_id] + + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + "NVIDIA_VISIBLE_DEVICES=${data.coder_parameter.gpu.value}", + "NVIDIA_DRIVER_CAPABILITIES=all", + "OLLAMA_HOST=${local.ollama_host}" + ] + + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + volumes { + container_path = "/home/coder" + host_path = local.workspace_home + read_only = false + } + + volumes { + container_path = "/var/run/docker.sock" + host_path = "/var/run/docker.sock" + read_only = false + } + + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} diff --git a/dev-rust/main.tf b/dev-rust/main.tf new file mode 100644 index 0000000..60a8b09 --- /dev/null +++ b/dev-rust/main.tf @@ -0,0 +1,35 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + docker = { + source = "kreuzwerker/docker" + } + } +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +variable "docker_group_id" { + default = "987" + description = "Host GID of the docker group, granted to workspaces for Docker-out-of-Docker" + type = string +} + +provider "docker" { + host = var.docker_socket != "" ? var.docker_socket : null +} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + workspace_home = "/media/blob/ChaiNet/coder/workspaces/coder-${data.coder_workspace.me.id}-home" + ollama_host = "http://host.docker.internal:11434" +} diff --git a/dev-rust/parameters.tf b/dev-rust/parameters.tf new file mode 100644 index 0000000..3b05daa --- /dev/null +++ b/dev-rust/parameters.tf @@ -0,0 +1,21 @@ +data "coder_parameter" "gpu" { + name = "gpu" + display_name = "GPU Assignment" + description = "Which GPU(s) to attach. GPU 1 is shared with Ollama and Immich, so GPU 0 is the default to avoid contention." + default = "0" + mutable = true + icon = "/icon/memory.svg" + + option { + name = "GPU 0 - RTX 3060" + value = "0" + } + option { + name = "GPU 1 - GTX 1660 (shared with Ollama and Immich)" + value = "1" + } + option { + name = "Both GPUs" + value = "all" + } +} diff --git a/dev-web/Dockerfile b/dev-web/Dockerfile new file mode 100644 index 0000000..3fc9d96 --- /dev/null +++ b/dev-web/Dockerfile @@ -0,0 +1,51 @@ +FROM codercom/enterprise-base:ubuntu + +ARG DEBIAN_FRONTEND=noninteractive +ARG TEA_VERSION= +ARG TEA_ARCH=amd64 +ARG NODE_VERSION=22 + +USER root + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y \ + build-essential ca-certificates curl git jq sudo unzip wget zip && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://get.docker.com | sh + +RUN tea_version="${TEA_VERSION}" && \ + if [ -z "$tea_version" ]; then \ + tea_version="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | jq -r .tag_name)"; \ + fi && \ + tea_version="${tea_version#v}" && \ + curl -fsSL "https://dl.gitea.com/tea/${tea_version}/tea-${tea_version}-linux-${TEA_ARCH}" \ + -o /usr/local/bin/tea && \ + chmod 755 /usr/local/bin/tea + +RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* + +RUN apt-get update && \ + apt-get install -y python3 python3-dev python3-pip python3-venv && \ + rm -rf /var/lib/apt/lists/* && \ + update-alternatives --install /usr/bin/python python /usr/bin/python3 1 + +ENV UV_INSTALL_DIR=/opt/uv/bin +RUN curl -LsSf https://astral.sh/uv/install.sh | sh + +ENV BUN_INSTALL=/opt/bun +RUN curl -fsSL https://bun.sh/install | bash && \ + chown -R coder:coder ${BUN_INSTALL} + +ENV CLAUDE_HOME=/opt/claude +RUN mkdir -p ${CLAUDE_HOME} && \ + env HOME=${CLAUDE_HOME} bash -c 'curl -fsSL https://claude.ai/install.sh | bash' && \ + ln -s ${CLAUDE_HOME}/.local/bin/claude /usr/local/bin/claude && \ + chown -R coder:coder ${CLAUDE_HOME} + +USER coder + +ENV PATH="/opt/bun/bin:/opt/uv/bin:${PATH}" diff --git a/dev-web/README.md b/dev-web/README.md new file mode 100644 index 0000000..873169a --- /dev/null +++ b/dev-web/README.md @@ -0,0 +1,87 @@ +--- +display_name: Web Development Workspace +description: Docker workspace for web development with Node.js, Bun, Python, and Claude Code. +icon: /icon/node.svg +maintainer_github: sirblob +verified: false +tags: [docker, container, gpu, nodejs, bun, python, claude, ollama] +--- + +# Web Development Workspace + +Docker workspace for web development with Node.js, Bun, Python, and Claude Code. + +Provisions a Docker container as a [Coder workspace](https://coder.com/docs/workspaces) with selectable GPU passthrough, Docker-out-of-Docker, and a persistent home directory. + +## Preinstalled tools + +- Node.js 22 with npm and npx +- Bun +- Python 3 with pip, venv, and uv +- Claude Code +- Docker CLI for Docker-out-of-Docker +- Gitea CLI (`tea`) +- code-server and JetBrains Toolbox support + +## Parameters + +| Parameter | Description | Default | +| --- | --- | --- | +| `GPU Assignment` | Which GPU(s) to attach: GPU 0, GPU 1, or both. GPU 1 is shared with the Ollama and Immich stacks. | GPU 0 | + +## Prerequisites + +The host needs the NVIDIA container toolkit for GPU passthrough. Coder itself runs as a container and creates workspaces through the host's Docker socket, so its service needs the socket mounted and the host `docker` group added: + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +group_add: + - "987" +``` + +Workspaces get the same GID through the `docker_group_id` template variable, which defaults to `987`. Confirm the host GID with `getent group docker` and override the variable if it differs. + +## Ollama + +`OLLAMA_HOST` is preset to `http://host.docker.internal:11434`. Verify with: + +```sh +curl "$OLLAMA_HOST/api/tags" +``` + +## Gitea + +The `tea` CLI manages repositories, issues, and pull requests from the workspace: + +```sh +tea login add --name sirblob --url https://git.sirblob.co --token +tea repos list +``` + +Docker-out-of-Docker lets you build and push images to the registry: + +```sh +docker login git.sirblob.co -u -p +docker build -t git.sirblob.co//: . +docker push git.sirblob.co//: +``` + +## Updates + +The image is built with the base packages already upgraded, and the agent runs `apt-get update` and `apt-get upgrade` on every workspace start so a long-lived container keeps picking up security updates. + +## Persistence + +`/home/coder` is mapped to a host directory and survives workspace restarts. The rest of the root filesystem is ephemeral, so every preinstalled tool lives under `/opt` or `/usr/local` instead of the home directory. + +## Files + +| File | Contents | +| --- | --- | +| `main.tf` | Providers, data sources, and shared locals | +| `parameters.tf` | Workspace parameters | +| `agent.tf` | Coder agent, startup script, and metadata | +| `apps.tf` | code-server and JetBrains modules | +| `container.tf` | Docker image build and container resource | +| `Dockerfile` | Workspace image | diff --git a/dev-web/agent.tf b/dev-web/agent.tf new file mode 100644 index 0000000..18b2d30 --- /dev/null +++ b/dev-web/agent.tf @@ -0,0 +1,92 @@ +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + + startup_script = <<-EOT + set -e + + sudo DEBIAN_FRONTEND=noninteractive apt-get update || true + sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y || true + + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ + touch ~/.init_done + fi + EOT + + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email + OLLAMA_HOST = local.ollama_host + } + + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "2_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "GPU Usage" + key = "3_gpu_usage" + script = "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | awk '{sum+=$1} END {print sum/NR \"%\"}'" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + script = </dev/null 2>&1 || sudo groupadd --gid "$group_id" "host-$group_id" || true + done + ${replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")} + EOT + ] + + gpus = data.coder_parameter.gpu.value == "all" ? "all" : "device=${data.coder_parameter.gpu.value}" + group_add = [var.docker_group_id] + + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + "NVIDIA_VISIBLE_DEVICES=${data.coder_parameter.gpu.value}", + "NVIDIA_DRIVER_CAPABILITIES=all", + "OLLAMA_HOST=${local.ollama_host}" + ] + + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + volumes { + container_path = "/home/coder" + host_path = local.workspace_home + read_only = false + } + + volumes { + container_path = "/var/run/docker.sock" + host_path = "/var/run/docker.sock" + read_only = false + } + + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} diff --git a/dev-web/main.tf b/dev-web/main.tf new file mode 100644 index 0000000..60a8b09 --- /dev/null +++ b/dev-web/main.tf @@ -0,0 +1,35 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + docker = { + source = "kreuzwerker/docker" + } + } +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +variable "docker_group_id" { + default = "987" + description = "Host GID of the docker group, granted to workspaces for Docker-out-of-Docker" + type = string +} + +provider "docker" { + host = var.docker_socket != "" ? var.docker_socket : null +} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + workspace_home = "/media/blob/ChaiNet/coder/workspaces/coder-${data.coder_workspace.me.id}-home" + ollama_host = "http://host.docker.internal:11434" +} diff --git a/dev-web/parameters.tf b/dev-web/parameters.tf new file mode 100644 index 0000000..3b05daa --- /dev/null +++ b/dev-web/parameters.tf @@ -0,0 +1,21 @@ +data "coder_parameter" "gpu" { + name = "gpu" + display_name = "GPU Assignment" + description = "Which GPU(s) to attach. GPU 1 is shared with Ollama and Immich, so GPU 0 is the default to avoid contention." + default = "0" + mutable = true + icon = "/icon/memory.svg" + + option { + name = "GPU 0 - RTX 3060" + value = "0" + } + option { + name = "GPU 1 - GTX 1660 (shared with Ollama and Immich)" + value = "1" + } + option { + name = "Both GPUs" + value = "all" + } +} diff --git a/main.tf b/main.tf deleted file mode 100644 index 7a58428..0000000 --- a/main.tf +++ /dev/null @@ -1,233 +0,0 @@ -terraform { - required_providers { - coder = { - source = "coder/coder" - } - docker = { - source = "kreuzwerker/docker" - } - } -} - -locals { - username = data.coder_workspace_owner.me.name -} - -variable "docker_socket" { - default = "" - description = "(Optional) Docker socket URI" - type = string -} - -provider "docker" { - host = var.docker_socket != "" ? var.docker_socket : null -} - -data "coder_provisioner" "me" {} -data "coder_workspace" "me" {} -data "coder_workspace_owner" "me" {} - -data "coder_parameter" "gpu" { - name = "gpu" - display_name = "GPU Assignment" - description = "Which GPU(s) to attach. GPU 1 is shared with Ollama and Immich, so GPU 0 is the default to avoid contention." - default = "0" - mutable = true - icon = "/icon/memory.svg" - - option { - name = "GPU 0 only" - value = "0" - } - option { - name = "GPU 1 only (shared with Ollama and Immich)" - value = "1" - } - option { - name = "Both GPUs" - value = "all" - } -} - -resource "coder_agent" "main" { - arch = data.coder_provisioner.me.arch - os = "linux" - startup_script = <<-EOT - set -e - - # 1. Allow user to use the Docker socket (Docker-out-of-Docker) - # We set strict permissions on the socket file to ensure access - if [ -e /var/run/docker.sock ]; then - sudo chmod 666 /var/run/docker.sock - fi - - # 2. Initialize Home Directory - if [ ! -f ~/.init_done ]; then - cp -rT /etc/skel ~ - touch ~/.init_done - fi - EOT - - env = { - GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) - GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}" - GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) - GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}" - OLLAMA_HOST = "http://host.docker.internal:11434" - } - - metadata { - display_name = "CPU Usage" - key = "0_cpu_usage" - script = "coder stat cpu" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "RAM Usage" - key = "1_ram_usage" - script = "coder stat mem" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "Home Disk" - key = "3_home_disk" - script = "coder stat disk --path $${HOME}" - interval = 60 - timeout = 1 - } - - metadata { - display_name = "GPU Usage" - key = "8_gpu_usage" - script = "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | awk '{sum+=$1} END {print sum/NR \"%\"}'" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "CPU Usage (Host)" - key = "4_cpu_usage_host" - script = "coder stat cpu --host" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "Memory Usage (Host)" - key = "5_mem_usage_host" - script = "coder stat mem --host" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "Load Average (Host)" - key = "6_load_host" - script = <