mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(examples/templates): add docker-devcontainer template and rename envbuilder template (#18741)
This change adds a new `docker-devcontainer` template which allows you to provision a workspace running in Docker, that also creates workspaces via Docker running inside (DinD). - **chore(examples/templates): rename `docker-devcontainer` to `docker-envbuilder`** - **feat(examples/templates): add `docker-devcontainer` example template**
This commit is contained in:
@@ -1,25 +1,27 @@
|
||||
---
|
||||
display_name: Docker (Devcontainer)
|
||||
description: Provision envbuilder containers as Coder workspaces
|
||||
display_name: Docker-in-Docker Dev Containers
|
||||
description: Provision Docker containers as Coder workspaces running Dev Containers via Docker-in-Docker.
|
||||
icon: ../../../site/static/icon/docker.png
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [container, docker, devcontainer]
|
||||
tags: [docker, container, devcontainer]
|
||||
---
|
||||
|
||||
# Remote Development on Docker Containers (with Devcontainers)
|
||||
# Remote Development on Dev Containers
|
||||
|
||||
Provision Devcontainers as [Coder workspaces](https://coder.com/docs/workspaces) in Docker with this example template.
|
||||
Provision Docker containers as [Coder workspaces](https://coder.com/docs/workspaces) running [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) via Docker-in-Docker.
|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Infrastructure
|
||||
|
||||
Coder must have access to a running Docker socket, and the `coder` user must be a member of the `docker` group:
|
||||
The VM you run Coder on must have a running Docker socket and the `coder` user must be added to the Docker group:
|
||||
|
||||
```shell
|
||||
```sh
|
||||
# Add coder user to Docker group
|
||||
sudo usermod -aG docker coder
|
||||
sudo adduser coder docker
|
||||
|
||||
# Restart Coder server
|
||||
sudo systemctl restart coder
|
||||
@@ -30,48 +32,18 @@ sudo -u coder docker ps
|
||||
|
||||
## Architecture
|
||||
|
||||
Coder supports Devcontainers 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 example uses the `codercom/enterprise-node:ubuntu` Docker image as a base image for the workspace. It includes necessary tools like Docker and Node.js, which are required for running Dev Containers via the `@devcontainers/cli` tool.
|
||||
|
||||
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 image (built by Docker socket and kept locally)
|
||||
- Docker container (ephemeral)
|
||||
- Docker volume (persistent on `/workspaces`)
|
||||
- Docker volume (persistent on `/home/coder`)
|
||||
- Docker volume (persistent on `/var/lib/docker`)
|
||||
|
||||
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!
|
||||
This means, when the workspace restarts, any tools or files outside of the home directory or docker library are not persisted.
|
||||
|
||||
For devcontainers running inside the workspace, data persistence is dependent on each projects `devcontainer.json` configuration.
|
||||
|
||||
> **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 a devcontainer built by Envbuilder.
|
||||
|
||||
## 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/devcontainer-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.
|
||||
|
||||
@@ -1,258 +1,87 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "~> 2.0"
|
||||
source = "coder/coder"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
}
|
||||
envbuilder = {
|
||||
source = "coder/envbuilder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
username = data.coder_workspace_owner.me.name
|
||||
|
||||
# Use a workspace image that supports rootless Docker
|
||||
# (Docker-in-Docker) and Node.js.
|
||||
workspace_image = "codercom/enterprise-node:ubuntu"
|
||||
}
|
||||
|
||||
variable "docker_socket" {
|
||||
default = ""
|
||||
description = "(Optional) Docker socket URI"
|
||||
type = string
|
||||
}
|
||||
|
||||
provider "coder" {}
|
||||
data "coder_parameter" "repo_url" {
|
||||
type = "string"
|
||||
name = "repo_url"
|
||||
display_name = "Git Repository"
|
||||
description = "Enter the URL of the Git repository to clone into your workspace. This repository should contain a devcontainer.json file to configure your development environment."
|
||||
default = "https://github.com/coder/coder"
|
||||
mutable = true
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
# Prepare user home with default files on first start.
|
||||
if [ ! -f ~/.init_done ]; then
|
||||
cp -rT /etc/skel ~
|
||||
touch ~/.init_done
|
||||
fi
|
||||
|
||||
# Add any commands that should be executed at workspace startup
|
||||
# (e.g. install requirements, start a program, etc) here.
|
||||
EOT
|
||||
shutdown_script = <<-EOT
|
||||
set -e
|
||||
|
||||
# Clean up the docker volume from 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
|
||||
|
||||
# Stop the Docker service.
|
||||
sudo service docker stop
|
||||
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
|
||||
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
|
||||
@@ -279,7 +108,7 @@ resource "coder_agent" "main" {
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $HOME"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
@@ -322,51 +151,159 @@ resource "coder_agent" "main" {
|
||||
}
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/code-server
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
resource "coder_script" "init_docker_in_docker" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
agent_id = coder_agent.main.id
|
||||
display_name = "Initialize Docker-in-Docker"
|
||||
run_on_start = true
|
||||
icon = "/icon/docker.svg"
|
||||
script = file("${path.module}/scripts/init-docker-in-docker.sh")
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/devcontainers-cli
|
||||
module "devcontainers-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/devcontainers-cli/coder"
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/jetbrains-gateway
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
|
||||
# JetBrains IDEs to make available for the user to select
|
||||
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
|
||||
default = "IU"
|
||||
|
||||
# Default folder to open when starting a JetBrains IDE
|
||||
folder = "/workspaces"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
# This ensures that the latest non-breaking version of the module gets
|
||||
# downloaded, you can also pin the module version to prevent breaking
|
||||
# changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
order = 2
|
||||
}
|
||||
|
||||
resource "coder_metadata" "container_info" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = coder_agent.main.id
|
||||
item {
|
||||
key = "workspace image"
|
||||
value = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
|
||||
# See https://registry.coder.com/modules/coder/git-clone
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
agent_id = coder_agent.main.id
|
||||
url = data.coder_parameter.repo_url.value
|
||||
base_dir = "~"
|
||||
# This ensures that the latest non-breaking version of the module gets
|
||||
# downloaded, you can also pin the module version to prevent breaking
|
||||
# changes in production.
|
||||
version = "~> 1.0"
|
||||
}
|
||||
|
||||
# Automatically start the devcontainer for the workspace.
|
||||
resource "coder_devcontainer" "repo" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
agent_id = coder_agent.main.id
|
||||
workspace_folder = "~/${module.git-clone[0].folder_name}"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
item {
|
||||
key = "git url"
|
||||
value = local.repo_url
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
item {
|
||||
key = "cache repo"
|
||||
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
|
||||
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 "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
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = local.workspace_image
|
||||
|
||||
# NOTE: The `privileged` mode is one way to run Docker-in-Docker,
|
||||
# which is required for the devcontainer to work. If this is not
|
||||
# desired, you can remove this line. However, you will need to ensure
|
||||
# that the devcontainer can run Docker commands in some other way.
|
||||
# Mounting the host Docker socket is strongly discouraged because
|
||||
# workspaces will then compete for control of the devcontainers.
|
||||
# For more information, see:
|
||||
# https://coder.com/docs/admin/templates/extending-templates/docker-in-workspaces
|
||||
privileged = true
|
||||
|
||||
# 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 docker gateway if the access URL is 127.0.0.1
|
||||
command = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||
env = [
|
||||
"CODER_AGENT_TOKEN=${coder_agent.main.token}"
|
||||
]
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
|
||||
# Workspace home volume persists user data across workspace restarts.
|
||||
volumes {
|
||||
container_path = "/home/coder"
|
||||
volume_name = docker_volume.home_volume.name
|
||||
read_only = false
|
||||
}
|
||||
|
||||
# Workspace docker volume persists Docker data across workspace
|
||||
# restarts, allowing the devcontainer cache to be reused.
|
||||
volumes {
|
||||
container_path = "/var/lib/docker"
|
||||
volume_name = docker_volume.docker_volume.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Docker-in-Docker setup for Coder dev containers using host.docker.internal
|
||||
# URLs. When Docker runs inside a container, the "docker0" bridge interface
|
||||
# can interfere with host.docker.internal DNS resolution, breaking
|
||||
# connectivity to the Coder server.
|
||||
|
||||
if [ "${CODER_AGENT_URL#*host.docker.internal}" = "$CODER_AGENT_URL" ]; then
|
||||
# External access URL detected, no networking workarounds needed.
|
||||
sudo service docker start
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# host.docker.internal URL detected. Docker's default bridge network creates
|
||||
# a "docker0" interface that can shadow the host.docker.internal hostname
|
||||
# resolution. This typically happens when Docker starts inside a devcontainer,
|
||||
# as the inner Docker daemon creates its own bridge network that conflicts
|
||||
# with the outer one.
|
||||
|
||||
# Enable IP forwarding to allow packets to route between the host network and
|
||||
# the devcontainer networks. Without this, traffic cannot flow properly
|
||||
# between the different Docker bridge networks.
|
||||
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
|
||||
sudo iptables -t nat -A POSTROUTING -j MASQUERADE
|
||||
|
||||
# Set up port forwarding to the host Docker gateway (typically 172.17.0.1).
|
||||
# We resolve host.docker.internal to get the actual IP and create NAT rules
|
||||
# to forward traffic from this workspace to the host.
|
||||
host_ip=$(getent hosts host.docker.internal | awk '{print $1}')
|
||||
|
||||
echo "Host IP for host.docker.internal: $host_ip"
|
||||
|
||||
# Extract the port from CODER_AGENT_URL. The URL format is typically
|
||||
# http://host.docker.internal:port/.
|
||||
port="${CODER_AGENT_URL##*:}"
|
||||
port="${port%%/*}"
|
||||
case "$port" in
|
||||
[0-9]*)
|
||||
# Specific port found, forward it to the host gateway.
|
||||
sudo iptables -t nat -A PREROUTING -p tcp --dport "$port" -j DNAT --to-destination "$host_ip:$port"
|
||||
echo "Forwarded port $port to $host_ip"
|
||||
;;
|
||||
*)
|
||||
# No specific port or non-numeric port, forward standard web ports.
|
||||
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination "$host_ip:80"
|
||||
sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination "$host_ip:443"
|
||||
echo "Forwarded default ports 80/443 to $host_ip"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Start Docker service, which creates the "docker0" interface if it doesn't
|
||||
# exist. We need the interface to extract the second IP address for DNS
|
||||
# resolution.
|
||||
sudo service docker start
|
||||
|
||||
# Configure DNS resolution to avoid requiring devcontainer project modifications.
|
||||
# While devcontainers can use the "--add-host" flag, it requires explicit
|
||||
# definition in devcontainer.json. Using a DNS server instead means every
|
||||
# devcontainer project doesn't need to accommodate this.
|
||||
|
||||
# Wait for the workspace to acquire its Docker bridge IP address. The
|
||||
# "hostname -I" command returns multiple IPs: the first is typically the host
|
||||
# Docker bridge (172.17.0.0/16 range) and the second is the workspace Docker
|
||||
# bridge (172.18.0.0/16). We need the second IP because that's where
|
||||
# devcontainers will be able to reach us.
|
||||
dns_ip=
|
||||
while [ -z "$dns_ip" ]; do
|
||||
dns_ip=$(hostname -I | awk '{print $2}')
|
||||
if [ -z "$dns_ip" ]; then
|
||||
echo "Waiting for hostname -I to return a valid second IP address..."
|
||||
sleep 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Using DNS IP: $dns_ip"
|
||||
|
||||
# Install dnsmasq to provide custom DNS resolution. This lightweight DNS
|
||||
# server allows us to override specific hostname lookups without affecting
|
||||
# other DNS queries.
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y dnsmasq
|
||||
|
||||
# Configure dnsmasq to resolve host.docker.internal to this workspace's IP.
|
||||
# This ensures devcontainers can find the Coder server even when the "docker0"
|
||||
# interface would normally shadow the hostname resolution.
|
||||
echo "no-hosts" | sudo tee /etc/dnsmasq.conf
|
||||
echo "address=/host.docker.internal/$dns_ip" | sudo tee -a /etc/dnsmasq.conf
|
||||
echo "resolv-file=/etc/resolv.conf" | sudo tee -a /etc/dnsmasq.conf
|
||||
echo "no-dhcp-interface=" | sudo tee -a /etc/dnsmasq.conf
|
||||
echo "bind-interfaces" | sudo tee -a /etc/dnsmasq.conf
|
||||
echo "listen-address=127.0.0.1,$dns_ip" | sudo tee -a /etc/dnsmasq.conf
|
||||
|
||||
sudo service dnsmasq restart
|
||||
|
||||
# Configure Docker daemon to use our custom DNS server. This is the critical
|
||||
# piece that ensures all containers (including devcontainers) use our dnsmasq
|
||||
# server for hostname resolution, allowing them to properly resolve
|
||||
# host.docker.internal.
|
||||
echo "{\"dns\": [\"$dns_ip\"]}" | sudo tee /etc/docker/daemon.json
|
||||
sudo service docker restart
|
||||
Reference in New Issue
Block a user