Obi Madu's Blog
Back to all articles
InfrastructureAIAI Engineering

Coder Tasks, Workspaces, and OpenCode: A Practical Mental Model

Build a clearer mental model for Coder workspaces, templates, provisioners, and AI coding tasks.

Coder Tasks, Workspaces, and OpenCode: A Practical Mental Model

Coder is one of those tools that becomes much easier to understand once you stop thinking of it as only a remote IDE platform.

At its core, Coder gives you reproducible development environments. A user clicks a button or runs a command, and Coder provisions a workspace from a template. That workspace might run in Docker, Kubernetes, a VM, or another infrastructure target. The developer connects to it with VS Code, JetBrains, a terminal, or a browser-based tool.

Coder Tasks build on that same foundation for AI coding agents. Instead of creating a workspace primarily for a human to use interactively, a task creates an isolated workspace for an agent to run inside.

That distinction is small but important. Coder Tasks are not a separate execution platform. They are Coder workspaces with an AI-agent-oriented interface.

The Basic Coder Model

Coder has three concepts you need to understand first: workspaces, templates, and provisioners.

A workspace is an isolated development environment. It is the thing the developer or agent actually uses.

A template defines how that workspace is created. Coder templates are written in Terraform, which means they can describe containers, Kubernetes pods, cloud VMs, volumes, agents, apps, metadata, and user-configurable parameters.

A provisioner is the process that runs Terraform. This matters more than it first appears. Infrastructure providers run where the provisioner runs, not inside the workspace.

Coder server -> Provisioner runs Terraform -> Docker, Kubernetes, or cloud provider creates workspace

If your template uses the Docker provider, Docker access must be available to the provisioner. If the provisioner is containerized, that container needs access to the Docker socket, a remote Docker host, or whatever Docker connection you configure.

Templates Are Terraform With Coder Data Sources

A minimal template usually declares the Coder provider, an infrastructure provider, workspace data sources, a Coder agent, and the resources that make up the workspace.

terraform {
  required_providers {
    coder = {
      source  = "coder/coder"
      version = ">= 2.13"
    }
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

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

The Coder agent is what lets Coder connect into the workspace, expose apps, run startup scripts, and report status.

The infrastructure provider creates the actual environment. With Docker, that usually means containers and volumes. With Kubernetes, it might mean pods and persistent volumes. With cloud providers, it might mean VMs and disks.

Terraform Variables vs Coder Parameters

One of the easiest places to get confused is the difference between Terraform variables and Coder parameters.

Terraform variables are set by the template administrator when the template is pushed. They are good for infrastructure-level configuration and values users should not choose directly.

Coder parameters are shown to the user when they create a workspace or task. They are good for things like container image, region, CPU size, model choice, or work directory.

ConceptSet bySet whenBest for
Terraform variableTemplate adminTemplate pushSecrets, defaults, infrastructure config
Coder parameterEnd userWorkspace or task creationUser choices

This example shows both.

variable "anthropic_api_key" {
  type      = string
  sensitive = true
}

data "coder_parameter" "container_image" {
  name         = "container_image"
  display_name = "Container Image"
  type         = "string"
  default      = "ubuntu:22.04"
  mutable      = false
}

The coder_parameter block is a data source. During workspace creation, it fetches the user's chosen value from Coder.

There is also an important security detail: sensitive = true prevents Terraform and Coder from casually displaying a value, but it does not magically protect the value inside a running workspace. If you inject a secret into a container as an environment variable, someone with access to that container may be able to read it.

The Two Phases of Template Execution

Coder templates behave differently during template import and workspace creation.

During coder templates push, Coder parses the Terraform, extracts parameters, builds the UI form, requires normal Terraform variable values, and detects whether the template supports tasks.

During workspace creation, Coder shows the extracted form to the user, stores the parameter values, runs terraform apply, and lets data "coder_parameter" fetch those values.

This is why parameters can feel strange at first. They are not normal Terraform variables. They are Coder-managed values that Terraform reads during apply.

Ephemeral parameters add one more rule. If ephemeral = true, the parameter must also be mutable and have a default value.

data "coder_parameter" "api_key" {
  name      = "api_key"
  type      = "string"
  mutable   = true
  ephemeral = true
  default   = ""
}

Ephemeral parameters are useful when you want a value supplied at creation time without storing it in workspace metadata.

What Coder Tasks Add

Coder Tasks provide an interface for running AI coding agents in isolated workspaces.

A user creates a task with a prompt. Coder stores that prompt. Terraform provisions the workspace. The template fetches the task prompt and passes it to the agent integration.

User submits task prompt
        |
        v
Coder stores prompt
        |
        v
Terraform provisions workspace
        |
        v
Agent receives prompt and runs inside workspace

Each task gets its own workspace. That gives the agent an isolated filesystem, dependencies, tools, and runtime. The workspace usually does not need GPUs because the agent talks to external LLM APIs.

A template becomes task-capable when it defines a coder_ai_task resource.

resource "coder_ai_task" "task" {
  app_id = module.opencode.task_app_id
}

The task prompt can be fetched with data "coder_task".

data "coder_task" "me" {}

module "opencode" {
  ai_prompt = data.coder_task.me.prompt
}

From the user's perspective, creating a task can be as simple as this:

coder tasks create \
  --template my-template \
  --parameter container_image="python:3.12" \
  "Refactor the authentication module"

The important part is that the task prompt becomes infrastructure input. The workspace is created around the job the agent needs to perform.

Where OpenCode Fits

The OpenCode Coder module integrates OpenCode into a Coder workspace. It can expose OpenCode as an app, run it in task mode, pass in prompts, and configure authentication or model settings.

A simple module configuration looks like this:

module "opencode" {
  source   = "registry.coder.com/coder-labs/opencode/coder"
  version  = "0.1.1"
  agent_id = coder_agent.main.id
  workdir  = "/home/coder/project"
}

For task usage, it usually connects to data.coder_task.me.prompt and the coder_ai_task resource.

data "coder_task" "me" {}

resource "coder_ai_task" "task" {
  app_id = module.opencode.task_app_id
}

module "opencode" {
  source           = "registry.coder.com/coder-labs/opencode/coder"
  version          = "0.1.1"
  agent_id         = coder_agent.main.id
  workdir          = "/home/coder/project"
  ai_prompt        = data.coder_task.me.prompt
  auth_json        = data.coder_parameter.opencode_auth_json.value
  config_json      = data.coder_parameter.opencode_config_json.value
  opencode_version = "latest"
}

The module also supports CLI-focused usage if you want OpenCode available in the terminal without web UI or task reporting.

module "opencode" {
  source       = "registry.coder.com/coder-labs/opencode/coder"
  version      = "0.1.1"
  agent_id     = coder_agent.main.id
  workdir      = "/home/coder"
  report_tasks = false
  cli_app      = true
}

The practical takeaway is that Coder provides the workspace and lifecycle, while OpenCode provides the agent experience inside that workspace.

Docker Provider Gotchas

Many local Coder setups use Docker, so the Docker Terraform provider becomes part of the template.

By default, the provider talks to the local Unix socket.

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

It can also connect to a remote host over SSH.

provider "docker" {
  host     = "ssh://user@remote-host:22"
  ssh_opts = ["-o", "StrictHostKeyChecking=no"]
}

The key detail is that SSH keys and Docker access must exist where the provisioner runs. If the provisioner is running inside a container, mounting your key into your laptop shell is not enough. The provisioner container needs access to it.

Docker socket permissions are another common issue. If Coder runs in Docker and needs to create sibling containers through the host Docker socket, the Coder container must have access to /var/run/docker.sock and the correct group permissions.

services:
  coder:
    image: ghcr.io/coder/coder:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    group_add:
      - "998"

The group ID must match the Docker group on the host.

Networking can also be surprising. If the workspace container needs to reach a Coder server bound to localhost on the host machine, localhost from inside the container points to the container itself. A common fix is to use host.docker.internal with a host gateway mapping.

Base Images and Workspace Lifecycle

Coder base images exist for a reason. They include a coder user, expected home directory behavior, agent dependencies, skeleton files, and common development tools.

Using a plain image like ubuntu:latest can work, but then you own more setup. You may need to create the user, install tools, configure the home directory, and make sure the Coder agent can start correctly.

Workspace lifecycle also matters. Some resources should only exist when the workspace is started. Coder exposes start_count, which templates often use with count.

resource "docker_container" "workspace" {
  count = data.coder_workspace.me.start_count
}

module "opencode" {
  count = data.coder_workspace.me.start_count
}

Persistent volumes are a separate concern. If you want a user's home directory or workspace data to survive stops and rebuilds, protect the volume carefully.

resource "docker_volume" "home" {
  name = "coder-${data.coder_workspace.me.id}-home"

  lifecycle {
    ignore_changes = all
  }
}

The Mental Model to Keep

Coder is a control plane for development environments. Terraform describes those environments. Provisioners run Terraform. Workspaces are the resulting machines, containers, or pods. Tasks are workspaces created around AI-agent prompts.

OpenCode fits into that model as a tool running inside the workspace. It is not the infrastructure layer. It is the agent layer.

Once that separation is clear, the common confusing parts become easier to debug.

If a Docker resource fails, look at the provisioner and Docker host. If a user parameter is missing, look at the Coder parameter flow. If a task prompt is not reaching the agent, look at data "coder_task" and the module wiring. If a secret appears inside a container, remember that Terraform sensitivity is not runtime secrecy.

Coder Tasks are powerful because they combine reproducible infrastructure with agent execution. The cost is that you need to understand both halves: the Coder/Terraform provisioning model and the agent integration running inside the resulting workspace.

References