Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added registry/sharkymark/.images/flask-app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions registry/sharkymark/templates/docker-devcontainer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
display_name: Dev Containers in a Docker workspace
description: Provision Docker containers as Coder workspaces running Dev Containers via Docker-in-Docker and the devcontainer CLI.
icon: "../../../../.icons/docker.svg"
maintainer_github: sharkymark
verified: false
tags: [docker, container, devcontainer, code-server]
---

# Remote Development on Dev Containers

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 and the Dev Container CLI.

![Flask App Screenshot](../../.images/flask-app.png)

## Example devcontainer

The default example devcontainer is one of Sharkymark's sales commissions calculators. It is a Python Flask app and will auto-start so should be accessible on port 5000 when the workspace is running. See the repo on GitHub [here](https://github.com/sharkymark/flask-commissions).

The example repo `devcontainer.json` includes a feature to Coder' code-server web IDE which will be available in the Web Editors button in the workspace. It defaults to open in the `/workspaces` folder which is where the repo is cloned.

Happy selling! 🦈

## Prerequisites

### Infrastructure

The VM you run Coder on must have a running Docker socket and the `coder` user must be added to the Docker group:

```sh
# Add coder user to Docker group
sudo adduser coder docker

# Restart Coder server
sudo systemctl restart coder

# Test Docker
sudo -u coder docker ps
```

## Things to know

### docker prune

The template's `shutdown_script` runs `docker system prune -f` to clean up unused Docker resources when the workspace is stopped. This helps manage disk space on the host machine. Consider adjusting to make the workspace load faster if needed.

### devcontainers-cli Coder module

The template uses Coder's [devcontainers-cli module](https://registry.coder.com/modules/coder/devcontainers-cli) to install the `@devcontainers/cli` tool in the workspace. This is used to run Dev Containers inside the Docker-in-Docker container.

### Docker-in-Docker script

The template runs a custom script in the template called `docker-in-docker.sh` to do the following:

- Detects if Coder workspace is accessed via host.docker.internal and applies networking fixes if needed.
- Enables IP forwarding and sets up NAT rules to allow proper traffic flow between devcontainers and host.
- Forwards relevant ports (from the agent URL, or 80/443 by default) to the host gateway.
- Starts Docker service and determines workspace bridge IP for DNS resolution.
- Installs and configures dnsmasq so devcontainers can resolve host.docker.internal to the workspace’s IP.
- Configures Docker to use the custom DNS server, ensuring hostname resolution works inside all containers.

## Architecture

This example uses the `codercom/enterprise-node:ubuntu` Docker image as a base image for the workspace. See image on [DockerHub](https://hub.docker.com/r/codercom/enterprise-node) and Dockerfile on [GitHub](https://github.com/coder/images/tree/main/images/node) 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:

- Docker image (built by Docker socket and kept locally)
- Docker container (ephemeral)
- Docker volume (persistent on `/home/coder`)
- Docker volume (persistent on `/var/lib/docker`)

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.
309 changes: 309 additions & 0 deletions registry/sharkymark/templates/docker-devcontainer/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
}
}

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
}

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. This default is a sample Flask web app to calculate sales commissions. 🦈"
default = "https://github.com/sharkymark/flask-commissions.git"
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
}

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

# 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

# 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 = <<EOT
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
EOT
interval = 60
timeout = 1
}

metadata {
display_name = "Swap Usage (Host)"
key = "7_swap_host"
script = <<EOT
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
EOT
interval = 10
timeout = 1
}
}

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

# 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"
}

# 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
}
# 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_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
}
}
Loading