Files
coder/dogfood/coder/main.tf
T
dependabot[bot] 9a0afb1c64 chore: bump the coder-modules group across 3 directories with 1 update (#28236)
Bumps the coder-modules group with 1 update in the /dogfood/coder
directory: coder/personalize/coder.
Bumps the coder-modules group with 1 update in the
/dogfood/coder-envbuilder directory: coder/personalize/coder.
Bumps the coder-modules group with 1 update in the /dogfood/vscode-coder
directory: coder/personalize/coder.

Updates `coder/personalize/coder` from 1.0.32 to 1.0.33

Updates `coder/personalize/coder` from 1.0.32 to 1.0.33

Updates `coder/personalize/coder` from 1.0.32 to 1.0.33


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 00:26:55 +00:00

1051 lines
34 KiB
Terraform

terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.13.0"
}
docker = {
source = "kreuzwerker/docker"
version = "~> 4.0"
}
}
}
// This module is a terraform no-op. It contains 5mb worth of files to test
// Coder's behavior dealing with larger modules. This is included to test
// protobuf message size limits and the performance of module loading.
//
// In reality, modules might have accidental bloat from non-terraform files such
// as images & documentation.
module "large-5mb-module" {
source = "git::https://github.com/coder/large-module.git"
}
locals {
// These are cluster service addresses mapped to Tailscale nodes. Ask Dean or
// Kyle for help.
docker_host = {
"" = "tcp://rubinsky-pit-cdr-dev.tailscale.svc.cluster.local:2375"
"us-pittsburgh" = "tcp://rubinsky-pit-cdr-dev.tailscale.svc.cluster.local:2375"
// For legacy reasons, this host is labelled `eu-helsinki` but it's
// actually in Germany now.
"eu-helsinki" = "tcp://katerose-fsn-cdr-dev.tailscale.svc.cluster.local:2375"
"ap-sydney" = "tcp://wolfgang-syd-cdr-dev.tailscale.svc.cluster.local:2375"
"za-cpt" = "tcp://schonkopf-cpt-cdr-dev.tailscale.svc.cluster.local:2375"
}
repo_base_dir = data.coder_parameter.repo_base_dir.value == "~" ? "/home/coder" : replace(data.coder_parameter.repo_base_dir.value, "/^~\\//", "/home/coder/")
repo_dir = replace(try(module.git-clone[0].repo_dir, ""), "/^~\\//", "/home/coder/")
container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
// Derive a stable per-workspace hour and minute from the workspace ID
// so that cache cleanup crons don't all hit the filesystem at once.
cache_cleanup_hour = parseint(substr(data.coder_workspace.me.id, 0, 2), 16) % 24
cache_cleanup_minute = parseint(substr(data.coder_workspace.me.id, 2, 2), 16) % 60
}
data "coder_workspace_preset" "pittsburgh" {
name = "Pittsburgh"
default = true
description = "Development workspace hosted in United States with 2 prebuild instances"
icon = "/emojis/1f1fa-1f1f8.png"
parameters = {
(data.coder_parameter.region.name) = "us-pittsburgh"
(data.coder_parameter.image_type.name) = data.coder_parameter.image_type.default
(data.coder_parameter.repo_base_dir.name) = "~"
}
prebuilds {
instances = 2
}
}
data "coder_workspace_preset" "cpt" {
name = "Cape Town"
description = "Development workspace hosted in South Africa with 1 prebuild instance"
icon = "/emojis/1f1ff-1f1e6.png"
parameters = {
(data.coder_parameter.region.name) = "za-cpt"
(data.coder_parameter.image_type.name) = data.coder_parameter.image_type.default
(data.coder_parameter.repo_base_dir.name) = "~"
}
prebuilds {
instances = 1
}
}
data "coder_workspace_preset" "falkenstein" {
name = "Falkenstein"
description = "Development workspace hosted in Europe with 1 prebuild instance"
icon = "/emojis/1f1ea-1f1fa.png"
parameters = {
(data.coder_parameter.region.name) = "eu-helsinki"
(data.coder_parameter.image_type.name) = data.coder_parameter.image_type.default
(data.coder_parameter.repo_base_dir.name) = "~"
}
prebuilds {
instances = 1
}
}
data "coder_workspace_preset" "sydney" {
name = "Sydney"
description = "Development workspace hosted in Australia with 1 prebuild instance"
icon = "/emojis/1f1e6-1f1fa.png"
parameters = {
(data.coder_parameter.region.name) = "ap-sydney"
(data.coder_parameter.image_type.name) = data.coder_parameter.image_type.default
(data.coder_parameter.repo_base_dir.name) = "~"
}
prebuilds {
instances = 1
}
}
locals {
image_tags = {
// Older style option values, where the option value was just supposed to
// be the exact name of the image on Docker hub. In practice, this is rather
// restrictive because the image_type parameter is immutable.
"codercom/oss-dogfood:latest" = "codercom/oss-dogfood:latest"
"ubuntu-latest" = "codercom/oss-dogfood:26.04"
}
}
data "coder_parameter" "repo_base_dir" {
type = "string"
name = "Coder Repository Base Directory"
default = "~"
description = "The directory specified will be created (if missing) and [coder/coder](https://github.com/coder/coder) will be automatically cloned into [base directory]/coder 🪄."
mutable = true
}
data "coder_parameter" "image_type" {
type = "string"
name = "Coder Image"
default = "ubuntu-latest"
description = "The Docker image used to run your workspace."
mutable = true
option {
icon = "/icon/coder.svg"
name = "Ubuntu 26.04"
value = "ubuntu-latest"
}
option {
icon = "/icon/coder.svg"
name = "Ubuntu 22.04 (Legacy)"
value = "codercom/oss-dogfood:latest"
}
}
locals {
default_regions = {
// keys should match group names
"north-america" : "us-pittsburgh"
"europe" : "eu-helsinki"
"australia" : "ap-sydney"
"africa" : "za-cpt"
}
user_groups = data.coder_workspace_owner.me.groups
user_region = coalescelist([
for g in local.user_groups :
local.default_regions[g] if contains(keys(local.default_regions), g)
], ["us-pittsburgh"])[0]
}
data "coder_parameter" "region" {
type = "string"
name = "Region"
icon = "/emojis/1f30e.png"
default = local.user_region
option {
icon = "/emojis/1f1fa-1f1f8.png"
name = "Pittsburgh"
value = "us-pittsburgh"
}
option {
icon = "/emojis/1f1e9-1f1ea.png"
name = "Falkenstein"
// For legacy reasons, this host is labelled `eu-helsinki` but it's
// actually in Germany now.
value = "eu-helsinki"
}
option {
icon = "/emojis/1f1e6-1f1fa.png"
name = "Sydney"
value = "ap-sydney"
}
option {
icon = "/emojis/1f1ff-1f1e6.png"
name = "Cape Town"
value = "za-cpt"
}
}
data "coder_parameter" "devcontainer_autostart" {
type = "bool"
name = "Automatically start devcontainer for coder/coder"
default = false
description = "If enabled, a devcontainer will be automatically started for the [coder/coder](https://github.com/coder/coder) repository."
mutable = true
}
data "coder_parameter" "enable_kvm" {
type = "bool"
name = "Expose /dev/kvm to the workspace"
default = false
description = "If enabled, the host's /dev/kvm device is mapped into the workspace container to allow hardware-accelerated VMs. Only works when the underlying host exposes /dev/kvm; leave disabled otherwise."
mutable = true
}
# dogfood/main.tf injects this value from a GH Actions secret;
# `coderd_template.dogfood` passes the value injected by .github/workflows/dogfood.yaml in `TF_VAR_CODER_DOGFOOD_ANTHROPIC_API_KEY` and `TF_VAR_CODER_DOGFOOD_OPENAI_API_KEY`.
# Currently unused since AI Gateway is always enabled, but kept for emergency fallback.
variable "anthropic_api_key" {
type = string
description = "The API key used to authenticate with the Anthropic API, if AI Gateway is disabled."
default = ""
sensitive = true
}
variable "openai_api_key" {
type = string
description = "The API key used to authenticate with the OpenAI API, if AI Gateway is disabled."
default = ""
sensitive = true
}
provider "docker" {
host = lookup(local.docker_host, data.coder_parameter.region.value)
}
provider "coder" {}
data "coder_external_auth" "github" {
id = "github"
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_workspace_tags" "tags" {
tags = {
"cluster" : "dogfood-v2"
"env" : "gke"
}
}
data "coder_workspace_tags" "prebuild" {
count = data.coder_workspace_owner.me.name == "prebuilds" ? 1 : 0
tags = {
"is_prebuild" = "true"
}
}
data "coder_parameter" "ide_choices" {
type = "list(string)"
name = "Select IDEs"
form_type = "multi-select"
mutable = true
description = "Choose one or more IDEs to enable in your workspace"
default = jsonencode(["vscode", "code-server", "cursor"])
option {
name = "VS Code Desktop"
value = "vscode"
icon = "/icon/code.svg"
}
option {
name = "code-server"
value = "code-server"
icon = "/icon/code.svg"
}
option {
name = "VS Code Web"
value = "vscode-web"
icon = "/icon/code.svg"
}
option {
name = "JetBrains IDEs"
value = "jetbrains"
icon = "/icon/jetbrains.svg"
}
option {
name = "Cursor"
value = "cursor"
icon = "/icon/cursor.svg"
}
option {
name = "Windsurf"
value = "windsurf"
icon = "/icon/windsurf.svg"
}
option {
name = "Zed"
value = "zed"
icon = "/icon/zed.svg"
}
}
module "slackme" {
count = data.coder_workspace.me.start_count
source = "dev.registry.coder.com/coder/slackme/coder"
version = "1.0.33"
agent_id = coder_agent.dev.id
auth_provider_id = "slack"
}
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "dev.registry.coder.com/coder/dotfiles/coder"
version = "1.4.2"
agent_id = coder_agent.dev.id
}
module "git-config" {
count = data.coder_workspace.me.start_count
source = "dev.registry.coder.com/coder/git-config/coder"
version = "1.0.34"
agent_id = coder_agent.dev.id
# If you prefer to commit with a different email, this allows you to do so.
allow_email_change = true
}
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "dev.registry.coder.com/coder/git-clone/coder"
version = "1.3.0"
agent_id = coder_agent.dev.id
url = "https://github.com/coder/coder"
base_dir = local.repo_base_dir
post_clone_script = <<-EOT
#!/usr/bin/env bash
set -eux -o pipefail
coder exp sync start git-clone
coder exp sync complete git-clone
EOT
}
module "personalize" {
count = data.coder_workspace.me.start_count
source = "dev.registry.coder.com/coder/personalize/coder"
version = "1.0.33"
agent_id = coder_agent.dev.id
}
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.5.0"
agent_id = coder_agent.dev.id
subdomain = true
display_name = "Mux"
add_project = local.repo_dir
install_version = "next"
package_manager = "bun"
restart_on_kill = true
max_restart_attempts = 10
}
module "code-server" {
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "code-server") ? data.coder_workspace.me.start_count : 0
source = "dev.registry.coder.com/coder/code-server/coder"
version = "1.5.2"
agent_id = coder_agent.dev.id
folder = local.repo_dir
auto_install_extensions = true
group = "Web Editors"
}
module "vscode-web" {
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "vscode-web") ? data.coder_workspace.me.start_count : 0
source = "dev.registry.coder.com/coder/vscode-web/coder"
version = "1.6.1"
agent_id = coder_agent.dev.id
folder = local.repo_dir
extensions = ["github.copilot"]
auto_install_extensions = true # will install extensions from the repos .vscode/extensions.json file
accept_license = true
group = "Web Editors"
}
module "jetbrains" {
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "jetbrains") ? data.coder_workspace.me.start_count : 0
source = "dev.registry.coder.com/coder/jetbrains/coder"
version = "1.4.0"
agent_id = coder_agent.dev.id
agent_name = "dev"
folder = local.repo_dir
major_version = "latest"
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
}
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "dev.registry.coder.com/coder/filebrowser/coder"
version = "1.1.5"
agent_id = coder_agent.dev.id
agent_name = "dev"
}
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "dev.registry.coder.com/coder/coder-login/coder"
version = "1.1.1"
agent_id = coder_agent.dev.id
}
module "cursor" {
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "cursor") ? data.coder_workspace.me.start_count : 0
source = "dev.registry.coder.com/coder/cursor/coder"
version = "1.4.1"
agent_id = coder_agent.dev.id
folder = local.repo_dir
}
module "windsurf" {
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "windsurf") ? data.coder_workspace.me.start_count : 0
source = "dev.registry.coder.com/coder/windsurf/coder"
version = "1.3.1"
agent_id = coder_agent.dev.id
folder = local.repo_dir
}
module "zed" {
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "zed") ? data.coder_workspace.me.start_count : 0
source = "dev.registry.coder.com/coder/zed/coder"
version = "1.1.5"
agent_id = coder_agent.dev.id
agent_name = "dev"
folder = local.repo_dir
}
module "devcontainers-cli" {
count = data.coder_workspace.me.start_count
source = "dev.registry.coder.com/modules/devcontainers-cli/coder"
version = ">= 1.0.0"
agent_id = coder_agent.dev.id
}
module "portabledesktop" {
source = "dev.registry.coder.com/coder/portabledesktop/coder"
version = "0.1.0"
agent_id = coder_agent.dev.id
}
resource "coder_agent" "dev" {
arch = "amd64"
os = "linux"
dir = local.repo_dir
env = merge(
{
OIDC_TOKEN : data.coder_workspace_owner.me.oidc_access_token,
# `mise oci build` bakes `ENV MISE_CONFIG_DIR=/etc/mise` into
# the image layer above Dockerfile.base, so mise treats
# /etc/mise as the user config dir and never reads
# ~/.config/mise/conf.d/*, silently dropping the trust file
# the install-deps coder_script below seeds. `[oci.env]` in
# mise.toml would be the natural place for this, but mise's
# internal env bake currently wins on MISE_* key collisions
# (non-MISE keys flow through). Move this back to `[oci.env]`
# once upstream mise fixes that.
MISE_CONFIG_DIR : "/home/coder/.config/mise",
# Keep user-installed mise tools on the persistent home volume.
# The image still exposes baked tools from /opt/mise/data via
# MISE_SHARED_INSTALL_DIRS, but /opt itself is image-resident
# and is recreated with the container on workspace restart.
MISE_DATA_DIR : "/home/coder/.local/share/mise",
},
{
ANTHROPIC_BASE_URL : "https://dev.coder.com/api/v2/ai-gateway/anthropic",
ANTHROPIC_AUTH_TOKEN : data.coder_workspace_owner.me.session_token,
OPENAI_BASE_URL : "https://dev.coder.com/api/v2/ai-gateway/openai/v1",
OPENAI_API_KEY : data.coder_workspace_owner.me.session_token,
}
)
startup_script_behavior = "blocking"
display_apps {
vscode = contains(jsondecode(data.coder_parameter.ide_choices.value), "vscode")
vscode_insiders = false
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
metadata {
display_name = "CPU Usage"
key = "cpu_usage"
order = 0
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "ram_usage"
order = 1
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "/home Usage"
key = "home_usage"
order = 2
script = "sudo du -sh /home/coder | awk '{print $1}'"
interval = 3600 # 1h to avoid thrashing disk
timeout = 60 # Longer than this is likely problematic
}
metadata {
display_name = "/var/lib/docker Usage"
key = "var_lib_docker_usage"
order = 3
script = "sudo du -sh /var/lib/docker 2>/dev/null | awk '{print $1}'"
interval = 3600 # 1h to avoid thrashing disk
timeout = 60 # Longer than this is likely problematic
}
metadata {
display_name = "Word of the Day"
key = "word"
order = 4
script = <<EOT
#!/usr/bin/env bash
curl -o - --silent https://www.merriam-webster.com/word-of-the-day 2>&1 | awk ' $0 ~ "Word of the Day: [A-z]+" { print $5; exit }'
EOT
interval = 86400
timeout = 5
}
resources_monitoring {
memory {
enabled = true
threshold = 80
}
volume {
enabled = true
threshold = 90
path = "/home/coder"
}
volume {
enabled = true
threshold = 90
path = "/var/lib/docker"
}
}
startup_script = <<-EOT
#!/usr/bin/env bash
set -eux -o pipefail
# Allow other scripts to wait for agent startup.
function cleanup() {
coder exp sync complete agent-startup
# Some folks will also use this for their personalize scripts.
touch /tmp/.coder-startup-script.done
}
trap cleanup EXIT
coder exp sync start agent-startup
# Authenticate GitHub CLI. `gh api user` is used instead of `gh auth
# status` because the latter exits non-zero when a stale token exists
# in ~/.config/gh/hosts.yml, even when a valid GITHUB_TOKEN is already
# present in the environment and gh commands work fine.
if ! gh api user --jq .login >/dev/null 2>&1; then
echo "Logging into GitHub CLI…"
if ! coder external-auth access-token github | gh auth login --hostname github.com --with-token; then
echo "GitHub CLI authentication failed; gh commands may not work."
fi
else
echo "GitHub CLI already has working credentials."
fi
# Configure Mux GitHub owner login for browser access (skip if
# already set). See: https://mux.coder.com/config/server-access
if [ ! -f ~/.mux/config.json ] || ! jq -e '.serverAuthGithubOwner' ~/.mux/config.json >/dev/null 2>&1; then
GH_USER=$(gh api user --jq .login 2>/dev/null || true)
if [ -n "$GH_USER" ]; then
mkdir -p ~/.mux
if [ -f ~/.mux/config.json ]; then
jq --arg owner "$GH_USER" '. + {serverAuthGithubOwner: $owner}' ~/.mux/config.json > /tmp/mux-config.json && mv /tmp/mux-config.json ~/.mux/config.json
else
jq -n --arg owner "$GH_USER" '{serverAuthGithubOwner: $owner}' > ~/.mux/config.json
fi
echo "Configured Mux GitHub owner login: $GH_USER"
fi
fi
# Increase the shutdown timeout of the docker service for improved cleanup.
# The 240 was picked as it's lower than the 300 seconds we set for the
# container shutdown grace period.
sudo sh -c 'jq ". += {\"shutdown-timeout\": 240}" /etc/docker/daemon.json > /tmp/daemon.json.new && mv /tmp/daemon.json.new /etc/docker/daemon.json'
# Start Docker service
sudo service docker start
EOT
shutdown_script = <<-EOT
#!/usr/bin/env bash
set -eux -o pipefail
# Clean up the Go build cache to prevent the home volume from
# accumulating waste and growing too large.
go clean -cache
# Clean up the coder build directory as this can get quite large
rm -rf "${local.repo_dir}/build"
# Clean up the unused resources to keep storage usage low.
#
# WARNING! This will remove:
# - all stopped containers
# - all networks not used by at least one container
# - all images without at least one container associated to them
# - all build cache
docker system prune -a -f
# Remove dangling named volumes that are older than KEEP_DAYS. Using
# 30 here as a conservative default (vacation, holidays, etc.).
KEEP_DAYS=30
docker volume ls -qf dangling=true \
| xargs -r docker volume inspect \
| jq -r --argjson days "$KEEP_DAYS" '.[] | select(.CreatedAt != null) | ((now - (.CreatedAt | fromdateiso8601)) / 86400 | floor) as $a | select($a >= $days) | "\($a)\t\(.Name)"' \
| while IFS=$'\t' read -r age name; do
echo "Removing volume $name ($age d)"
docker volume rm "$name" >/dev/null
done
# Stop the Docker service to prevent errors during workspace destroy.
sudo service docker stop
EOT
}
resource "coder_script" "install-deps" {
agent_id = coder_agent.dev.id
display_name = "Installing Dependencies"
run_on_start = true
start_blocks_login = false
script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
trap 'coder exp sync complete install-deps' EXIT
# Ensure /opt/mise is writable by coder before any login shell
# or other script touches mise. `mise oci build` emits its tar
# layers in deterministic mode with hardcoded uid=0/gid=0 (see
# mise's src/oci/layer.rs), and `prefix_parents` walks the full
# mount_point chain, so the final image stamps /opt, /opt/mise,
# and /opt/mise/data as root:root regardless of what the base
# Dockerfile sets. Without this chown mise warns `migrate:
# failed create_dir_all: /opt/mise/data/migrations` and skips
# its state writes. /opt/mise is image-resident (not on the
# home volume), so this runs every workspace start. Runs
# before the git-clone sync barrier so early shells never
# observe the unwritable state.
sudo chown -R coder:coder /opt/mise
coder exp sync want install-deps git-clone
coder exp sync start install-deps
# Seed a user-owned mise trust config on the persistent home
# volume. The image ships /etc/mise/conf.d/00-coder-trust.toml as
# a fallback, but mise's `trusted_config_paths` setting doesn't
# merge across config layers, so anything the user adds to their
# own file would otherwise replace the system fallback. Writing
# this file once (if-absent) seeds the defaults under
# ~/.config/mise/conf.d/ where the user can edit it and the edits
# survive workspace restart.
TRUST_FILE="$HOME/.config/mise/conf.d/00-coder-trust.toml"
if [ ! -f "$TRUST_FILE" ]; then
mkdir -p "$(dirname "$TRUST_FILE")"
cat > "$TRUST_FILE" <<'TRUST'
# mise trust paths for the dogfood workspace. Edit to add your own
# paths; this file lives on the persistent home volume so changes
# survive workspace restart. The install-deps coder_script only
# writes this file when it's absent.
[settings]
trusted_config_paths = [
"/home/coder/coder",
"/etc/mise",
]
TRUST
fi
# Install playwright dependencies
# We want to use the playwright version from site/package.json
cd "${local.repo_dir}" && make clean
cd "${local.repo_dir}/site" && pnpm install
# Two playwright installs: site/'s @playwright/test and
# @playwright/mcp@0.0.75 bundle different playwright-core versions
# with different chromium revisions, and both are used at runtime
# (site tests + the claude-code/codex MCP servers below).
cd "${local.repo_dir}/site" && pnpm exec playwright install chromium
npx --yes --package=@playwright/mcp@0.0.75 playwright-core install --no-shell chromium
# Keep this version pinned because the dashboard is embeddable only while
# it omits X-Frame-Options and CSP headers. This is not a documented
# contract, so verify those headers before updating.
npm install -g agent-browser@0.33.2
agent-browser install
# Keep the agent skill aligned with the installed CLI version.
skill_src="$(npm root -g)/agent-browser/skills/agent-browser"
for skill_dir in "$HOME/.claude/skills" "$HOME/.agents/skills"; do
mkdir -p "$skill_dir"
rm -rf "$skill_dir/agent-browser"
cp -r "$skill_src" "$skill_dir/agent-browser"
done
agent-browser dashboard start
EOT
}
resource "coder_script" "go-cache-cleanup-cron" {
agent_id = coder_agent.dev.id
display_name = "Go Build Cache Cleanup Cron"
icon = "${data.coder_workspace.me.access_url}/emojis/1f9f9.png" // 🧹
cron = "0 ${local.cache_cleanup_minute} ${local.cache_cleanup_hour} * * *"
script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
cache_dir=$(go env GOCACHE)
echo "Cleaning Go build cache entries not used in the last 2 days..."
before=$(du -s "$cache_dir" 2>/dev/null | awk '{print $1}')
find "$cache_dir" -type f -mtime +2 -delete
find "$cache_dir" -type d -empty -delete
after=$(du -s "$cache_dir" 2>/dev/null | awk '{print $1}')
freed=$(( (before - after) / 1024 ))
echo "Freed $${freed}MB from Go build cache."
EOT
}
resource "coder_devcontainer" "coder" {
count = data.coder_parameter.devcontainer_autostart.value ? data.coder_workspace.me.start_count : 0
agent_id = coder_agent.dev.id
workspace_folder = local.repo_dir
}
# Add a cost so we get some quota usage in dev.coder.com
resource "coder_metadata" "home_volume" {
resource_id = docker_volume.home_volume.id
daily_cost = 1
}
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
resource "coder_metadata" "homebrew_volume" {
resource_id = docker_volume.homebrew_volume.id
hide = true # Hide it as it only backs Homebrew state.
}
resource "docker_volume" "homebrew_volume" {
name = "coder-${data.coder_workspace.me.id}-homebrew"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
resource "coder_metadata" "docker_volume" {
resource_id = docker_volume.docker_volume.id
hide = true # Hide it as it is not useful to see in the UI.
}
resource "docker_volume" "docker_volume" {
name = "coder-${data.coder_workspace.me.id}-docker"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
data "docker_registry_image" "dogfood" {
name = local.image_tags[data.coder_parameter.image_type.value]
}
resource "docker_image" "dogfood" {
name = "${local.image_tags[data.coder_parameter.image_type.value]}@${data.docker_registry_image.dogfood.sha256_digest}"
# CI rebuilds and pushes when any baked-in input changes, so the
# digest captures every effective change on its own.
pull_triggers = [
data.docker_registry_image.dogfood.sha256_digest,
]
keep_locally = true
}
resource "docker_container" "workspace" {
lifecycle {
// Ignore changes that would invalidate prebuilds
ignore_changes = [
name,
hostname,
labels,
env,
entrypoint
]
}
count = data.coder_workspace.me.start_count
image = docker_image.dogfood.name
name = local.container_name
# Hostname makes the shell more user friendly: coder@my-workspace:~$
hostname = data.coder_workspace.me.name
# Use the docker gateway if the access URL is 127.0.0.1
entrypoint = ["sh", "-c", coder_agent.dev.init_script]
# CPU limits are unnecessary since Docker will load balance automatically
memory = data.coder_workspace_owner.me.name == "code-asher" ? 65536 : 32768
runtime = "sysbox-runc"
restart = "unless-stopped"
# Ensure the workspace is given time to:
# - Execute shutdown scripts
# - Stop the in workspace Docker daemon
# - Stop the container, especially when using devcontainers,
# deleting the overlay filesystem can take a while.
destroy_grace_seconds = 300
stop_timeout = 300
stop_signal = "SIGINT"
env = [
"CODER_AGENT_TOKEN=${coder_agent.dev.token}",
"USE_CAP_NET_ADMIN=true",
"CODER_PROC_PRIO_MGMT=1",
"CODER_PROC_OOM_SCORE=10",
"CODER_PROC_NICE_SCORE=1",
"CODER_AGENT_DEVCONTAINERS_ENABLE=1",
"CODER_AGENT_EXP_MCP_CONFIG_FILES=~/.mcp.json,.mcp.json",
]
host {
host = "host.docker.internal"
ip = "host-gateway"
}
volumes {
container_path = "/home/coder/"
volume_name = docker_volume.home_volume.name
read_only = false
}
# Homebrew is baked into this path. A Docker named volume copies the
# image contents on first mount, then persists user-installed formulae.
volumes {
container_path = "/home/linuxbrew/"
volume_name = docker_volume.homebrew_volume.name
read_only = false
}
volumes {
container_path = "/var/lib/docker/"
volume_name = docker_volume.docker_volume.name
read_only = false
}
capabilities {
add = ["CAP_NET_ADMIN", "CAP_SYS_NICE"]
}
# Gated behind a parameter because mapping /dev/kvm fails container creation
# on hosts that do not expose it.
dynamic "devices" {
for_each = data.coder_parameter.enable_kvm.value ? [1] : []
content {
host_path = "/dev/kvm"
container_path = "/dev/kvm"
}
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
labels {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}
resource "coder_metadata" "container_info" {
count = data.coder_workspace.me.start_count
resource_id = docker_container.workspace[0].id
item {
key = "memory"
value = docker_container.workspace[0].memory
}
item {
key = "runtime"
value = docker_container.workspace[0].runtime
}
item {
key = "region"
value = data.coder_parameter.region.option[index(data.coder_parameter.region.option.*.value, data.coder_parameter.region.value)].name
}
}
resource "coder_script" "boundary_config_setup" {
agent_id = coder_agent.dev.id
display_name = "Boundary Setup Configuration"
run_on_start = true
script = <<-EOF
#!/bin/sh
trap 'coder exp sync complete boundary-config-setup' EXIT
coder exp sync start boundary-config-setup
mkdir -p ~/.config/coder_boundary
echo '${base64encode(file("${path.module}/boundary-config.yaml"))}' | base64 -d > ~/.config/coder_boundary/config.yaml
chmod 600 ~/.config/coder_boundary/config.yaml
EOF
}
module "claude-code" {
count = data.coder_workspace.me.start_count
source = "dev.registry.coder.com/coder/claude-code/coder"
version = "5.4.0"
enable_ai_gateway = true
anthropic_api_key = ""
agent_id = coder_agent.dev.id
workdir = local.repo_dir
mcp = <<-EOF
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["--", "@playwright/mcp@0.0.75", "--headless", "--isolated", "--no-sandbox"]
}
}
}
EOF
}
resource "coder_app" "claude" {
agent_id = coder_agent.dev.id
slug = "claude"
display_name = "Claude Code"
icon = "/icon/claude.svg"
open_in = "slim-window"
command = <<-EOT
#!/bin/bash
set -e
cd "${local.repo_dir}"
exec tmux new-session -A -s claude claude
EOT
}
module "codex" {
source = "dev.registry.coder.com/coder-labs/codex/coder"
version = "5.3.0"
agent_id = coder_agent.dev.id
workdir = local.repo_dir
enable_ai_gateway = true
openai_api_key = ""
mcp = <<-EOT
[mcp_servers.playwright]
command = "npx"
args = ["--", "@playwright/mcp@0.0.75", "--headless", "--isolated", "--no-sandbox"]
type = "stdio"
EOT
}
resource "coder_app" "codex" {
agent_id = coder_agent.dev.id
slug = "codex"
display_name = "Codex"
icon = "/icon/openai-codex.svg"
open_in = "slim-window"
command = <<-EOT
#!/bin/bash
set -e
cd "${local.repo_dir}"
exec tmux new-session -A -s codex codex
EOT
}
# Live view of agent browser sessions, served by the dashboard that
# install-deps starts. The dashboard has no authentication, so restrict
# it to the workspace owner. "preview" is reserved for apps that need
# the iframe navigation toolbar.
resource "coder_app" "agent_browser" {
agent_id = coder_agent.dev.id
slug = "agent-browser"
display_name = "Agent Browser"
icon = "${data.coder_workspace.me.access_url}/emojis/1f310.png" // 🌐
url = "http://localhost:4848"
share = "owner"
subdomain = true
open_in = "tab"
healthcheck {
url = "http://localhost:4848/"
interval = 5
threshold = 6
}
}