Organize templates into per-stack folders
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user