diff --git a/registry/coder/.images/tasks-screenshot.png b/registry/coder/.images/tasks-screenshot.png new file mode 100644 index 000000000..8d9f605ae Binary files /dev/null and b/registry/coder/.images/tasks-screenshot.png differ diff --git a/registry/coder/templates/docker-envbuilder/README.md b/registry/coder/templates/docker-envbuilder/README.md new file mode 100644 index 000000000..31b9acf92 --- /dev/null +++ b/registry/coder/templates/docker-envbuilder/README.md @@ -0,0 +1,76 @@ +--- +display_name: Docker (Envbuilder) +description: Provision envbuilder containers as Coder workspaces +icon: ../../../../.icons/docker.svg +verified: true +tags: [container, docker, devcontainer, envbuilder] +--- + +# Remote Development on Docker Containers (with Envbuilder) + +Provision Envbuilder containers based on `devcontainer.json` as [Coder workspaces](https://coder.com/docs/workspaces) in Docker with this example template. + +## Prerequisites + +### Infrastructure + +Coder must have access to a running Docker socket, and the `coder` user must be a member of the `docker` group: + +```shell +# Add coder user to Docker group +sudo usermod -aG docker coder + +# Restart Coder server +sudo systemctl restart coder + +# Test Docker +sudo -u coder docker ps +``` + +## Architecture + +Coder supports Envbuilder containers based on `devcontainer.json` via [envbuilder](https://github.com/coder/envbuilder), an open source project. Read more about this in [Coder's documentation](https://coder.com/docs/templates/dev-containers). + +This template provisions the following resources: + +- Envbuilder cached image (conditional, persistent) using [`terraform-provider-envbuilder`](https://github.com/coder/terraform-provider-envbuilder) +- Docker image (persistent) using [`envbuilder`](https://github.com/coder/envbuilder) +- Docker container (ephemeral) +- Docker volume (persistent on `/workspaces`) + +The Git repository is cloned inside the `/workspaces` volume if not present. +Any local changes to the Devcontainer files inside the volume will be applied when you restart the workspace. +Keep in mind that any tools or files outside of `/workspaces` or not added as part of the Devcontainer specification are not persisted. +Edit the `devcontainer.json` instead! + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## Docker-in-Docker + +See the [Envbuilder documentation](https://github.com/coder/envbuilder/blob/main/docs/docker.md) for information on running Docker containers inside an Envbuilder container. + +## Caching + +To speed up your builds, you can use a container registry as a cache. +When creating the template, set the parameter `cache_repo` to a valid Docker repository. + +For example, you can run a local registry: + +```shell +docker run --detach \ + --volume registry-cache:/var/lib/registry \ + --publish 5000:5000 \ + --name registry-cache \ + --net=host \ + registry:2 +``` + +Then, when creating the template, enter `localhost:5000/envbuilder-cache` for the parameter `cache_repo`. + +See the [Envbuilder Terraform Provider Examples](https://github.com/coder/terraform-provider-envbuilder/blob/main/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf/) for a more complete example of how the provider works. + +> [!NOTE] +> We recommend using a registry cache with authentication enabled. +> To allow Envbuilder to authenticate with the registry cache, specify the variable `cache_repo_docker_config_path` +> with the path to a Docker config `.json` on disk containing valid credentials for the registry. diff --git a/registry/coder/templates/docker-envbuilder/main.tf b/registry/coder/templates/docker-envbuilder/main.tf new file mode 100644 index 000000000..47e486c81 --- /dev/null +++ b/registry/coder/templates/docker-envbuilder/main.tf @@ -0,0 +1,362 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "~> 2.0" + } + docker = { + source = "kreuzwerker/docker" + } + envbuilder = { + source = "coder/envbuilder" + } + } +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +provider "coder" {} +provider "docker" { + # Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default + host = var.docker_socket != "" ? var.docker_socket : null +} +provider "envbuilder" {} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "coder_parameter" "repo" { + description = "Select a repository to automatically clone and start working with a devcontainer." + display_name = "Repository (auto)" + mutable = true + name = "repo" + option { + name = "vercel/next.js" + description = "The React Framework" + value = "https://github.com/vercel/next.js" + } + option { + name = "home-assistant/core" + description = "🏡 Open source home automation that puts local control and privacy first." + value = "https://github.com/home-assistant/core" + } + option { + name = "discourse/discourse" + description = "A platform for community discussion. Free, open, simple." + value = "https://github.com/discourse/discourse" + } + option { + name = "denoland/deno" + description = "A modern runtime for JavaScript and TypeScript." + value = "https://github.com/denoland/deno" + } + option { + name = "microsoft/vscode" + icon = "/icon/code.svg" + description = "Code editing. Redefined." + value = "https://github.com/microsoft/vscode" + } + option { + name = "Custom" + icon = "/emojis/1f5c3.png" + description = "Specify a custom repo URL below" + value = "custom" + } + order = 1 +} + +data "coder_parameter" "custom_repo_url" { + default = "" + description = "Optionally enter a custom repository URL, see [awesome-devcontainers](https://github.com/manekinekko/awesome-devcontainers)." + display_name = "Repository URL (custom)" + name = "custom_repo_url" + mutable = true + order = 2 +} + +data "coder_parameter" "fallback_image" { + default = "codercom/enterprise-base:ubuntu" + description = "This image runs if the devcontainer fails to build." + display_name = "Fallback Image" + mutable = true + name = "fallback_image" + order = 3 +} + +data "coder_parameter" "devcontainer_builder" { + description = <<-EOF +Image that will build the devcontainer. +We highly recommend using a specific release as the `:latest` tag will change. +Find the latest version of Envbuilder here: https://github.com/coder/envbuilder/pkgs/container/envbuilder +EOF + display_name = "Devcontainer Builder" + mutable = true + name = "devcontainer_builder" + default = "ghcr.io/coder/envbuilder:latest" + order = 4 +} + +variable "cache_repo" { + default = "" + description = "(Optional) Use a container registry as a cache to speed up builds." + type = string +} + +variable "insecure_cache_repo" { + default = false + description = "Enable this option if your cache registry does not serve HTTPS." + type = bool +} + +variable "cache_repo_docker_config_path" { + default = "" + description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required." + sensitive = true + type = string +} + +locals { + container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" + devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value + 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 + repo_url = data.coder_parameter.repo.value == "custom" ? data.coder_parameter.custom_repo_url.value : data.coder_parameter.repo.value + # The envbuilder provider requires a key-value map of environment variables. + envbuilder_env = { + # ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider + # if the cache repo is enabled. + "ENVBUILDER_GIT_URL" : local.repo_url, + "ENVBUILDER_CACHE_REPO" : var.cache_repo, + "CODER_AGENT_TOKEN" : coder_agent.main.token, + # Use the docker gateway if the access URL is 127.0.0.1 + "CODER_AGENT_URL" : replace(data.coder_workspace.me.access_url, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), + # Use the docker gateway if the access URL is 127.0.0.1 + "ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), + "ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value, + "ENVBUILDER_DOCKER_CONFIG_BASE64" : try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, ""), + "ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true", + "ENVBUILDER_INSECURE" : "${var.insecure_cache_repo}", + } + # Convert the above map to the format expected by the docker provider. + docker_env = [ + for k, v in local.envbuilder_env : "${k}=${v}" + ] +} + +data "local_sensitive_file" "cache_repo_dockerconfigjson" { + count = var.cache_repo_docker_config_path == "" ? 0 : 1 + filename = var.cache_repo_docker_config_path +} + +resource "docker_image" "devcontainer_builder_image" { + name = local.devcontainer_builder_image + keep_locally = true +} + +resource "docker_volume" "workspaces" { + name = "coder-${data.coder_workspace.me.id}" + # 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 + } +} + +# Check for the presence of a prebuilt image in the cache repo +# that we can use instead. +resource "envbuilder_cached_image" "cached" { + count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count + builder_image = local.devcontainer_builder_image + git_url = local.repo_url + cache_repo = var.cache_repo + extra_env = local.envbuilder_env + insecure = var.insecure_cache_repo +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image + # Uses lower() to avoid Docker restriction on container names. + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" + # Hostname makes the shell more user friendly: coder@my-workspace:~$ + hostname = data.coder_workspace.me.name + # Use the environment specified by the envbuilder provider, if available. + env = var.cache_repo == "" ? local.docker_env : envbuilder_cached_image.cached.0.env + # network_mode = "host" # Uncomment if testing with a registry running on `localhost`. + host { + host = "host.docker.internal" + ip = "host-gateway" + } + volumes { + container_path = "/workspaces" + volume_name = docker_volume.workspaces.name + read_only = false + } + # 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_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script = <<-EOT + set -e + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT + dir = "/workspaces" + + # These environment variables allow you to make Git commits right away after creating a + # workspace. Note that they take precedence over configuration defined in ~/.gitconfig! + # You can remove this block if you'd prefer to configure Git manually or using + # dotfiles. (see docs/dotfiles.md) + env = { + GIT_AUTHOR_NAME = local.git_author_name + GIT_AUTHOR_EMAIL = local.git_author_email + GIT_COMMITTER_NAME = local.git_author_name + GIT_COMMITTER_EMAIL = local.git_author_email + } + + # 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. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "3_home_disk" + script = "coder stat disk --path $HOME" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = </dev/null 2>&1; then + yes | npx playwright install chrome + fi + + # MCP: Install and configure MCP Servers + npm install -g @wonderwhy-er/desktop-commander + claude mcp add playwright npx -- @playwright/mcp@latest --headless --isolated --no-sandbox + claude mcp add desktop-commander desktop-commander + + # Repo: Clone and pull changes from the git repository + if [ ! -d "realworld-django-rest-framework-angular" ]; then + git clone https://github.com/coder-contrib/realworld-django-rest-framework-angular.git + else + cd realworld-django-rest-framework-angular + git fetch + # Check for uncommitted changes + if git diff-index --quiet HEAD -- && \ + [ -z "$(git status --porcelain --untracked-files=no)" ] && \ + [ -z "$(git log --branches --not --remotes)" ]; then + echo "Repo is clean. Pulling latest changes..." + git pull + else + echo "Repo has uncommitted or unpushed changes. Skipping pull." + fi + + cd .. + fi + + # Initialize: Start the development server + cd realworld-django-rest-framework-angular && ./start-dev.sh + EOT + "preview_port" = "4200" + "container_image" = "codercom/example-universal:ubuntu" + } + + # Pre-builds is a Coder Premium + # feature to speed up workspace creation + # + # see https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces + # prebuilds { + # instances = 1 + # expiration_policy { + # ttl = 86400 # Time (in seconds) after which unclaimed prebuilds are expired (1 day) + # } + # } +} + +# Advanced parameters (these are all set via preset) +data "coder_parameter" "system_prompt" { + name = "system_prompt" + display_name = "System Prompt" + type = "string" + form_type = "textarea" + description = "System prompt for the agent with generalized instructions" + mutable = false +} +data "coder_parameter" "setup_script" { + name = "setup_script" + display_name = "Setup Script" + type = "string" + form_type = "textarea" + description = "Script to run before running the agent" + mutable = false +} +data "coder_parameter" "container_image" { + name = "container_image" + display_name = "Container Image" + type = "string" + default = "codercom/example-universal:ubuntu" + mutable = false +} +data "coder_parameter" "preview_port" { + name = "preview_port" + display_name = "Preview Port" + description = "The port the web app is running to preview in Tasks" + type = "number" + default = "3000" + mutable = false +} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script = <<-EOT + set -e + # Prepare user home with default files on first start. + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ + touch ~/.init_done + fi + EOT + + # These environment variables allow you to make Git commits right away after creating a + # workspace. Note that they take precedence over configuration defined in ~/.gitconfig! + # You can remove this block if you'd prefer to configure Git manually or using + # dotfiles. (see docs/dotfiles.md) + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}" + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}" + } + + # 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. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "3_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = <