x
This commit is contained in:
@@ -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
67
tf-dockerfile/.terraform.lock.hcl
generated
Normal 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
50
tf-dockerfile/README.md
Normal 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 [Coder’s 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
134
tf-dockerfile/apps.tf
Normal 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
193
tf-dockerfile/main.tf
Normal 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
46
tf-dockerfile/outputs.tf
Normal 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
80
tf-dockerfile/scripts.tf
Normal 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}"
|
||||
}
|
||||
27
tf-dockerfile/scripts/git-hooks.sh
Executable file
27
tf-dockerfile/scripts/git-hooks.sh
Executable 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."
|
||||
57
tf-dockerfile/scripts/port-forward.sh
Executable file
57
tf-dockerfile/scripts/port-forward.sh
Executable 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
296
tf-dockerfile/services.tf
Normal 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
|
||||
}
|
||||
}
|
||||
8
tf-dockerfile/terraform.tfvars
Normal file
8
tf-dockerfile/terraform.tfvars
Normal 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
116
tf-dockerfile/variables.tf
Normal 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
225
tf-dockerfile/workspace.tf
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user