Skip to main content
HomeGet startedCustomize your templateAdd a programming language

Add a programming language

Now that you've finished Launch your first workspace, you can add another language toolchain to every workspace you create.

The Quickstart template installs a language only when the workspace owner selects it from the Programming Languages parameter. In this guide, you add Ruby as an option.

Note

This guide assumes your Quickstart template is open for editing. If it's not, you can edit the template from the web by finding the template, selecting the three dots menu, and selecting Edit files. Refer to Customize workspace startup for more information.

What you'll do

  • ✅ Add a Ruby option to the Programming Languages parameter.
  • ✅ Publish the change and apply it to your running workspace.
  • ✅ Install the Ruby toolchain so selecting Ruby works.

Parameters in brief

A parameter is a question Coder asks when someone creates a workspace. Each parameter comes from a coder_parameter data source in the template. The Programming Languages parameter is a multi-select list, and each language the reader can choose is an option block:

data "coder_parameter" "languages" { name = "languages" display_name = "Programming Languages" description = "Select the languages to pre-install in your workspace" type = "list(string)" form_type = "multi-select" default = jsonencode(["python"]) mutable = true icon = "/icon/code.svg" order = 1 option { name = "Python" value = "python" icon = "/icon/python.svg" } # ...more options }

This produces the following dropdown parameter:

Programming Language dropdown parameter from the Quickstart templateProgramming Language dropdown parameter from the Quickstart template

Adding an option adds a choice to that list.

Step 1: Add the Ruby option

In main.tf, find the data "coder_parameter" "languages" block. Add a Ruby option after the last existing option, before the parameter's closing brace:

option { name = "Ruby" value = "ruby" icon = "/icon/ruby.svg" }

Important

The option block must sit at the same indentation as the other option blocks inside the parameter. Coder reads the parameter's choices from these blocks, so a misplaced option doesn't appear in the form.

The icon value points at an icon bundled with your Coder deployment. To browse the full set and copy a path, open https://<your-coder-url>/icons.

Now publish the change as a new template version:

In the web editor, make the edit above in main.tf. Select Build, wait for the build to pass, then select Publish.

Step 2: Add Ruby to your workspace

Your workspace from Launch your first workspace is still on the old template version. Update it to the version you just published, and add Ruby to its Programming Languages selection:

  1. From your workspace, open Workspace settings (top right), then select Parameters.
  2. Add Ruby to the Programming Languages parameter.
  3. Apply the change. Coder updates the workspace to the new template version and restarts it with Ruby selected.

Adding the Ruby option to the parameter shows the following:

Programming Language dropdown parameter from the Quickstart template with RubyProgramming Language dropdown parameter from the Quickstart template with Ruby

Step 3: Check whether Ruby is installed

When the workspace restarts, open a terminal and ask for the Ruby version:

ruby --version

The command fails:

ruby: command not found

You added the option, selected it, and rebuilt the workspace, but Ruby isn't there. Adding the option only changed the form. It added Ruby to the list of choices, but nothing in the template acts on that choice yet. A separate startup script installs each selected toolchain, and you haven't taught it about Ruby.

Step 4: Install Ruby when the workspace starts

The template installs each selected language from install-languages.sh.tftpl, a startup script that runs when the workspace boots. Open that file and add a branch that installs Ruby when the reader selects it:

if echo "$LANGUAGES" | grep -q "ruby"; then if command -v ruby >/dev/null 2>&1; then echo "Ruby: $(ruby --version | head -1)" else echo "Installing Ruby toolchain..." apt_update sudo apt-get install -y -qq ruby-full echo "Installed Ruby: $(ruby --version | head -1)" fi fi

The script installs Ruby with apt-get, the package manager built into the workspace image.

Warning

Use the package manager the workspace image provides, not a personal one. If you replace the apt-get line with brew install ruby, the build fails: the codercom/enterprise-base:ubuntu image doesn't include Homebrew, so the workspace logs brew: command not found and Ruby never installs. To install a personal tool like a Homebrew formula in your own workspace, refer to Install your own command-line tools.

Publish the change, then update your workspace again:

  1. In the web editor, make the edit above in install-languages.sh.tftpl.
  2. Select Build, wait for the build to pass, then select Publish.
  3. On your workspace's home tab, select Update and restart.

Ruby is already selected, so you don't change parameters this time. When the workspace restarts, open a terminal and check again:

ruby --version

This time the workspace reports a Ruby version.

Step 5: Add a Ruby preset

Steps 1 through 4 make Ruby selectable as a parameter and install it at startup. The Quickstart template also ships presets: named bundles of parameter values that appear on the workspace creation form:

Default presets with the Quickstart templateDefault presets with the Quickstart template

The shipped presets cover combinations like Go and Python, but none selects Ruby, so add one to keep the parameter and preset choices in sync.

In main.tf, find the # --- Presets --- section and add a Ruby preset next to the existing ones:

data "coder_workspace_preset" "ruby_dev" { name = "Ruby Development" icon = "/icon/ruby.svg" parameters = { languages = jsonencode(["ruby"]) ides = jsonencode(["code-server", "jetbrains"]) jetbrains_ides = jsonencode(["RM"]) git_repo = "" } }

This preset adds JetBrains RubyMine as a default option for the language, but you can customize the preferred IDEs in this default based on your needs.

Publish the template again. The next time you create a workspace, Ruby Development appears in the preset list and preselects Ruby:

Updated presets with the Quickstart templateUpdated presets with the Quickstart template

Expanding the preset parameters after selecting Ruby Development shows the parameter values Coder will use to create the workspace:

Expanded parameter values for the Ruby Development presetExpanded parameter values for the Ruby Development preset

What just happened

You changed two different things to add one language:

  • The coder_parameter option block added Ruby to the workspace creation form.
  • The startup script installed the Ruby toolchain when a workspace owner selected Ruby.

A parameter collects a choice. A startup script acts on it. A new language needs both.

You also added support for a new preset, so workspace users can launch a workspace from this template quickly, without having to fill out the parameters manually.

How does Coder install the language you pick?

The selection travels through the template in four steps:

  1. The option block in data "coder_parameter" "languages" (main.tf) adds ruby to the values the form accepts.

  2. When the workspace builds, local.languages decodes the selection in main.tf:

    languages = jsondecode(data.coder_parameter.languages.value)
  3. coder_script.install_languages renders the startup script with that list and runs it on the agent (also in main.tf):

    script = templatefile("${path.module}/install-languages.sh.tftpl", { LANGUAGES = join(",", local.languages) })
  4. Inside the rendered script, the ruby branch matches and installs the toolchain:

    if echo "$LANGUAGES" | grep -q "ruby"; then

The first three steps ran as soon as you added the option, which is why Ruby appeared in the form. Step 4 is the part you were missing in Step 3, so the script had nothing to do for ruby.

Why a .tftpl file instead of a plain script?

The install script needs to know which languages the workspace owner selected, and only Terraform has that value when the workspace builds. templatefile() renders install-languages.sh.tftpl and replaces ${LANGUAGES} with join(",", local.languages), producing a finished script with the selection baked in. A static .sh file couldn't receive that value, so the .tftpl is the bridge between the parameter and the shell script.

Final code

The complete template files

Your template files after this guide's changes, starting from the Quickstart template:

terraform { required_providers { coder = { source = "coder/coder" } docker = { source = "kreuzwerker/docker" } external = { source = "hashicorp/external" } } } variable "docker_socket" { default = "" description = "(Optional) Docker socket URI" type = string } provider "docker" { host = var.docker_socket != "" ? var.docker_socket : null } data "coder_provisioner" "me" {} data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} # --- Parameters --- data "coder_parameter" "languages" { name = "languages" display_name = "Programming Languages" description = "Select the languages to pre-install in your workspace" type = "list(string)" form_type = "multi-select" default = jsonencode(["python"]) mutable = true icon = "/icon/code.svg" order = 1 option { name = "Python" value = "python" icon = "/icon/python.svg" } option { name = "Node.js" value = "nodejs" icon = "/icon/nodejs.svg" } option { name = "Go" value = "go" icon = "/icon/go.svg" } option { name = "Rust" value = "rust" icon = "/icon/rust.svg" } option { name = "Java" value = "java" icon = "/icon/java.svg" } option { name = "C/C++" value = "cpp" icon = "/icon/cpp.svg" } option { name = "Ruby" value = "ruby" icon = "/icon/ruby.svg" } } data "coder_parameter" "ides" { name = "ides" display_name = "IDEs & Editors" description = "Select the development environments for your workspace" type = "list(string)" form_type = "multi-select" default = jsonencode(["code-server"]) mutable = true icon = "/icon/code.svg" order = 2 option { name = "VS Code (Browser)" value = "code-server" icon = "/icon/code.svg" } option { name = "Cursor" value = "cursor" icon = "/icon/cursor.svg" } option { name = "JetBrains IDEs" value = "jetbrains" icon = "/icon/jetbrains.svg" } option { name = "Zed" value = "zed" icon = "/icon/zed.svg" } option { name = "Windsurf" value = "windsurf" icon = "/icon/windsurf.svg" } } # Shown only when "JetBrains IDEs" is selected in the IDEs parameter. # Pre-selects IDEs that match the chosen languages. data "coder_parameter" "jetbrains_ides" { count = contains(local.ides, "jetbrains") ? 1 : 0 name = "jetbrains_ides" display_name = "JetBrains IDEs" description = "Select the JetBrains IDEs to install" type = "list(string)" form_type = "multi-select" default = jsonencode(local.jetbrains_ides_from_languages) mutable = true icon = "/icon/jetbrains.svg" order = 3 option { name = "IntelliJ IDEA" value = "IU" icon = "/icon/intellij.svg" } option { name = "PyCharm" value = "PY" icon = "/icon/pycharm.svg" } option { name = "GoLand" value = "GO" icon = "/icon/goland.svg" } option { name = "WebStorm" value = "WS" icon = "/icon/webstorm.svg" } option { name = "RustRover" value = "RR" icon = "/icon/rustrover.svg" } option { name = "CLion" value = "CL" icon = "/icon/clion.svg" } option { name = "PhpStorm" value = "PS" icon = "/icon/phpstorm.svg" } option { name = "RubyMine" value = "RM" icon = "/icon/rubymine.svg" } option { name = "Rider" value = "RD" icon = "/icon/rider.svg" } } data "coder_parameter" "git_repo" { name = "git_repo" display_name = "Git Repository (Optional)" description = "URL of a Git repository to clone into your workspace (leave empty to skip)" type = "string" default = "" mutable = true icon = "/icon/git.svg" order = 4 } # --- Locals --- locals { username = data.coder_workspace_owner.me.name languages = jsondecode(data.coder_parameter.languages.value) ides = jsondecode(data.coder_parameter.ides.value) # Map selected languages to the relevant JetBrains IDE product codes. # Used as the default for the JetBrains IDE selector parameter. jetbrains_by_language = { python = ["PY"] go = ["GO"] java = ["IU"] nodejs = ["WS"] rust = ["RR"] cpp = ["CL"] } jetbrains_ides_from_languages = distinct(flatten([ for lang in local.languages : lookup(local.jetbrains_by_language, lang, []) ])) # The actual JetBrains IDEs to install, from the user's selection # in the conditional JetBrains parameter (or empty if not shown). jetbrains_selected = contains(local.ides, "jetbrains") ? jsondecode(data.coder_parameter.jetbrains_ides[0].value) : [] } # --- Agent --- resource "coder_agent" "main" { arch = data.coder_provisioner.me.arch os = "linux" startup_script = <<-EOT set -e if [ ! -f ~/.init_done ]; then cp -rT /etc/skel ~ touch ~/.init_done fi EOT 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}" } 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 } } # --- Language installation --- # All languages install in a single script to avoid apt-get lock # conflicts (coder_script resources run in parallel). resource "coder_script" "install_languages" { count = length(local.languages) > 0 ? 1 : 0 agent_id = coder_agent.main.id display_name = "Install Languages" icon = "/icon/code.svg" run_on_start = true start_blocks_login = true script = templatefile("${path.module}/install-languages.sh.tftpl", { LANGUAGES = join(",", local.languages) }) } # --- IDE modules --- module "code-server" { count = data.coder_workspace.me.start_count * (contains(local.ides, "code-server") ? 1 : 0) source = "registry.coder.com/coder/code-server/coder" version = "~> 1.0" agent_id = coder_agent.main.id order = 1 } module "cursor" { count = data.coder_workspace.me.start_count * (contains(local.ides, "cursor") ? 1 : 0) source = "registry.coder.com/coder/cursor/coder" version = "~> 1.0" agent_id = coder_agent.main.id folder = "/home/coder" order = 3 } # TODO: Re-add the coder/jetbrains module once Coder's dynamic # parameter system respects module count for parameter visibility. # The module's internal coder_parameter appears even when count = 0, # creating a ghost parameter in the workspace creation form. # module "jetbrains" { # count = data.coder_workspace.me.start_count * (contains(local.ides, "jetbrains") && length(local.jetbrains_selected) > 0 ? 1 : 0) # source = "registry.coder.com/coder/jetbrains/coder" # version = "~> 1.0" # agent_id = coder_agent.main.id # folder = "/home/coder" # default = toset(local.jetbrains_selected) # } module "zed" { count = data.coder_workspace.me.start_count * (contains(local.ides, "zed") ? 1 : 0) source = "registry.coder.com/coder/zed/coder" version = "~> 1.0" agent_id = coder_agent.main.id folder = "/home/coder" order = 5 } module "windsurf" { count = data.coder_workspace.me.start_count * (contains(local.ides, "windsurf") ? 1 : 0) source = "registry.coder.com/coder/windsurf/coder" version = "~> 1.0" agent_id = coder_agent.main.id folder = "/home/coder" order = 6 } # --- Git clone --- module "git-clone" { count = data.coder_workspace.me.start_count * (data.coder_parameter.git_repo.value != "" ? 1 : 0) source = "registry.coder.com/coder/git-clone/coder" version = "~> 2.0" agent_id = coder_agent.main.id url = data.coder_parameter.git_repo.value } # --- Presets --- data "coder_workspace_preset" "web_dev" { name = "Web Development" icon = "/icon/nodejs.svg" parameters = { languages = jsonencode(["python", "nodejs"]) ides = jsonencode(["code-server"]) git_repo = "" } } data "coder_workspace_preset" "backend_go" { name = "Backend (Go)" icon = "/icon/go.svg" parameters = { languages = jsonencode(["go"]) ides = jsonencode(["code-server", "jetbrains"]) jetbrains_ides = jsonencode(["GO"]) git_repo = "" } } data "coder_workspace_preset" "data_science" { name = "Data Science" icon = "/icon/python.svg" parameters = { languages = jsonencode(["python"]) ides = jsonencode(["code-server"]) git_repo = "" } } data "coder_workspace_preset" "full_stack" { name = "Full Stack" icon = "/icon/code.svg" parameters = { languages = jsonencode(["python", "nodejs", "go"]) ides = jsonencode(["code-server", "cursor"]) git_repo = "" } } data "coder_workspace_preset" "ruby_dev" { name = "Ruby Development" icon = "/icon/ruby.svg" parameters = { languages = jsonencode(["ruby"]) ides = jsonencode(["code-server", "jetbrains"]) jetbrains_ides = jsonencode(["RM"]) git_repo = "" } } # --- Docker resources --- resource "docker_volume" "home_volume" { name = "coder-${data.coder_workspace.me.id}-home" lifecycle { ignore_changes = all } 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_at_creation" value = data.coder_workspace.me.name } depends_on = [] } resource "docker_container" "workspace" { count = data.coder_workspace.me.start_count image = "codercom/enterprise-base:ubuntu" name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" hostname = data.coder_workspace.me.name entrypoint = [ "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" } volumes { container_path = "/home/coder" volume_name = docker_volume.home_volume.name read_only = false } 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 } depends_on = [] }

What's next?

Now that you added a language, you can install your own command-line tools.

Learn more