feat: improve the image management experience with template builder (#27018)

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 <jeremyruppel@users.noreply.github.com>
This commit is contained in:
Ben Potter
2026-07-06 20:37:34 +00:00
committed by GitHub
co-authored by Jeremy Ruppel
parent bf58e8a402
commit 7b19ec3933
12 changed files with 165 additions and 39 deletions
@@ -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:
+13 -2
View File
@@ -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
}
]
}
@@ -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)}"
@@ -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
@@ -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",
@@ -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]
+2 -2
View File
@@ -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)
})
+54
View File
@@ -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.
+1 -1
View File
@@ -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:~$
+1 -1
View File
@@ -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 {
+3 -2
View File
@@ -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",