From 7b19ec393307b2b026f464714e7574d13d3d971d Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 6 Jul 2026 16:37:34 -0400 Subject: [PATCH] feat: improve the image management experience with template builder (#27018) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes it easier to pick the right workspace image, both in the template builder and in the docs. - Template builder: the Docker and Kubernetes bases now expose a `container_image` variable in the wizard (freeform text, defaults to `codercom/example-base:ubuntu`), and their prerequisites explain why image choice matters, with tradeoffs between `codercom/example-base:ubuntu` (minimal) and `codercom/example-universal:ubuntu` (catch-all), plus pointers to [coder/images](https://github.com/coder/images) and the image management docs. - Docs: reworked [image management](https://coder.com/docs/@ben%2Fdevrel-201-image-guidance-prereqs/admin/templates/managing-templates/image-management) into a clearer maturity ladder (minimal → golden → project-specific → developer customization), with pullable image references in every example, `codercom/oss-dogfood` as a project-specific example, and Dev Containers + [mise](https://mise.jdx.dev/) as ways to customize without new images. Companion PR for the starter templates: coder/registry#943 Part of DEVREL-201. 🤖 Generated with Coder Agents using Claude, on behalf of @bpmct (wizard variable by @jeremyruppel in #27024) --------- Co-authored-by: Jeremy Ruppel --- coderd/templatebuilder/bases/docker/README.md | 13 +++ coderd/templatebuilder/bases/docker/base.json | 15 +++- .../templatebuilder/bases/docker/main.tf.tmpl | 2 +- .../bases/kubernetes/README.md | 13 ++- .../bases/kubernetes/base.json | 11 ++- .../bases/kubernetes/main.tf.tmpl | 2 +- coderd/templatebuilder/bases_test.go | 4 +- coderd/templatebuilder/compose_test.go | 54 +++++++++++++ .../templatebuilder/testdata/docker.tf.golden | 2 +- .../testdata/kubernetes.tf.golden | 2 +- coderd/templatebuilder_handler_test.go | 5 +- .../managing-templates/image-management.md | 81 ++++++++++++------- 12 files changed, 165 insertions(+), 39 deletions(-) diff --git a/coderd/templatebuilder/bases/docker/README.md b/coderd/templatebuilder/bases/docker/README.md index 6398547ef5..60b95bfbbf 100644 --- a/coderd/templatebuilder/bases/docker/README.md +++ b/coderd/templatebuilder/bases/docker/README.md @@ -17,6 +17,19 @@ Provision Docker containers as [Coder workspaces](https://coder.com/docs/user-gu ## Prerequisites +### Workspace image + +The container image determines what tools, languages, and runtimes are available in the workspace out of the box, so it has a major impact on the developer experience. + +Some options to consider: + +- [`codercom/example-base:ubuntu`](https://github.com/coder/images/tree/main/images/base) (default): minimal and lightweight, but may not include many tools developers expect by default +- [`codercom/example-universal:ubuntu`](https://github.com/coder/images/tree/main/images/universal): catch-all image with many languages and tools available, but larger and slower to pull + +More language-specific images (Go, Java, Node.js, and more) are available in [coder/images](https://github.com/coder/images), and the [devcontainers/images](https://github.com/devcontainers/images) collection is another good source of ready-made development images. +You can also build your own image to pre-bake the exact tools your team needs. +Visit [Coder's image management docs](https://coder.com/docs/admin/templates/managing-templates/image-management) for additional guidance. + ### Infrastructure The VM you run Coder on must have a running Docker socket and the `coder` user must be added to the Docker group: diff --git a/coderd/templatebuilder/bases/docker/base.json b/coderd/templatebuilder/bases/docker/base.json index 09b8224c01..1ded9b022a 100644 --- a/coderd/templatebuilder/bases/docker/base.json +++ b/coderd/templatebuilder/bases/docker/base.json @@ -3,6 +3,17 @@ "display_name": "Docker", "os": "linux", "default_context": { - "container_image": "codercom/enterprise-base:ubuntu" - } + "container_image": "codercom/example-base:ubuntu" + }, + "variables": [ + { + "name": "container_image", + "type": "string", + "description": "Container image for workspaces. The image determines which tools and languages are available in the workspace by default. See the template README for guidance on choosing an image.", + "default": "codercom/example-base:ubuntu", + "required": false, + "sensitive": false, + "computed": false + } + ] } diff --git a/coderd/templatebuilder/bases/docker/main.tf.tmpl b/coderd/templatebuilder/bases/docker/main.tf.tmpl index b044974892..c462ef9094 100644 --- a/coderd/templatebuilder/bases/docker/main.tf.tmpl +++ b/coderd/templatebuilder/bases/docker/main.tf.tmpl @@ -166,7 +166,7 @@ resource "docker_container" "workspace" { {{- if .ImageOptions }} image = data.coder_parameter.container_image.value {{- else }} - image = "{{ .ContainerImage }}" + image = {{ .Variables.container_image }} {{- end }} # Uses lower() to avoid Docker restriction on container names. name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" diff --git a/coderd/templatebuilder/bases/kubernetes/README.md b/coderd/templatebuilder/bases/kubernetes/README.md index 26e7e07e38..f97f78c7d4 100644 --- a/coderd/templatebuilder/bases/kubernetes/README.md +++ b/coderd/templatebuilder/bases/kubernetes/README.md @@ -21,7 +21,18 @@ Provision Kubernetes Pods as [Coder workspaces](https://coder.com/docs/user-guid **Cluster**: This template requires an existing Kubernetes cluster -**Container Image**: This template uses the [codercom/enterprise-base:ubuntu image](https://github.com/coder/enterprise-images/tree/main/images/base) with some dev tools preinstalled. To add additional tools, extend this image or build it yourself. +### Workspace image + +The container image determines what tools, languages, and runtimes are available in the workspace out of the box, so it has a major impact on the developer experience. + +Some options to consider: + +- [`codercom/example-base:ubuntu`](https://github.com/coder/images/tree/main/images/base) (default): minimal and lightweight, but may not include many tools developers expect by default +- [`codercom/example-universal:ubuntu`](https://github.com/coder/images/tree/main/images/universal): catch-all image with many languages and tools available, but larger and slower to pull + +More language-specific images (Go, Java, Node.js, and more) are available in [coder/images](https://github.com/coder/images), and the [devcontainers/images](https://github.com/devcontainers/images) collection is another good source of ready-made development images. +You can also build your own image to pre-bake the exact tools your team needs. +Visit [Coder's image management docs](https://coder.com/docs/admin/templates/managing-templates/image-management) for additional guidance. ### Authentication diff --git a/coderd/templatebuilder/bases/kubernetes/base.json b/coderd/templatebuilder/bases/kubernetes/base.json index a3b04acbaa..d6c4583b26 100644 --- a/coderd/templatebuilder/bases/kubernetes/base.json +++ b/coderd/templatebuilder/bases/kubernetes/base.json @@ -3,9 +3,18 @@ "display_name": "Kubernetes", "os": "linux", "default_context": { - "container_image": "codercom/enterprise-base:ubuntu" + "container_image": "codercom/example-base:ubuntu" }, "variables": [ + { + "name": "container_image", + "type": "string", + "description": "Container image for workspaces. The image determines which tools and languages are available in the workspace by default. See the template README for guidance on choosing an image.", + "default": "codercom/example-base:ubuntu", + "required": false, + "sensitive": false, + "computed": false + }, { "name": "use_kubeconfig", "type": "bool", diff --git a/coderd/templatebuilder/bases/kubernetes/main.tf.tmpl b/coderd/templatebuilder/bases/kubernetes/main.tf.tmpl index b50495ffb3..d32c8e0bd9 100644 --- a/coderd/templatebuilder/bases/kubernetes/main.tf.tmpl +++ b/coderd/templatebuilder/bases/kubernetes/main.tf.tmpl @@ -260,7 +260,7 @@ resource "kubernetes_deployment_v1" "main" { {{- if .ImageOptions }} image = data.coder_parameter.container_image.value {{- else }} - image = "{{ .ContainerImage }}" + image = {{ .Variables.container_image }} {{- end }} image_pull_policy = "Always" command = ["sh", "-c", coder_agent.main.init_script] diff --git a/coderd/templatebuilder/bases_test.go b/coderd/templatebuilder/bases_test.go index cb2109331c..f01f6f48a0 100644 --- a/coderd/templatebuilder/bases_test.go +++ b/coderd/templatebuilder/bases_test.go @@ -65,14 +65,14 @@ func TestDefaultBaseRenderContext(t *testing.T) { t.Run("Docker", func(t *testing.T) { t.Parallel() rc := templatebuilder.DefaultBaseRenderContext("docker") - require.Equal(t, "codercom/enterprise-base:ubuntu", rc.ContainerImage) + require.Equal(t, "codercom/example-base:ubuntu", rc.ContainerImage) require.Nil(t, rc.ImageOptions) }) t.Run("Kubernetes", func(t *testing.T) { t.Parallel() rc := templatebuilder.DefaultBaseRenderContext("kubernetes") - require.Equal(t, "codercom/enterprise-base:ubuntu", rc.ContainerImage) + require.Equal(t, "codercom/example-base:ubuntu", rc.ContainerImage) require.Nil(t, rc.ImageOptions) }) diff --git a/coderd/templatebuilder/compose_test.go b/coderd/templatebuilder/compose_test.go index b15692036f..dca7877f82 100644 --- a/coderd/templatebuilder/compose_test.go +++ b/coderd/templatebuilder/compose_test.go @@ -290,6 +290,60 @@ func TestCompose(t *testing.T) { require.Contains(t, err.Error(), "interpolation") }) + t.Run("DockerDefaultContainerImage", func(t *testing.T) { + t.Parallel() + result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{ + BaseTemplateID: "docker", + RegistryURL: "https://registry.coder.com", + }) + require.NoError(t, err) + require.Contains(t, string(result.MainTF), `"codercom/example-base:ubuntu"`) + }) + + t.Run("DockerCustomContainerImage", func(t *testing.T) { + t.Parallel() + result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{ + BaseTemplateID: "docker", + RegistryURL: "https://registry.coder.com", + BaseVariableValues: map[string]string{ + "container_image": "myregistry/myimage:v2", + }, + }) + require.NoError(t, err) + mainTF := string(result.MainTF) + require.Contains(t, mainTF, `"myregistry/myimage:v2"`) + require.NotContains(t, mainTF, `codercom/example-base:ubuntu`) + }) + + t.Run("KubernetesDefaultContainerImage", func(t *testing.T) { + t.Parallel() + result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{ + BaseTemplateID: "kubernetes", + RegistryURL: "https://registry.coder.com", + BaseVariableValues: map[string]string{ + "namespace": "default", + }, + }) + require.NoError(t, err) + require.Contains(t, string(result.MainTF), `"codercom/example-base:ubuntu"`) + }) + + t.Run("KubernetesCustomContainerImage", func(t *testing.T) { + t.Parallel() + result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{ + BaseTemplateID: "kubernetes", + RegistryURL: "https://registry.coder.com", + BaseVariableValues: map[string]string{ + "namespace": "default", + "container_image": "custom/workspace:latest", + }, + }) + require.NoError(t, err) + mainTF := string(result.MainTF) + require.Contains(t, mainTF, `"custom/workspace:latest"`) + require.NotContains(t, mainTF, `codercom/example-base:ubuntu`) + }) + t.Run("MissingRequiredVariable", func(t *testing.T) { t.Parallel() // git-clone has a required "url" variable with no default. diff --git a/coderd/templatebuilder/testdata/docker.tf.golden b/coderd/templatebuilder/testdata/docker.tf.golden index 11d9ded4f6..9efa2ee27a 100644 --- a/coderd/templatebuilder/testdata/docker.tf.golden +++ b/coderd/templatebuilder/testdata/docker.tf.golden @@ -150,7 +150,7 @@ resource "docker_volume" "home_volume" { resource "docker_container" "workspace" { count = data.coder_workspace.me.start_count - image = "codercom/enterprise-base:ubuntu" + image = "codercom/example-base:ubuntu" # 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:~$ diff --git a/coderd/templatebuilder/testdata/kubernetes.tf.golden b/coderd/templatebuilder/testdata/kubernetes.tf.golden index 876f3c1dfc..7bcb33e298 100644 --- a/coderd/templatebuilder/testdata/kubernetes.tf.golden +++ b/coderd/templatebuilder/testdata/kubernetes.tf.golden @@ -244,7 +244,7 @@ resource "kubernetes_deployment_v1" "main" { container { name = "dev" - image = "codercom/enterprise-base:ubuntu" + image = "codercom/example-base:ubuntu" image_pull_policy = "Always" command = ["sh", "-c", coder_agent.main.init_script] security_context { diff --git a/coderd/templatebuilder_handler_test.go b/coderd/templatebuilder_handler_test.go index baa89e80ae..3629bb2d27 100644 --- a/coderd/templatebuilder_handler_test.go +++ b/coderd/templatebuilder_handler_test.go @@ -45,13 +45,14 @@ func TestTemplateBuilderBases(t *testing.T) { { id: "docker", expectedOS: "linux", - hasVariables: false, + hasVariables: true, + expectedVars: []string{"container_image"}, }, { id: "kubernetes", expectedOS: "linux", hasVariables: true, - expectedVars: []string{"namespace", "use_kubeconfig"}, + expectedVars: []string{"container_image", "namespace", "use_kubeconfig"}, }, { id: "aws-linux", diff --git a/docs/admin/templates/managing-templates/image-management.md b/docs/admin/templates/managing-templates/image-management.md index 0a7cca2d62..ac7b1e2dca 100644 --- a/docs/admin/templates/managing-templates/image-management.md +++ b/docs/admin/templates/managing-templates/image-management.md @@ -1,24 +1,27 @@ # Image Management While Coder provides example -[base container images](https://github.com/coder/enterprise-images) for -workspaces, it's often best to create custom images that matches the needs of -your users. This document serves a guide to operational maturity with some best -practices around managing workspaces images for Coder. +[container images](https://github.com/coder/images) for +workspaces, it's often best to create custom images that match the needs of +your users. This document serves as a guide to operational maturity with some +best practices around managing workspace images for Coder. -1. Create a minimal base image -2. Create golden image(s) with standard tooling -3. Allow developers to bring their own images and customizations with Dev - Containers +After following this tutorial, you'll accomplish the following: + +1. Create a minimal base image. +2. Create golden images with standard tooling. +3. Create project-specific images for common use cases. +4. Let developers customize their own environment. An image is just one of the many properties defined within the template. Templates can pull images from a public image registry (e.g. Docker Hub) or an -internal one, thanks to Terraform. +internal one, thanks to Terraform. Each image reference below can be dropped +directly into a template's image variable or parameter. ## Create a minimal base image While you may not use this directly in Coder templates, it's useful to have a -minimal base image is a small image that contains only the necessary +minimal base image as a small image that contains only the necessary dependencies to work in your network and work with Coder. Here are some things to consider: @@ -31,17 +34,18 @@ to consider: `docker`, `bash`, `jq`, and/or internal tooling - Consider creating (and starting the container with) a non-root user -See Coder's -[example base image](https://github.com/coder/enterprise-images/tree/main/images/minimal) -for reference. +Examples: -## Create general-purpose golden image(s) with standard tooling +- [`codercom/example-minimal:ubuntu`](https://github.com/coder/images/tree/main/images/minimal): only the necessary dependencies for a Coder workspace to bootstrap +- [`codercom/example-base:ubuntu`](https://github.com/coder/images/tree/main/images/base): a slightly more padded starting point with common utilities preinstalled -It's often practical to have a few golden images that contain standard tooling -for developers. These images should contain a number of languages (e.g. Python, -Java, TypeScript), IDEs (VS Code, JetBrains, PyCharm), and other tools (e.g. -`docker`). Unlike project-specific images (which are also important), general -purpose images are great for: +## Create golden images with standard tooling + +Building on the base image, it's often practical to have a few golden images +that contain standard tooling for developers. These images should contain a +number of languages (e.g. Python, Java, TypeScript), IDEs (VS Code, JetBrains, +PyCharm), and other tools (e.g. `docker`). Unlike project-specific images +(which are also important), general purpose images are great for: - **Scripting:** Developers may just want to hop in a Coder workspace to run basic scripts or queries. @@ -61,14 +65,37 @@ most cases) with a well-defined scope. Examples: -- [Universal Dev Containers Image](https://github.com/devcontainers/images/tree/main/src/universal) +- [`codercom/example-universal:ubuntu`](https://github.com/coder/images/tree/main/images/universal): a catch-all image with many languages and tools preinstalled. Runs as the `coder` user, so it works with Coder templates out of the box. +- [`mcr.microsoft.com/devcontainers/universal`](https://github.com/devcontainers/images/tree/main/src/universal): the Universal Dev Containers image -## Allow developers to bring their own images and customizations with Dev Containers +## Create project-specific images for common use cases -While golden images are great for general use cases, developers will often need -specific tooling for their projects. The [Dev Container](https://containers.dev) -specification allows developers to define their projects dependencies within a -`devcontainer.json` in their Git repository. +Beyond golden images, create images scoped to a specific project, language, or +use case (e.g., a Go backend, a Node.js frontend, or a data science stack). +These images stay smaller and faster to pull than one larger image that tries to install all dependencies. -- [Configure a template for Dev Containers](../../integrations/devcontainers/integration.md) (recommended) -- [Learn about Envbuilder](../../integrations/devcontainers/envbuilder/index.md) (alternative for environments without Docker) +Examples: + +- [`codercom/example-golang:ubuntu`](https://github.com/coder/images/tree/main/images/golang), + [`codercom/example-java:ubuntu`](https://github.com/coder/images/tree/main/images/java), + [`codercom/example-node:ubuntu`](https://github.com/coder/images/tree/main/images/node): + Coder's example language-specific images, all running as the `coder` user. + Refer to [coder/images](https://github.com/coder/images) for the full list. +- [`codercom/oss-dogfood:latest`](https://github.com/coder/coder/tree/main/dogfood): + the image Coder's own engineers use to develop Coder. A good reference for a + project-specific image tailored to a specific team or monorepo setup. + +## Let developers customize their own environment + +Even with well-scoped images, developers will often need tooling that is +specific to their project or personal workflow. Instead of maintaining an +image for every combination, developers can layer their own customizations +on top of a smaller image: + +- [Dev Containers](https://containers.dev): developers define their project's + dependencies in a `devcontainer.json` in their Git repository, and Coder + builds the environment on top of your base image. Visit + [configure a template for Dev Containers](../../integrations/devcontainers/integration.md). +- [mise](https://mise.jdx.dev/): developers install and pin language runtimes + and CLI tools at workspace startup without rebuilding the image. Visit the + [install command-line tools guide](../../../get-started/customize-your-template/install-command-line-tools.md).