This commit is contained in:
2025-09-19 18:22:32 -04:00
parent 5b841fd06e
commit d2a3a335d0
13 changed files with 1429 additions and 43 deletions

View File

@@ -4,6 +4,11 @@ FROM mcr.microsoft.com/devcontainers/universal:2-linux
# Switch to root for installations
USER root
# Change codespace user to coder for Coder compatibility
RUN usermod -l coder codespace && \
groupmod -n coder codespace && \
usermod -d /home/coder -m coder
# Install additional development tools not included in features
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \
apt-get install -y \
@@ -11,75 +16,157 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \
ripgrep \
fd-find \
bat \
eza \
htop \
btop \
ncdu \
ranger \
tmux \
neovim \
tree \
curl \
wget \
unzip \
# Development tools
jq \
yq \
httpie \
lazygit \
lazydocker \
# Build tools
build-essential \
cmake \
pkg-config \
make \
# Database clients
postgresql-client \
redis-tools \
# Networking tools for port forwarding
socat \
psmisc \
# Additional utilities for scripts
git \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Install Rust-based tools using cargo
USER codespace
RUN cargo install \
starship \
zoxide \
tokei \
git-delta \
--locked
# Switch to coder user for user-specific installations
USER coder
# Install Node.js global packages
RUN npm install -g \
pnpm \
yarn \
turbo \
@claude-ai/cli \
vercel \
netlify-cli \
tsx \
nodemon
# Install uv early for Python package management
RUN pip install --user uv
# Install Python packages
RUN pip install --user \
poetry \
pipenv \
black \
ruff \
mypy \
pytest \
httpx \
rich
# Install Rust-based tools using cargo (check if available first)
RUN if [ -f ~/.cargo/env ]; then \
/bin/bash -c "source ~/.cargo/env && cargo install starship zoxide tokei git-delta --locked"; \
elif command -v cargo >/dev/null 2>&1; then \
cargo install starship zoxide tokei git-delta --locked; \
else \
echo "Rust/cargo not available, skipping Rust tools"; \
fi
# Create necessary directories with correct permissions
# Install Node.js and Python packages in single layer
RUN echo "Installing Node.js packages..." && \
npm install -g --silent \
pnpm@latest \
yarn@latest \
turbo@latest \
@anthropic-ai/claude-code@latest \
vercel@latest \
netlify-cli@latest \
tsx@latest \
nodemon@latest \
tldr@latest \
fkill-cli@latest \
repomix@latest && \
echo "Installing Python packages..." && \
export PATH="/home/coder/.local/bin:$PATH" && \
~/.local/bin/uv tool install poetry && \
~/.local/bin/uv tool install black && \
~/.local/bin/uv tool install ruff && \
~/.local/bin/uv tool install mypy && \
~/.local/bin/uv tool install pytest && \
pip install --user --quiet pipenv httpx rich && \
echo "All packages installed successfully"
# Create necessary directories and copy scripts
USER root
RUN mkdir -p /home/coder /workspaces && \
chown -R 1000:1000 /home/coder /workspaces
# Copy our custom scripts
COPY tf/scripts/*.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/*.sh
# Switch to coder user
USER coder
RUN mkdir -p ~/bin ~/.config
WORKDIR /workspaces
# Set up shell configuration
RUN echo 'eval "$(starship init zsh)"' >> ~/.zshrc && \
echo 'eval "$(zoxide init zsh)"' >> ~/.zshrc && \
echo 'alias ll="eza -la"' >> ~/.zshrc && \
# Set up shell configuration with enhanced aliases and tools
RUN echo 'export PATH="$HOME/.local/bin:$HOME/bin:$PATH"' >> ~/.zshrc && \
echo 'alias ll="ls -la"' >> ~/.zshrc && \
echo 'alias cat="bat"' >> ~/.zshrc && \
echo 'alias find="fd"' >> ~/.zshrc
echo 'alias find="fd"' >> ~/.zshrc && \
echo '# Rust tools (if available)' >> ~/.zshrc && \
echo 'command -v starship >/dev/null 2>&1 && eval "$(starship init zsh)"' >> ~/.zshrc && \
echo 'command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init zsh)"' >> ~/.zshrc
# Set up IDE configurations using our scripts
RUN /usr/local/bin/cursor-setup.sh && \
/usr/local/bin/windsurf-setup.sh
# Create devinfo utility
RUN cat <<'EOF' > "$HOME/bin/devinfo" \
&& chmod +x "$HOME/bin/devinfo"
#!/usr/bin/env bash
set -euo pipefail
echo "Workspace diagnostics"
echo "----------------------"
echo "User: $(whoami)"
echo "Home: ${HOME}"
echo "Workspace: /workspaces"
if command -v node >/dev/null 2>&1; then
echo "Node: $(node --version)"
fi
if command -v npm >/dev/null 2>&1; then
echo "npm: $(npm --version)"
fi
if command -v python3 >/dev/null 2>&1; then
echo "Python: $(python3 --version | awk '{print $2}')"
fi
if command -v rustc >/dev/null 2>&1; then
echo "Rust: $(rustc --version | awk '{print $2}')"
fi
if command -v cargo >/dev/null 2>&1; then
echo "Cargo: $(cargo --version | awk '{print $2}')"
fi
if [[ -n "${POSTGRES_URL:-}" ]]; then
echo "PostgreSQL: ${POSTGRES_URL}"
fi
if [[ -n "${REDIS_URL:-}" ]]; then
echo "Redis: ${REDIS_URL}"
fi
if [[ -n "${QDRANT_URL:-}" ]]; then
echo "Qdrant: ${QDRANT_URL}"
fi
EOF
# Create Claude CLI helper
RUN cat <<'EOF' > "$HOME/bin/claude-help" \
&& chmod +x "$HOME/bin/claude-help"
#!/usr/bin/env bash
cat <<'TXT'
Claude CLI quick start
----------------------
claude auth login # authenticate
claude chat # open an interactive chat
claude edit <file> # AI assisted editing
claude analyze . # Review the current directory
TXT
EOF
# Set environment variables for consistent operation
ENV USER_NAME=coder
ENV HOME_DIR=/home/coder
ENV WORKSPACES_DIR=/workspaces
ENV BIN_DIR=/home/coder/bin
ENV META_DIR=/tmp/git-metadata
# Expose common development ports
EXPOSE 3000 5000 8000 8080 8888 5050 6333
# Default command
CMD ["/bin/zsh"]

67
tf-dockerfile/.terraform.lock.hcl generated Normal file
View File

@@ -0,0 +1,67 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/coder/coder" {
version = "2.11.0"
constraints = ">= 2.5.0, >= 2.7.0"
hashes = [
"h1:nvP1/5GV2smNJ1Vkoq7pCCKYmC/BtmyhJFwNnDzWNT4=",
"zh:149f9da8a52d4d0352f97b71cdd2527a79ab79745dabab917f0f44b183a25a28",
"zh:218dcadb37927fac084e7465fc47d25a2e73e8db7773d7cbed96ff331e93e8be",
"zh:3adb6978c6128e56a688c14224fcbce0a0c9813418698418c69bdbecbe37bbcc",
"zh:4b3dc8c770fb087c0ea319e456b3b0c30bc29c40e54ee829b68e8c3bf13136bf",
"zh:4ca8dfd586ff36af83d146a75993c248df6fea42fc1749321bc45b8800f6854f",
"zh:62f3fb140b300fb9bc346b96620ecf601a0d66bf8411c556a933899beb1f9a30",
"zh:737bb3ee2b6db703dc228142dacb5633f21d773b1987cba4bfc15ab545546a00",
"zh:73c7fc8eba3ba1da76fbfb2ecbe79f0c9ecf37ede3ba8e018ad5d2273bd1a4d3",
"zh:790e296cb7a4d7852098d26df50b248e6ed42e7f929f1db81072106f4818aa42",
"zh:7cfd162d5d33ec374fda6003a18699e2735e24624c3bf5f1c225a2620cb9443f",
"zh:cc1c3191021ac0a189d994a924af60bbefe0f314fc36976cc9e15a1fd7b2569a",
"zh:ea3dcdeb80e15b8e52c388b01dd0605ae2ba028016ff6dd8b73f1007c94c9e04",
"zh:ebf9a9f7dd8e04aac0c4384342b2dd733db45cbb2c220732d8ab4a04f91b062f",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
"zh:f734663759f3d535f4c616f8f1d1877450a0c4e09f79dc29f3ff2596f2e5807e",
]
}
provider "registry.terraform.io/hashicorp/http" {
version = "3.5.0"
constraints = ">= 3.0.0"
hashes = [
"h1:8bUoPwS4hahOvzCBj6b04ObLVFXCEmEN8T/5eOHmWOM=",
"zh:047c5b4920751b13425efe0d011b3a23a3be97d02d9c0e3c60985521c9c456b7",
"zh:157866f700470207561f6d032d344916b82268ecd0cf8174fb11c0674c8d0736",
"zh:1973eb9383b0d83dd4fd5e662f0f16de837d072b64a6b7cd703410d730499476",
"zh:212f833a4e6d020840672f6f88273d62a564f44acb0c857b5961cdb3bbc14c90",
"zh:2c8034bc039fffaa1d4965ca02a8c6d57301e5fa9fff4773e684b46e3f78e76a",
"zh:5df353fc5b2dd31577def9cc1a4ebf0c9a9c2699d223c6b02087a3089c74a1c6",
"zh:672083810d4185076c81b16ad13d1224b9e6ea7f4850951d2ab8d30fa6e41f08",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:7b4200f18abdbe39904b03537e1a78f21ebafe60f1c861a44387d314fda69da6",
"zh:843feacacd86baed820f81a6c9f7bd32cf302db3d7a0f39e87976ebc7a7cc2ee",
"zh:a9ea5096ab91aab260b22e4251c05f08dad2ed77e43e5e4fadcdfd87f2c78926",
"zh:d02b288922811739059e90184c7f76d45d07d3a77cc48d0b15fd3db14e928623",
]
}
provider "registry.terraform.io/kreuzwerker/docker" {
version = "2.25.0"
constraints = "~> 2.25"
hashes = [
"h1:nB2atWOMNrq3tfVH216oFFCQ/TNjAXXno6ZyZhlGdQs=",
"zh:02ca00d987b2e56195d2e97d82349f680d4b94a6a0d514dc6c0031317aec4f11",
"zh:432d333412f01b7547b3b264ec85a2627869fdf5f75df9d237b0dc6a6848b292",
"zh:4709e81fea2b9132020d6c786a1d1d02c77254fc0e299ea1bb636892b6cadac6",
"zh:53c4a4ab59a1e0671d2292d74f14e060489482d430ad811016bf7cb95503c5de",
"zh:6c0865e514ceffbf19ace806fb4595bf05d0a165dd9c8664f8768da385ccc091",
"zh:6d72716d58b8c18cd0b223265b2a190648a14973223cc198a019b300ede07570",
"zh:a710ce90557c54396dfc27b282452a8f5373eb112a10e9fd77043ca05d30e72f",
"zh:e0868c7ac58af596edfa578473013bd550e40c0a1f6adc2c717445ebf9fd694e",
"zh:e2ab2c40631f100130e7b525e07be7a9b8d8fcb8f57f21dca235a3e15818636b",
"zh:e40c93b1d99660f92dd0c75611bcb9e68ae706d4c0bc6fac32f672e19e6f05bf",
"zh:e480501b2dd1399135ec7eb820e1be88f9381d32c4df093f2f4645863f8c48f4",
"zh:f1a71e90aa388d34691595883f6526543063f8e338792b7c2c003b2c8c63d108",
"zh:f346cd5d25a31991487ca5dc7a05e104776c3917482bc2a24ec6a90bb697b22e",
"zh:fa822a4eb4e6385e88fbb133fd63d3a953693712a7adeb371913a2d477c0148c",
]
}

50
tf-dockerfile/README.md Normal file
View File

@@ -0,0 +1,50 @@
# Terraform Workspace Template
This Terraform module provisions a Coder workspace that mirrors the devcontainer experience defined in this repository. The files in `tf/` are mounted into the workspace at `/home/coder/code-tools`, so the helper scripts referenced below are always available.
## What You Get
- One Docker workspace container built from `var.devcontainer_image` (defaults to the universal Dev Container image).
- Optional PostgreSQL, Redis, and Qdrant services running on the same Docker network, plus pgAdmin and Jupyter toggles.
- Startup scripts that install core tooling and (optionally) AI helpers for Claude, Cursor, and Windsurf.
- A trimmed Coder application list (VS Code, Terminal, pgAdmin, Qdrant, Jupyter, and a few common dev ports).
## Key Inputs
| Name | Description | Default |
| --- | --- | --- |
| `devcontainer_image` | Workspace container image | `mcr.microsoft.com/devcontainers/universal:2-linux` |
| `workspace_memory_limit` | Memory limit in MB (0 = image default) | `8192` |
| `enable_docker_in_docker` | Mount `/var/run/docker.sock` | `true` |
| `postgres_password` / `redis_password` | Service credentials | `devpassword` |
| `postgres_max_connections` | PostgreSQL connection cap | `100` |
| `redis_max_memory` | Redis maxmemory setting | `512mb` |
| `pgadmin_email` / `pgadmin_password` | pgAdmin login | `admin@dev.local` / `adminpassword` |
| `install_*` flags | Control which AI helpers run when enabled | all `true` |
Workspace creators see only a handful of parameters:
1. Optional repository URL to clone into `/workspaces`.
2. Toggles for data services, AI tooling, pgAdmin, Jupyter, and JetBrains Gateway.
## Files
```
main.tf # Providers, parameters, locals, Docker primitives
workspace.tf # Coder agent and workspace container
services.tf # PostgreSQL / Redis / Qdrant (+ pgAdmin & Jupyter)
apps.tf # Essential Coder apps and dev-port helpers
scripts.tf # Core + AI scripts wired to the agent
variables.tf # Minimal variable surface area
terraform.tfvars# Opinionated defaults you can override
outputs.tf # Helpful connection strings and metadata
scripts/ # Shell scripts invoked by Terraform resources
```
## Usage
1. From the Coder deployment (mounted at `/home/coder/code-tools/tf`), run `terraform init` and `terraform apply`.
2. When prompted for the **Project repository**, supply any Git URL to clone into `/workspaces` or leave it blank for an empty workspace.
3. Toggle services and AI tools to suit the workspace. If services are enabled, the bundled `port-forward.sh` script exposes pgAdmin on `localhost:5050` and Qdrant on `localhost:6333`.
4. The devcontainer image should install language toolchains; the `workspace-setup.sh` and `dev-tools.sh` scripts simply finish configuration inside the workspace.
Refer to [Coders devcontainer template guide](https://coder.com/docs/@v2.26.0/admin/templates/managing-templates/devcontainers/add-devcontainer) for broader context on how this Terraform fits into your deployment.

134
tf-dockerfile/apps.tf Normal file
View File

@@ -0,0 +1,134 @@
resource "coder_app" "code_server" {
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "VS Code"
url = "http://localhost:8080"
icon = "/icon/code.svg"
subdomain = true
share = "owner"
healthcheck {
url = "http://localhost:8080/healthz"
interval = 10
threshold = 5
}
}
resource "coder_app" "terminal" {
agent_id = coder_agent.main.id
slug = "terminal"
display_name = "Terminal"
icon = "/icon/terminal.svg"
command = "bash"
}
resource "coder_app" "pgadmin" {
count = local.services_enabled && data.coder_parameter.enable_pgadmin.value ? 1 : 0
agent_id = coder_agent.main.id
slug = "pgadmin"
display_name = "pgAdmin"
url = "http://localhost:5050"
icon = "/icon/postgres.svg"
subdomain = true
share = "owner"
healthcheck {
url = "http://localhost:5050"
interval = 15
threshold = 5
}
}
resource "coder_app" "qdrant" {
count = local.services_enabled ? 1 : 0
agent_id = coder_agent.main.id
slug = "qdrant"
display_name = "Qdrant"
url = "http://localhost:6333"
icon = "/icon/database.svg"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:6333/health"
interval = 30
threshold = 10
}
}
resource "coder_app" "jupyter" {
count = data.coder_parameter.enable_jupyter.value ? 1 : 0
agent_id = coder_agent.main.id
slug = "jupyter"
display_name = "JupyterLab"
url = "http://localhost:8888"
icon = "/icon/jupyter.svg"
subdomain = true
share = "owner"
healthcheck {
url = "http://localhost:8888"
interval = 20
threshold = 10
}
}
locals {
dev_ports = {
"dev-3000" = {
display = "Web Dev (3000)"
url = "http://localhost:3000"
icon = "/icon/javascript.svg"
}
"api-8000" = {
display = "API (8000)"
url = "http://localhost:8000"
icon = "/icon/node.svg"
}
"vite-5173" = {
display = "Vite (5173)"
url = "http://localhost:5173"
icon = "/icon/typescript.svg"
}
}
}
resource "coder_app" "dev_ports" {
for_each = local.dev_ports
agent_id = coder_agent.main.id
slug = each.key
display_name = each.value.display
url = each.value.url
icon = each.value.icon
subdomain = true
share = "owner"
healthcheck {
url = each.value.url
interval = 10
threshold = 10
}
}
resource "coder_app" "claude_cli" {
count = data.coder_parameter.enable_ai_tools.value ? 1 : 0
agent_id = coder_agent.main.id
slug = "claude-cli"
display_name = "Claude CLI"
icon = "/icon/claude.svg"
command = "bash -lc 'claude --dangerously-skip-permissions'"
group = "AI Tools"
order = 10
}
resource "coder_app" "codex_cli" {
count = data.coder_parameter.enable_ai_tools.value ? 1 : 0
agent_id = coder_agent.main.id
slug = "codex-cli"
display_name = "Codex CLI"
icon = "/icon/code.svg"
command = "bash -lc 'codex --dangerously-bypass-approvals-and-sandbox'"
group = "AI Tools"
order = 20
}

193
tf-dockerfile/main.tf Normal file
View File

@@ -0,0 +1,193 @@
terraform {
required_version = ">= 1.3.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
docker = {
source = "kreuzwerker/docker"
version = "~> 2.25"
}
http = {
source = "hashicorp/http"
version = ">= 3.0"
}
}
}
provider "coder" {}
provider "docker" {
host = var.docker_socket != "" ? var.docker_socket : null
}
provider "http" {}
# Workspace context
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
# User inputs kept intentionally small so the template is easy to launch.
data "coder_parameter" "project_repository" {
name = "project_repository"
display_name = "Project repository"
description = "Optional Git URL cloned into /workspaces on first startup."
default = ""
mutable = true
order = 1
}
data "coder_parameter" "enable_services" {
name = "enable_services"
display_name = "Enable PostgreSQL / Redis / Qdrant"
description = "Provision bundled data services inside the workspace network."
type = "bool"
default = "true"
mutable = true
order = 2
}
data "coder_parameter" "enable_ai_tools" {
name = "enable_ai_tools"
display_name = "Install AI tooling"
description = "Run the bundled AI helper scripts (Claude, Cursor, Windsurf)."
type = "bool"
default = "true"
mutable = true
order = 3
}
data "coder_parameter" "enable_pgadmin" {
name = "enable_pgadmin"
display_name = "Expose pgAdmin"
description = "Start the pgAdmin container when database services are enabled."
type = "bool"
default = "true"
mutable = true
order = 4
}
data "coder_parameter" "enable_jupyter" {
name = "enable_jupyter"
display_name = "Expose JupyterLab"
description = "Start the optional JupyterLab container."
type = "bool"
default = "false"
mutable = true
order = 5
}
data "coder_parameter" "enable_jetbrains" {
name = "enable_jetbrains"
display_name = "JetBrains Gateway"
description = "Install JetBrains Gateway integration for this workspace."
type = "bool"
default = "true"
mutable = true
order = 6
}
data "coder_parameter" "ai_prompt" {
name = "AI Prompt"
display_name = "AI Task Prompt"
description = "Optional pre-filled prompt shown when starting a Claude Code task."
type = "string"
default = ""
mutable = true
order = 7
form_type = "textarea"
}
locals {
bind_mounts = [
{ host = "${var.host_home_path}/.gitconfig", container = "/home/coder/.gitconfig" },
{ host = "${var.host_home_path}/.git-credentials", container = "/home/coder/.git-credentials" },
{ host = "${var.host_home_path}/.ssh", container = "/home/coder/.ssh" },
{ host = "${var.host_home_path}/.zshrc", container = "/home/coder/.zshrc" },
{ host = "${var.host_home_path}/.oh-my-zsh", container = "/home/coder/.oh-my-zsh" },
{ host = "${var.host_home_path}/.zsh_history", container = "/home/coder/.zsh_history" },
{ host = "${var.host_home_path}/.p10k.zsh", container = "/home/coder/.p10k.zsh" },
{ host = "${var.host_home_path}/.claude", container = "/home/coder/.claude" },
{ host = "${var.host_home_path}/.codex", container = "/home/coder/.codex" },
{ host = "${var.host_home_path}/.1password", container = "/home/coder/.1password" },
{ host = "${var.host_home_path}/.config", container = "/home/coder/.config" },
{ host = "${var.host_home_path}", container = "/home/coder", read_only = false },
{ host = "${var.host_home_path}/.local", container = "/home/coder/.local" },
{ host = "${var.host_home_path}/.cache", container = "/home/coder/.cache" },
{ host = "${var.host_home_path}/.docker/config.json", container = "/home/coder/.docker/config.json" },
{ host = "${var.host_home_path}/code-tools", container = "/home/coder/code-tools" },
{ host = "${var.host_home_path}/claude-scripts", container = "/home/coder/claude-scripts" },
]
workspace_id = data.coder_workspace.me.id
container_name = "coder-${local.workspace_id}"
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
project_repo_url = trimspace(data.coder_parameter.project_repository.value)
repo_clone_command = local.project_repo_url != "" ? "git clone ${local.project_repo_url} /workspaces" : "echo 'No repository requested'"
services_enabled = data.coder_parameter.enable_services.value
pgadmin_enabled = data.coder_parameter.enable_pgadmin.value
jupyter_enabled = data.coder_parameter.enable_jupyter.value
port_forwarding = local.services_enabled || local.jupyter_enabled
postgres_url = "postgresql://postgres:${var.postgres_password}@postgres-${local.workspace_id}:5432/postgres"
redis_url = "redis://:${var.redis_password}@redis-${local.workspace_id}:6379"
qdrant_url = "http://qdrant-${local.workspace_id}:6333"
agent_startup = join("\n", compact([
"set -eu",
"export CODER_WORKSPACE_ID=${local.workspace_id}",
"git config --global user.name \"${local.git_author_name}\"",
"git config --global user.email \"${local.git_author_email}\"",
local.project_repo_url != "" ? "if [ ! -d /workspaces/.git ]; then ${local.repo_clone_command}; fi" : "",
"export ENABLE_PGADMIN=${tostring(local.pgadmin_enabled)}",
"export ENABLE_JUPYTER=${tostring(local.jupyter_enabled)}",
local.port_forwarding ? "bash /home/coder/code-tools/tf/scripts/port-forward.sh" : "echo 'No service port forwarding requested'"
]))
}
# Workspace network keeps the workspace stack isolated from the host.
resource "docker_network" "workspace" {
name = "coder-${local.workspace_id}"
driver = "bridge"
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
}
# Persistent workspace data volume mounted at /workspaces inside the container.
resource "docker_volume" "workspaces" {
name = "workspaces-${local.workspace_id}"
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
labels {
label = "coder.type"
value = "workspace-data"
}
}
# Separate persistent home directory for the coder user.
# Base development container image (customise via terraform.tfvars).
resource "docker_image" "devcontainer" {
name = var.devcontainer_image
keep_locally = true
}

46
tf-dockerfile/outputs.tf Normal file
View File

@@ -0,0 +1,46 @@
output "workspace_id" {
description = "Coder workspace ID"
value = local.workspace_id
}
output "workspace_name" {
description = "Coder workspace name"
value = data.coder_workspace.me.name
}
output "container_name" {
description = "Name of the workspace Docker container"
value = local.container_name
}
output "project_repository" {
description = "Repository cloned into /workspaces on first startup"
value = local.project_repo_url
}
output "postgres_url" {
description = "Internal PostgreSQL connection string"
value = local.services_enabled ? local.postgres_url : null
sensitive = true
}
output "redis_url" {
description = "Internal Redis connection string"
value = local.services_enabled ? local.redis_url : null
sensitive = true
}
output "qdrant_url" {
description = "Internal Qdrant endpoint"
value = local.services_enabled ? local.qdrant_url : null
}
output "docker_network_name" {
description = "Docker network assigned to this workspace"
value = docker_network.workspace.name
}
output "workspace_volume_name" {
description = "Docker volume used for /workspaces"
value = docker_volume.workspaces.name
}

80
tf-dockerfile/scripts.tf Normal file
View File

@@ -0,0 +1,80 @@
locals {
core_scripts = {
workspace = {
display = "Setup Development Workspace"
icon = "/icon/container.svg"
script = "/usr/local/bin/workspace-setup.sh"
order = 1
blocks_login = true
}
dev_tools = {
display = "Install Development Tools"
icon = "/icon/code.svg"
script = "/usr/local/bin/dev-tools.sh"
order = 2
blocks_login = true
}
git_hooks = {
display = "Configure Git Hooks"
icon = "/icon/git.svg"
path = "${path.module}/scripts/git-hooks.sh"
order = 3
blocks_login = false
}
}
ai_scripts = {
claude = {
enabled = data.coder_parameter.enable_ai_tools.value && var.install_claude_code
display = "Install Claude CLI"
icon = "/icon/claude.svg"
script = "/usr/local/bin/claude-install.sh"
blocks_login = false
}
codex = {
enabled = data.coder_parameter.enable_ai_tools.value && var.install_codex_support
display = "Install Codex CLI"
icon = "/icon/code.svg"
script = "/usr/local/bin/codex-setup.sh"
blocks_login = false
}
cursor = {
enabled = data.coder_parameter.enable_ai_tools.value && var.install_cursor_support
display = "Configure Cursor"
icon = "/icon/cursor.svg"
script = "/usr/local/bin/cursor-setup.sh"
blocks_login = false
}
windsurf = {
enabled = data.coder_parameter.enable_ai_tools.value && var.install_windsurf_support
display = "Configure Windsurf"
icon = "/icon/windsurf.svg"
script = "/usr/local/bin/windsurf-setup.sh"
blocks_login = false
}
}
}
resource "coder_script" "core" {
for_each = local.core_scripts
agent_id = coder_agent.main.id
display_name = each.value.display
icon = each.value.icon
run_on_start = true
start_blocks_login = each.value.blocks_login
script = lookup(each.value, "script", null) != null ? "bash ${each.value.script}" : "echo '${base64encode(file(each.value.path))}' | base64 -d | tr -d '\\r' | bash"
}
resource "coder_script" "ai" {
for_each = { for key, value in local.ai_scripts : key => value if value.enabled }
agent_id = coder_agent.main.id
display_name = each.value.display
icon = each.value.icon
run_on_start = true
start_blocks_login = each.value.blocks_login
script = "bash ${each.value.script}"
}

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_DIR="/workspaces"
HOOK_DIR="$REPO_DIR/.git/hooks"
META_DIR="/tmp/git-metadata"
if [[ ! -d "$REPO_DIR/.git" ]]; then
echo "No Git repository found in $REPO_DIR; skipping hook install"
exit 0
fi
mkdir -p "$HOOK_DIR" "$META_DIR"
cat <<'HOOK' > "$HOOK_DIR/post-commit"
#!/usr/bin/env bash
set -e
META_DIR=/tmp/git-metadata
mkdir -p "$META_DIR"
git branch --show-current > "$META_DIR/current-branch" 2>/dev/null || echo "main" > "$META_DIR/current-branch"
git rev-parse HEAD > "$META_DIR/commit-hash" 2>/dev/null || echo "unknown" > "$META_DIR/commit-hash"
git remote get-url origin > "$META_DIR/remote-url" 2>/dev/null || echo "no-remote" > "$META_DIR/remote-url"
HOOK
chmod +x "$HOOK_DIR/post-commit"
echo "Git post-commit hook installed for metadata capture."

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
WORKSPACE_ID="${CODER_WORKSPACE_ID:-}"
if [[ -z "${WORKSPACE_ID}" && -f /tmp/git-metadata/workspace-id ]]; then
WORKSPACE_ID="$(cat /tmp/git-metadata/workspace-id)"
fi
if [[ -z "${WORKSPACE_ID}" ]]; then
echo "Unable to determine CODER_WORKSPACE_ID; skipping port forwarding" >&2
exit 0
fi
SERVICES_ENABLED="${ENABLE_SERVICES:-false}"
PGADMIN_ENABLED="${ENABLE_PGADMIN:-false}"
JUPYTER_ENABLED="${ENABLE_JUPYTER:-false}"
if ! command -v socat >/dev/null 2>&1; then
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y socat >/dev/null
elif command -v apk >/dev/null 2>&1; then
sudo apk add --no-cache socat >/dev/null
else
echo "socat is required for port forwarding but could not be installed automatically" >&2
exit 0
fi
fi
# stop previous forwards if they exist
pkill -f "socat.*pgadmin" >/dev/null 2>&1 || true
pkill -f "socat.*qdrant" >/dev/null 2>&1 || true
pkill -f "socat.*jupyter" >/dev/null 2>&1 || true
if [[ "${SERVICES_ENABLED}" == "true" ]]; then
if [[ "${PGADMIN_ENABLED}" == "true" ]]; then
echo "Forwarding pgAdmin to localhost:5050"
nohup socat TCP-LISTEN:5050,reuseaddr,fork TCP:pgadmin-${WORKSPACE_ID}:80 >/tmp/socat-pgadmin.log 2>&1 &
else
echo "pgAdmin disabled; skipping port forward"
fi
echo "Forwarding Qdrant to localhost:6333"
nohup socat TCP-LISTEN:6333,reuseaddr,fork TCP:qdrant-${WORKSPACE_ID}:6333 >/tmp/socat-qdrant.log 2>&1 &
else
echo "Database services disabled; skipping pgAdmin/Qdrant forwards"
fi
if [[ "${JUPYTER_ENABLED}" == "true" ]]; then
echo "Forwarding JupyterLab to localhost:8888"
nohup socat TCP-LISTEN:8888,reuseaddr,fork TCP:jupyter-${WORKSPACE_ID}:8888 >/tmp/socat-jupyter.log 2>&1 &
else
echo "JupyterLab disabled; skipping port forward"
fi
sleep 2
ps -o pid,cmd -C socat || true

296
tf-dockerfile/services.tf Normal file
View File

@@ -0,0 +1,296 @@
# Data services run inside the per-workspace Docker network. They stay optional
# so light-weight workspaces can skip all of them.
resource "docker_volume" "postgres_data" {
count = local.services_enabled ? 1 : 0
name = "postgres-data-${local.workspace_id}"
labels {
label = "coder.service"
value = "postgres"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}
resource "docker_container" "postgres" {
count = local.services_enabled ? 1 : 0
image = "postgres:${var.postgres_version}-alpine"
name = "postgres-${local.workspace_id}"
env = [
"POSTGRES_DB=postgres",
"POSTGRES_USER=postgres",
"POSTGRES_PASSWORD=${var.postgres_password}",
"POSTGRES_INITDB_ARGS=--auth-local=trust --auth-host=md5",
"POSTGRES_SHARED_PRELOAD_LIBRARIES=pg_stat_statements",
"POSTGRES_MAX_CONNECTIONS=${var.postgres_max_connections}"
]
networks_advanced {
name = docker_network.workspace.name
}
volumes {
volume_name = docker_volume.postgres_data[0].name
container_path = "/var/lib/postgresql/data"
}
healthcheck {
test = ["CMD-SHELL", "pg_isready -U postgres"]
interval = "15s"
timeout = "5s"
retries = 5
start_period = "30s"
}
restart = "unless-stopped"
labels {
label = "coder.service"
value = "postgres"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}
resource "docker_volume" "redis_data" {
count = local.services_enabled ? 1 : 0
name = "redis-data-${local.workspace_id}"
labels {
label = "coder.service"
value = "redis"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}
resource "docker_container" "redis" {
count = local.services_enabled ? 1 : 0
image = "redis:${var.redis_version}-alpine"
name = "redis-${local.workspace_id}"
command = [
"redis-server",
"--requirepass", var.redis_password,
"--appendonly", "yes",
"--appendfsync", "everysec",
"--maxmemory", var.redis_max_memory,
"--maxmemory-policy", "allkeys-lru"
]
networks_advanced {
name = docker_network.workspace.name
}
volumes {
volume_name = docker_volume.redis_data[0].name
container_path = "/data"
}
healthcheck {
test = ["CMD", "redis-cli", "-a", var.redis_password, "ping"]
interval = "15s"
timeout = "3s"
retries = 5
start_period = "10s"
}
restart = "unless-stopped"
labels {
label = "coder.service"
value = "redis"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}
resource "docker_volume" "qdrant_data" {
count = local.services_enabled ? 1 : 0
name = "qdrant-data-${local.workspace_id}"
labels {
label = "coder.service"
value = "qdrant"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}
resource "docker_container" "qdrant" {
count = local.services_enabled ? 1 : 0
image = "qdrant/qdrant:${var.qdrant_version}"
name = "qdrant-${local.workspace_id}"
env = [
"QDRANT__SERVICE__HTTP_PORT=6333",
"QDRANT__SERVICE__GRPC_PORT=6334",
"QDRANT__SERVICE__HOST=0.0.0.0",
"QDRANT__LOG_LEVEL=INFO"
]
networks_advanced {
name = docker_network.workspace.name
}
volumes {
volume_name = docker_volume.qdrant_data[0].name
container_path = "/qdrant/storage"
}
healthcheck {
test = ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:6333/health || exit 1"]
interval = "20s"
timeout = "5s"
retries = 5
start_period = "40s"
}
restart = "unless-stopped"
labels {
label = "coder.service"
value = "qdrant"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}
resource "docker_volume" "pgadmin_data" {
count = local.services_enabled && data.coder_parameter.enable_pgadmin.value ? 1 : 0
name = "pgadmin-data-${local.workspace_id}"
labels {
label = "coder.service"
value = "pgadmin"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}
resource "docker_container" "pgadmin" {
count = local.services_enabled && data.coder_parameter.enable_pgadmin.value ? 1 : 0
image = "dpage/pgadmin4:latest"
name = "pgadmin-${local.workspace_id}"
env = [
"PGADMIN_DEFAULT_EMAIL=${var.pgadmin_email}",
"PGADMIN_DEFAULT_PASSWORD=${var.pgadmin_password}",
"PGADMIN_CONFIG_SERVER_MODE=False",
"PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False",
"PGADMIN_LISTEN_PORT=80"
]
networks_advanced {
name = docker_network.workspace.name
}
volumes {
volume_name = docker_volume.pgadmin_data[0].name
container_path = "/var/lib/pgadmin"
}
healthcheck {
test = ["CMD-SHELL", "nc -z localhost 80 || exit 1"]
interval = "30s"
timeout = "10s"
retries = 3
start_period = "60s"
}
restart = "unless-stopped"
labels {
label = "coder.service"
value = "pgadmin"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}
resource "docker_volume" "jupyter_data" {
count = data.coder_parameter.enable_jupyter.value ? 1 : 0
name = "jupyter-data-${local.workspace_id}"
labels {
label = "coder.service"
value = "jupyter"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}
resource "docker_container" "jupyter" {
count = data.coder_parameter.enable_jupyter.value ? 1 : 0
image = "jupyter/scipy-notebook:latest"
name = "jupyter-${local.workspace_id}"
env = [
"JUPYTER_ENABLE_LAB=yes",
"JUPYTER_TOKEN=",
"RESTARTABLE=yes",
"JUPYTER_PORT=8888"
]
networks_advanced {
name = docker_network.workspace.name
}
volumes {
volume_name = docker_volume.jupyter_data[0].name
container_path = "/home/jovyan/work"
}
volumes {
volume_name = docker_volume.workspaces.name
container_path = "/home/jovyan/workspaces"
}
healthcheck {
test = ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8888"]
interval = "30s"
timeout = "10s"
retries = 5
}
restart = "unless-stopped"
labels {
label = "coder.service"
value = "jupyter"
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
}

View File

@@ -0,0 +1,8 @@
devcontainer_image = "mcr.microsoft.com/devcontainers/universal:2-linux"
workspace_memory_limit = 8192
postgres_password = "devpassword"
redis_password = "devpassword"
postgres_max_connections = 100
redis_max_memory = "512mb"
pgadmin_email = "admin@dev.local"
pgadmin_password = "adminpassword"

116
tf-dockerfile/variables.tf Normal file
View File

@@ -0,0 +1,116 @@
variable "host_home_path" {
description = "Absolute path to the host home directory for bind mounts."
type = string
default = "/home/trav"
}
variable "docker_socket" {
description = "Docker daemon socket URI (leave blank for default)."
type = string
default = ""
}
variable "devcontainer_image" {
description = "Container image used for the main workspace."
type = string
default = "git.lab/vasceannie/code-tools:golden-devcontainer"
}
variable "workspace_memory_limit" {
description = "Workspace memory limit in MB. Use 0 to inherit the image defaults."
type = number
default = 8192
}
variable "enable_docker_in_docker" {
description = "Mount /var/run/docker.sock into the workspace container."
type = bool
default = true
}
variable "block_file_transfer" {
description = "Set CODER_AGENT_BLOCK_FILE_TRANSFER=1 to disable file transfer tooling."
type = bool
default = false
}
variable "postgres_version" {
description = "PostgreSQL image tag."
type = string
default = "17"
}
variable "postgres_password" {
description = "PostgreSQL password for the postgres user."
type = string
default = "devpassword"
sensitive = true
}
variable "postgres_max_connections" {
description = "Maximum PostgreSQL connections."
type = number
default = 100
}
variable "redis_version" {
description = "Redis image tag."
type = string
default = "7"
}
variable "redis_password" {
description = "Redis AUTH password."
type = string
default = "devpassword"
sensitive = true
}
variable "redis_max_memory" {
description = "Redis maxmemory value (e.g. 256mb)."
type = string
default = "512mb"
}
variable "qdrant_version" {
description = "Qdrant image tag."
type = string
default = "latest"
}
variable "pgadmin_email" {
description = "pgAdmin login email."
type = string
default = "admin@dev.local"
}
variable "pgadmin_password" {
description = "pgAdmin login password."
type = string
default = "adminpassword"
sensitive = true
}
variable "install_claude_code" {
description = "Install the Claude CLI helper when AI tooling is enabled."
type = bool
default = true
}
variable "install_cursor_support" {
description = "Install Cursor configuration when AI tooling is enabled."
type = bool
default = true
}
variable "install_windsurf_support" {
description = "Install Windsurf configuration when AI tooling is enabled."
type = bool
default = true
}
variable "install_codex_support" {
description = "Install OpenAI Codex CLI when AI tooling is enabled."
type = bool
default = true
}

225
tf-dockerfile/workspace.tf Normal file
View File

@@ -0,0 +1,225 @@
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
dir = "/workspaces"
env = {
"GIT_AUTHOR_NAME" = local.git_author_name
"GIT_AUTHOR_EMAIL" = local.git_author_email
"CODER_WORKSPACE_ID" = local.workspace_id
"CODER_WORKSPACE_REPO" = local.project_repo_url
"POSTGRES_URL" = local.services_enabled ? local.postgres_url : ""
"REDIS_URL" = local.services_enabled ? local.redis_url : ""
"QDRANT_URL" = local.services_enabled ? local.qdrant_url : ""
"ENABLE_PGADMIN" = tostring(local.pgadmin_enabled)
"ENABLE_JUPYTER" = tostring(local.jupyter_enabled)
"ENABLE_SERVICES" = tostring(local.services_enabled)
"CODER_AGENT_BLOCK_FILE_TRANSFER" = var.block_file_transfer ? "1" : ""
}
startup_script = local.agent_startup
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu 2>/dev/null || echo 'n/a'"
interval = 30
timeout = 5
}
metadata {
display_name = "Memory Usage"
key = "1_memory_usage"
script = "coder stat mem 2>/dev/null || echo 'n/a'"
interval = 30
timeout = 5
}
metadata {
display_name = "Disk Usage"
key = "2_disk_usage"
script = "df -h /workspaces 2>/dev/null | awk 'NR==2 {print $5}' || echo 'n/a'"
interval = 300
timeout = 10
}
metadata {
display_name = "Git Branch"
key = "3_git_branch"
script = "cd /workspaces && git branch --show-current 2>/dev/null || echo 'no-repo'"
interval = 300
timeout = 5
}
metadata {
display_name = "PostgreSQL"
key = "4_postgres"
script = local.services_enabled ? format("pg_isready -h postgres-%s -p 5432 -U postgres >/dev/null && echo healthy || echo down", local.workspace_id) : "echo 'disabled'"
interval = 60
timeout = 5
}
metadata {
display_name = "Redis"
key = "5_redis"
script = local.services_enabled ? format("redis-cli -h redis-%s -a %s ping 2>/dev/null | grep -qi pong && echo healthy || echo down", local.workspace_id, var.redis_password) : "echo 'disabled'"
interval = 60
timeout = 5
}
metadata {
display_name = "Qdrant"
key = "6_qdrant"
script = local.services_enabled ? format("wget --no-verbose --tries=1 --spider http://qdrant-%s:6333/health 2>/dev/null && echo healthy || echo down", local.workspace_id) : "echo 'disabled'"
interval = 60
timeout = 5
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = docker_image.devcontainer.image_id
name = local.container_name
hostname = data.coder_workspace.me.name
memory = var.workspace_memory_limit > 0 ? var.workspace_memory_limit * 1024 * 1024 : null
env = compact([
"GIT_AUTHOR_NAME=${local.git_author_name}",
"GIT_AUTHOR_EMAIL=${local.git_author_email}",
"CODER_AGENT_TOKEN=${coder_agent.main.token}",
"CODER_AGENT_DEVCONTAINERS_ENABLE=true",
local.project_repo_url != "" ? "CODER_WORKSPACE_REPO=${local.project_repo_url}" : "",
])
networks_advanced {
name = docker_network.workspace.name
}
host {
host = "host.docker.internal"
ip = "host-gateway"
}
volumes {
container_path = "/workspaces"
volume_name = docker_volume.workspaces.name
}
dynamic "volumes" {
for_each = local.bind_mounts
content {
host_path = volumes.value.host
container_path = volumes.value.container
read_only = try(volumes.value.read_only, false)
}
}
dynamic "volumes" {
for_each = var.enable_docker_in_docker ? [1] : []
content {
host_path = "/var/run/docker.sock"
container_path = "/var/run/docker.sock"
}
}
working_dir = "/workspaces"
command = ["/bin/bash", "-c", "${coder_agent.main.init_script} && sleep infinity"]
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
depends_on = [
docker_network.workspace,
docker_volume.workspaces,
docker_image.devcontainer
]
}
module "cursor_desktop" {
count = data.coder_workspace.me.start_count > 0 && data.coder_parameter.enable_ai_tools.value ? 1 : 0
source = "registry.coder.com/coder/cursor/coder"
agent_id = coder_agent.main.id
folder = "/workspaces"
group = "Desktop IDEs"
order = 40
}
module "windsurf_desktop" {
count = data.coder_workspace.me.start_count > 0 && data.coder_parameter.enable_ai_tools.value ? 1 : 0
source = "registry.coder.com/coder/windsurf/coder"
agent_id = coder_agent.main.id
folder = "/workspaces"
group = "Desktop IDEs"
order = 50
}
module "pycharm_desktop" {
count = data.coder_workspace.me.start_count > 0 && data.coder_parameter.enable_jetbrains.value ? 1 : 0
source = "registry.coder.com/coder/jetbrains-gateway/coder"
agent_id = coder_agent.main.id
folder = "/workspaces"
jetbrains_ides = ["PY"]
default = "PY"
group = "Desktop IDEs"
order = 60
coder_parameter_order = 6
slug = "pycharm-gateway"
}
module "claude_code" {
count = data.coder_workspace.me.start_count > 0 && data.coder_parameter.enable_ai_tools.value ? 1 : 0
source = "registry.coder.com/coder/claude-code/coder"
agent_id = coder_agent.main.id
workdir = "/workspaces"
group = "AI Tools"
order = 30
pre_install_script = <<-EOT
#!/bin/bash
set -euo pipefail
if curl --help 2>&1 | grep -q -- "--retry-all-errors"; then
exit 0
fi
if ! command -v apt-get >/dev/null 2>&1; then
exit 0
fi
wait_for_apt() {
if command -v fuser >/dev/null 2>&1; then
while \
fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 ||
fuser /var/lib/apt/lists/lock >/dev/null 2>&1 ||
fuser /var/lib/dpkg/lock >/dev/null 2>&1; do
echo "Waiting for apt locks to clear..." >&2
sleep 2
done
else
sleep 2
fi
}
apt_exec() {
if command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
"$@"
fi
}
export DEBIAN_FRONTEND=noninteractive
wait_for_apt
apt_exec apt-get update -qq
wait_for_apt
apt_exec apt-get install -y curl >/dev/null
EOT
}