Organize templates into per-stack folders
This commit is contained in:
-29
@@ -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}"
|
|
||||||
@@ -1,69 +1,63 @@
|
|||||||
---
|
# Coder Templates
|
||||||
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]
|
|
||||||
---
|
|
||||||
|
|
||||||
# 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-<stack>/
|
||||||
|
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
|
## 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:
|
```yaml
|
||||||
|
volumes:
|
||||||
```sh
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
sudo adduser coder docker
|
group_add:
|
||||||
sudo systemctl restart coder
|
- "987"
|
||||||
sudo -u coder docker ps
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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`.
|
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.
|
||||||
- **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 <user> -p <token>
|
|
||||||
docker build -t git.sirblob.co/<user>/<image>:<tag> .
|
|
||||||
docker push git.sirblob.co/<user>/<image>:<tag>
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
@@ -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 <token>
|
||||||
|
tea repos list
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker-out-of-Docker lets you build and push images to the registry:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker login git.sirblob.co -u <user> -p <token>
|
||||||
|
docker build -t git.sirblob.co/<user>/<image>:<tag> .
|
||||||
|
docker push git.sirblob.co/<user>/<image>:<tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 |
|
||||||
@@ -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 = <<EOT
|
||||||
|
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||||
|
EOT
|
||||||
|
interval = 60
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "Swap Usage (Host)"
|
||||||
|
key = "7_swap_host"
|
||||||
|
script = <<EOT
|
||||||
|
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||||
|
EOT
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/code-server/coder"
|
||||||
|
version = "~> 1.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
order = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jetbrains" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/jetbrains/coder"
|
||||||
|
version = "~> 1.1"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
agent_name = "main"
|
||||||
|
folder = "/home/coder"
|
||||||
|
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
resource "docker_image" "workspace" {
|
||||||
|
name = "coder-${data.coder_workspace.me.id}"
|
||||||
|
|
||||||
|
build {
|
||||||
|
context = "."
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_locally = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_container" "workspace" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
image = docker_image.workspace.name
|
||||||
|
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||||
|
hostname = data.coder_workspace.me.name
|
||||||
|
|
||||||
|
entrypoint = [
|
||||||
|
"sh", "-c",
|
||||||
|
<<EOT
|
||||||
|
sudo chown -R coder:coder /home/coder
|
||||||
|
for group_id in $(id -G); do
|
||||||
|
getent group "$group_id" >/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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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 <token>
|
||||||
|
tea repos list
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker-out-of-Docker lets you build and push images to the registry:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker login git.sirblob.co -u <user> -p <token>
|
||||||
|
docker build -t git.sirblob.co/<user>/<image>:<tag> .
|
||||||
|
docker push git.sirblob.co/<user>/<image>:<tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 |
|
||||||
@@ -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 = <<EOT
|
||||||
|
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||||
|
EOT
|
||||||
|
interval = 60
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "Swap Usage (Host)"
|
||||||
|
key = "7_swap_host"
|
||||||
|
script = <<EOT
|
||||||
|
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||||
|
EOT
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/code-server/coder"
|
||||||
|
version = "~> 1.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
order = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jetbrains" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/jetbrains/coder"
|
||||||
|
version = "~> 1.1"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
agent_name = "main"
|
||||||
|
folder = "/home/coder"
|
||||||
|
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
resource "docker_image" "workspace" {
|
||||||
|
name = "coder-${data.coder_workspace.me.id}"
|
||||||
|
|
||||||
|
build {
|
||||||
|
context = "."
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_locally = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_container" "workspace" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
image = docker_image.workspace.name
|
||||||
|
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||||
|
hostname = data.coder_workspace.me.name
|
||||||
|
|
||||||
|
entrypoint = [
|
||||||
|
"sh", "-c",
|
||||||
|
<<EOT
|
||||||
|
sudo chown -R coder:coder /home/coder
|
||||||
|
for group_id in $(id -G); do
|
||||||
|
getent group "$group_id" >/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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
@@ -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 <token>
|
||||||
|
tea repos list
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker-out-of-Docker lets you build and push images to the registry:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker login git.sirblob.co -u <user> -p <token>
|
||||||
|
docker build -t git.sirblob.co/<user>/<image>:<tag> .
|
||||||
|
docker push git.sirblob.co/<user>/<image>:<tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 |
|
||||||
@@ -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 = <<EOT
|
||||||
|
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||||
|
EOT
|
||||||
|
interval = 60
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "Swap Usage (Host)"
|
||||||
|
key = "7_swap_host"
|
||||||
|
script = <<EOT
|
||||||
|
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||||
|
EOT
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/code-server/coder"
|
||||||
|
version = "~> 1.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
order = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jetbrains" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/jetbrains/coder"
|
||||||
|
version = "~> 1.1"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
agent_name = "main"
|
||||||
|
folder = "/home/coder"
|
||||||
|
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
resource "docker_image" "workspace" {
|
||||||
|
name = "coder-${data.coder_workspace.me.id}"
|
||||||
|
|
||||||
|
build {
|
||||||
|
context = "."
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_locally = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_container" "workspace" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
image = docker_image.workspace.name
|
||||||
|
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||||
|
hostname = data.coder_workspace.me.name
|
||||||
|
|
||||||
|
entrypoint = [
|
||||||
|
"sh", "-c",
|
||||||
|
<<EOT
|
||||||
|
sudo chown -R coder:coder /home/coder
|
||||||
|
for group_id in $(id -G); do
|
||||||
|
getent group "$group_id" >/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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
@@ -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 <token>
|
||||||
|
tea repos list
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker-out-of-Docker lets you build and push images to the registry:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker login git.sirblob.co -u <user> -p <token>
|
||||||
|
docker build -t git.sirblob.co/<user>/<image>:<tag> .
|
||||||
|
docker push git.sirblob.co/<user>/<image>:<tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 |
|
||||||
@@ -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 = <<EOT
|
||||||
|
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||||
|
EOT
|
||||||
|
interval = 60
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "Swap Usage (Host)"
|
||||||
|
key = "7_swap_host"
|
||||||
|
script = <<EOT
|
||||||
|
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||||
|
EOT
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/code-server/coder"
|
||||||
|
version = "~> 1.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
order = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jetbrains" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/jetbrains/coder"
|
||||||
|
version = "~> 1.1"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
agent_name = "main"
|
||||||
|
folder = "/home/coder"
|
||||||
|
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
resource "docker_image" "workspace" {
|
||||||
|
name = "coder-${data.coder_workspace.me.id}"
|
||||||
|
|
||||||
|
build {
|
||||||
|
context = "."
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_locally = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_container" "workspace" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
image = docker_image.workspace.name
|
||||||
|
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||||
|
hostname = data.coder_workspace.me.name
|
||||||
|
|
||||||
|
entrypoint = [
|
||||||
|
"sh", "-c",
|
||||||
|
<<EOT
|
||||||
|
sudo chown -R coder:coder /home/coder
|
||||||
|
for group_id in $(id -G); do
|
||||||
|
getent group "$group_id" >/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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
@@ -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 <token>
|
||||||
|
tea repos list
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker-out-of-Docker lets you build and push images to the registry:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker login git.sirblob.co -u <user> -p <token>
|
||||||
|
docker build -t git.sirblob.co/<user>/<image>:<tag> .
|
||||||
|
docker push git.sirblob.co/<user>/<image>:<tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 |
|
||||||
@@ -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 = <<EOT
|
||||||
|
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||||
|
EOT
|
||||||
|
interval = 60
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "Swap Usage (Host)"
|
||||||
|
key = "7_swap_host"
|
||||||
|
script = <<EOT
|
||||||
|
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||||
|
EOT
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/code-server/coder"
|
||||||
|
version = "~> 1.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
order = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jetbrains" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/jetbrains/coder"
|
||||||
|
version = "~> 1.1"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
agent_name = "main"
|
||||||
|
folder = "/home/coder"
|
||||||
|
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
resource "docker_image" "workspace" {
|
||||||
|
name = "coder-${data.coder_workspace.me.id}"
|
||||||
|
|
||||||
|
build {
|
||||||
|
context = "."
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_locally = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_container" "workspace" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
image = docker_image.workspace.name
|
||||||
|
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||||
|
hostname = data.coder_workspace.me.name
|
||||||
|
|
||||||
|
entrypoint = [
|
||||||
|
"sh", "-c",
|
||||||
|
<<EOT
|
||||||
|
sudo chown -R coder:coder /home/coder
|
||||||
|
for group_id in $(id -G); do
|
||||||
|
getent group "$group_id" >/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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
@@ -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 <token>
|
||||||
|
tea repos list
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker-out-of-Docker lets you build and push images to the registry:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker login git.sirblob.co -u <user> -p <token>
|
||||||
|
docker build -t git.sirblob.co/<user>/<image>:<tag> .
|
||||||
|
docker push git.sirblob.co/<user>/<image>:<tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 |
|
||||||
@@ -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 = <<EOT
|
||||||
|
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||||
|
EOT
|
||||||
|
interval = 60
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
display_name = "Swap Usage (Host)"
|
||||||
|
key = "7_swap_host"
|
||||||
|
script = <<EOT
|
||||||
|
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||||
|
EOT
|
||||||
|
interval = 10
|
||||||
|
timeout = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/code-server/coder"
|
||||||
|
version = "~> 1.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
order = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jetbrains" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/jetbrains/coder"
|
||||||
|
version = "~> 1.1"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
agent_name = "main"
|
||||||
|
folder = "/home/coder"
|
||||||
|
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
resource "docker_image" "workspace" {
|
||||||
|
name = "coder-${data.coder_workspace.me.id}"
|
||||||
|
|
||||||
|
build {
|
||||||
|
context = "."
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_locally = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_container" "workspace" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
image = docker_image.workspace.name
|
||||||
|
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||||
|
hostname = data.coder_workspace.me.name
|
||||||
|
|
||||||
|
entrypoint = [
|
||||||
|
"sh", "-c",
|
||||||
|
<<EOT
|
||||||
|
sudo chown -R coder:coder /home/coder
|
||||||
|
for group_id in $(id -G); do
|
||||||
|
getent group "$group_id" >/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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = <<EOT
|
|
||||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
|
||||||
EOT
|
|
||||||
interval = 60
|
|
||||||
timeout = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata {
|
|
||||||
display_name = "Swap Usage (Host)"
|
|
||||||
key = "7_swap_host"
|
|
||||||
script = <<EOT
|
|
||||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
|
||||||
EOT
|
|
||||||
interval = 10
|
|
||||||
timeout = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module "code-server" {
|
|
||||||
count = data.coder_workspace.me.start_count
|
|
||||||
source = "registry.coder.com/coder/code-server/coder"
|
|
||||||
version = "~> 1.0"
|
|
||||||
agent_id = coder_agent.main.id
|
|
||||||
order = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
module "jetbrains" {
|
|
||||||
count = data.coder_workspace.me.start_count
|
|
||||||
source = "registry.coder.com/coder/jetbrains/coder"
|
|
||||||
version = "~> 1.1"
|
|
||||||
agent_id = coder_agent.main.id
|
|
||||||
agent_name = "main"
|
|
||||||
folder = "/home/coder"
|
|
||||||
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "docker_image" "workspace" {
|
|
||||||
name = "coder-${data.coder_workspace.me.id}"
|
|
||||||
build {
|
|
||||||
context = "."
|
|
||||||
dockerfile = "Dockerfile"
|
|
||||||
}
|
|
||||||
keep_locally = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "docker_container" "workspace" {
|
|
||||||
count = data.coder_workspace.me.start_count
|
|
||||||
image = docker_image.workspace.name
|
|
||||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
|
||||||
hostname = data.coder_workspace.me.name
|
|
||||||
|
|
||||||
entrypoint = [
|
|
||||||
"sh", "-c",
|
|
||||||
<<EOT
|
|
||||||
sudo chown -R coder:coder /home/coder
|
|
||||||
${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}"
|
|
||||||
|
|
||||||
env = [
|
|
||||||
"CODER_AGENT_TOKEN=${coder_agent.main.token}",
|
|
||||||
"NVIDIA_VISIBLE_DEVICES=${data.coder_parameter.gpu.value}",
|
|
||||||
"NVIDIA_DRIVER_CAPABILITIES=all",
|
|
||||||
"OLLAMA_HOST=http://host.docker.internal:11434"
|
|
||||||
]
|
|
||||||
|
|
||||||
host {
|
|
||||||
host = "host.docker.internal"
|
|
||||||
ip = "host-gateway"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 1. Existing Home Directory Volume
|
|
||||||
volumes {
|
|
||||||
container_path = "/home/coder"
|
|
||||||
host_path = "/media/blob/ChaiNet/coder/workspaces/coder-${data.coder_workspace.me.id}-home"
|
|
||||||
read_only = false
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2. NEW: Docker Socket Volume (Enables Docker-out-of-Docker)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user