mirror of
https://github.com/coder/coder.git
synced 2026-09-23 05:43:53 +08:00
Lands the first concrete rule under the `Coder` style: `Coder.BrandNames`, a bundled `substitution` rule that enforces canonical brand casing in prose. HashiCorp is the first entry; [DOCS-188](https://linear.app/codercom/issue/DOCS-188) extends it with GitHub, OpenTofu, Kubernetes, Terraform, JetBrains, and VS Code. ## What changes Four commits, ordered so each is independently valid: 1. **`docs: fix HashiCorp casing in prose and sidebar`** ([06d769dad1](https://github.com/coder/coder/pull/25501/commits/06d769dad179cf85c535b058df3b6bafdc1f9565)). 5 Markdown files plus 2 `docs/manifest.json` entries. Drives the corpus violation count to zero. 2. **`feat(docs/.style/styles/Coder): add Coder.BrandNames Vale rule`** ([e00fc780a7](https://github.com/coder/coder/pull/25501/commits/e00fc780a7a20dcf82105d997af5cfcddd4b1855)). New `BrandNames.yml` with the HashiCorp swap at `level: error`, plus a new `### Brand names` subsection in `docs/.style/style-guide.md`. 3. **`docs(.style/styles/Coder/README.md): scrub planned-rules notes obsoleted by Coder.BrandNames`** ([af8833b9f5](https://github.com/coder/coder/pull/25501/commits/af8833b9f58dd617732ae533bbf53eb4fc2e816a)). Removes the README's "intentionally empty for now" lead-in and the obsolete HashiCorp casing bullet from the planned-coverage list. 4. **`docs: apply semantic line breaks and fix Vale findings on PR-touched files`** ([e9f11df188](https://github.com/coder/coder/pull/25501/commits/e9f11df1886fdf0d5efc5e8a2cab95fecbd898f9)). Pre-review pass on every Markdown file this PR modifies. Full sembr and Vale-warning cleanup on the style-guide infrastructure (`style-guide.md`, `Coder/README.md`); sembr applied to the HashiCorp swap paragraph only on the five product docs, per scoping discussion with @nickvigilante. ## Severity rationale `error` from day one. HashiCorp's brand owner publishes a canonical casing; any other casing in prose is wrong, not a judgment call. Matches the `error = low FPs x high gravity` framework. False-positive rate is effectively zero because Vale's `substitution` rule skips inline code, fenced code blocks, and URLs by default, so `hashicorp/kubernetes` (Terraform provider source) and `developer.hashicorp.com` stay untouched. ## Verification - `make lint/markdown`: 0 errors across 487 files. - `make lint/prose`: 1 error, 1 warning, 1 suggestion in 468 files. All three findings are the intentional `Coder.DemoError`, `Coder.DemoWarning`, and `Coder.DemoSuggestion` annotations on `docs/.style/style-guide/demo/demo.md` (added on main as part of the [DOCS-425](https://linear.app/codercom/issue/DOCS-425) inline-annotation demo), not real findings. `Coder.BrandNames` fires zero times against the cleaned-up corpus. - `make pre-commit-light`: passed (7s). - Self-test: ran the rule against an unmodified `docs/` and confirmed it flags the 7 prose instances the cleanup commit fixes, then re-ran against the post-cleanup state and confirmed zero alerts. ## Known future conflict When [#26632](https://github.com/coder/coder/pull/26632) ([DOCS-434](https://linear.app/codercom/issue/DOCS-434)) merges, the monolithic `docs/.style/style-guide.md` is split into the `docs/.style/style-guide/` multi-page structure. The `### Brand names` subsection added in commit 2 will need to land in `docs/.style/style-guide/word-choice.md` (which already references the rule), and the `link:` in `docs/.style/styles/Coder/BrandNames.yml` will need to update from `style-guide.md#brand-names` to `style-guide/word-choice.md#brand-names`. Resolution path documented in an inline comment on this PR. <details> <summary>Implementation plan and decision log</summary> ### Why bundle into Coder.BrandNames rather than one file per brand Vale's convention (mirrored by `Google.WordList` with ~70 swaps in a single file) is to bundle `substitution` rules when they share severity, message template, and link. All brand-name rules share that shape: `error`, `Use '%s' instead of '%s'`, link to the style guide section. Bundling reduces "add a brand" to a one-line YAML diff and keeps `CODEOWNERS` and blame coherent. Per-rule performance is irrelevant at this scale; Vale's per-rule overhead is sub-millisecond and dwarfed by Markdown parsing. ### Why the cleanup lands first Commits are ordered cleanup-then-rule so each commit is a known-good state: - After commit 1: corpus is HashiCorp-clean, but no rule exists yet. - After commit 2: rule exists and lints a clean corpus. Reversing the order would land the rule at commit 1 (firing 7 errors on uncleaned content) and resolve them at commit 2. Under `--no-exit` the CI job still passes, but the inline annotations on commit 1 would be misleading. ### Why HashiCorp first instead of all brands at once Proof-of-concept value. HashiCorp is the smallest cleanup (7 prose lines plus 2 sidebar lines = 9 lines), zero FPs, zero ambiguity. Once the loop (rule plus cleanup plus style-guide section) is proven, [DOCS-188](https://linear.app/codercom/issue/DOCS-188) appends the other brands as additional commits to the same bundle. ### Brand-token sensitivity The `swap:` table only matches: - `Hashicorp` (capital H, lowercase rest), the actual wrong form in the corpus. - `HASHICORP` (all caps), defensive; doesn't appear in current corpus but cheap to include. `hashicorp` (all lowercase) is **not** in the swap table. The lowercase form appears 49 times in URLs (`developer.hashicorp.com`, `registry.terraform.io/providers/hashicorp/...`, `github.com/hashicorp/...`) and 6 times as Terraform provider sources (`source = "hashicorp/kubernetes"`), all of which are correct lowercase by convention. Vale's substitution rule scope ensures URLs and code blocks are skipped, but skipping the rule entirely for `hashicorp` (lowercase) is the explicit decision; if a prose typo of lowercase "hashicorp" ever shows up, we'd catch it through `Vale.Spelling` ([DOCS-187](https://linear.app/codercom/issue/DOCS-187)) instead. ### Self-reference in the style guide The `### Brand names` section's example table needed `Hashicorp` and `HashiCorp` as literal demonstration tokens. Wrapping them in backticks (`` `Hashicorp` ``, `` `HashiCorp` ``) keeps Vale from flagging the wrong-case example as a real violation. This is correct typography too: demonstration tokens get code formatting. ### Manifest.json Vale doesn't lint JSON, so the two `docs/manifest.json` entries are fixed by direct edit rather than tool enforcement. The sidebar `path` (`./admin/integrations/vault.md`) is unchanged; the title change does not affect the page URL on coder.com. No redirect needed in `coder/coder.com:redirects.json`. ### Pre-mortem - **Generated docs noise**: `Coder.BrandNames` does not fire on auto-generated `docs/reference/` content because no codersdk identifier matches the swap pattern. Zero risk. - **Future-additions friction**: adding GitHub to the swap table is one YAML line and a cleanup commit. The bundling shape pays off here. - **Disable footgun**: if a contributor needs to write the wrong casing on purpose (quoting an external bug report verbatim, for example), they can wrap the literal in backticks (already correct typography) or use the per-line Vale skip comment. </details> Closes [DOCS-34](https://linear.app/codercom/issue/DOCS-34). --- *Filed via [Coder Agents](https://coder.com/docs/ai-coder/agents) on Nick's behalf.*
444 lines
14 KiB
Markdown
444 lines
14 KiB
Markdown
# Write a template from scratch
|
|
|
|
> [!TIP]
|
|
> If you want to create a template without writing Terraform, use the
|
|
> [template builder](../admin/templates/creating-templates.md#template-builder)
|
|
> instead. The builder guides you through selecting base infrastructure and
|
|
> adding modules through a visual interface.
|
|
|
|
A template is a common configuration that you use to deploy workspaces.
|
|
|
|
This tutorial teaches you how to create a template that provisions a workspace
|
|
as a Docker container with Ubuntu.
|
|
|
|
## Before you start
|
|
|
|
You'll need a computer or cloud computing instance with both
|
|
[Docker](https://docs.docker.com/get-docker/) and [Coder](../install/index.md)
|
|
installed on it.
|
|
|
|
## What's in a template
|
|
|
|
The main part of a Coder template is a [Terraform](https://terraform.io) `tf`
|
|
file. A Coder template often has other files to configure the other resources
|
|
that the template needs. In this tour you'll also create a `Dockerfile`.
|
|
|
|
Coder can provision all Terraform modules, resources, and properties. The Coder
|
|
server essentially runs a `terraform apply` every time a workspace is created,
|
|
started, or stopped.
|
|
|
|
> [!TIP]
|
|
> Haven't written Terraform before?
|
|
> Check out HashiCorp's [Getting Started Guides](https://developer.hashicorp.com/terraform/tutorials).
|
|
|
|
Here's a simplified diagram that shows the main parts of the template we'll
|
|
create:
|
|
|
|

|
|
|
|
## 1. Create template files
|
|
|
|
On your local computer, create a directory for your template and create the
|
|
`Dockerfile`. You will upload the files to your Coder instance later.
|
|
|
|
```sh
|
|
mkdir -p template-tour/build && cd $_
|
|
```
|
|
|
|
Enter content into a `Dockerfile` that starts with the
|
|
[official Ubuntu image](https://hub.docker.com/_/ubuntu/). In your editor, enter
|
|
and save the following text in `Dockerfile` then exit the editor:
|
|
|
|
```dockerfile
|
|
FROM ubuntu
|
|
|
|
RUN apt-get update \
|
|
&& apt-get install -y \
|
|
sudo \
|
|
curl \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
ARG USER=coder
|
|
RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \
|
|
&& echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \
|
|
&& chmod 0440 /etc/sudoers.d/${USER}
|
|
USER ${USER}
|
|
WORKDIR /home/${USER}
|
|
```
|
|
|
|
`Dockerfile` adds a few things to the parent `ubuntu` image, which your template
|
|
needs later:
|
|
|
|
- It installs the `sudo` and `curl` packages.
|
|
- It adds a `coder` user, including a home directory.
|
|
|
|
## 2. Set up template providers
|
|
|
|
Edit the Terraform `main.tf` file to provision the workspace's resources.
|
|
|
|
Start by setting up the providers. At a minimum, we need the `coder` provider.
|
|
For this template, we also need the `docker` provider:
|
|
|
|
```tf
|
|
terraform {
|
|
required_providers {
|
|
coder = {
|
|
source = "coder/coder"
|
|
}
|
|
docker = {
|
|
source = "kreuzwerker/docker"
|
|
}
|
|
}
|
|
}
|
|
|
|
locals {
|
|
username = data.coder_workspace_owner.me.name
|
|
}
|
|
|
|
data "coder_provisioner" "me" {
|
|
}
|
|
|
|
provider "docker" {
|
|
}
|
|
|
|
provider "coder" {
|
|
}
|
|
|
|
data "coder_workspace" "me" {
|
|
}
|
|
|
|
data "coder_workspace_owner" "me" {
|
|
}
|
|
```
|
|
|
|
Notice that the `provider` blocks for `coder` and `docker` are empty. In a more
|
|
practical template, you would add arguments to these blocks to configure the
|
|
providers, if needed.
|
|
|
|
The
|
|
[`coder_workspace`](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace)
|
|
data source provides details about the state of a workspace, such as its name,
|
|
owner, and so on. The data source also lets us know when a workspace is being
|
|
started or stopped. We'll use this information in later steps to:
|
|
|
|
- Set some environment variables based on the workspace owner.
|
|
- Manage ephemeral and persistent storage.
|
|
|
|
## 3. coder_agent
|
|
|
|
All templates need to create and run a
|
|
[Coder agent](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent).
|
|
This lets developers connect to their workspaces. The `coder_agent` resource
|
|
runs inside the compute aspect of your workspace, typically a VM or container.
|
|
In our case, it will run in Docker.
|
|
|
|
You do not need to have any open ports on the compute aspect, but the agent
|
|
needs `curl` access to the Coder server.
|
|
|
|
Add this snippet after the last closing `}` in `main.tf` to create the agent:
|
|
|
|
```tf
|
|
resource "coder_agent" "main" {
|
|
arch = data.coder_provisioner.me.arch
|
|
os = "linux"
|
|
startup_script = <<-EOT
|
|
set -e
|
|
|
|
# install and start code-server
|
|
curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server
|
|
/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &
|
|
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
|
|
}
|
|
}
|
|
```
|
|
|
|
Because Docker is running locally in the Coder server, there is no need to
|
|
authenticate `coder_agent`. But if your `coder_agent` is running on a remote
|
|
host, your template will need
|
|
[authentication credentials](../admin/external-auth/index.md).
|
|
|
|
This template's agent also runs a startup script, sets environment variables,
|
|
and provides metadata.
|
|
|
|
- [`startup script`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)
|
|
|
|
- Installs [code-server](https://coder.com/docs/code-server), a browser-based
|
|
[VS Code](https://code.visualstudio.com/) app that runs in the workspace.
|
|
|
|
We'll give users access to code-server through `coder_app` later.
|
|
|
|
- [`env` block](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#env)
|
|
|
|
- Sets environments variables for the workspace.
|
|
|
|
We use the data source from `coder_workspace` to set the environment
|
|
variables based on the workspace's owner. This way, the owner can make git
|
|
commits immediately without any manual configuration.
|
|
|
|
- [`metadata`](../admin/templates/extending-templates/agent-metadata.md) blocks
|
|
|
|
- Your template can use metadata to show information to the workspace owner
|
|
Coder displays this metadata in the Coder dashboard.
|
|
|
|
Our template has `metadata` blocks for CPU and RAM usage.
|
|
|
|
## 4. coder_app
|
|
|
|
A
|
|
[`coder_app`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app)
|
|
resource lets a developer use an app from the workspace's Coder dashboard.
|
|
|
|

|
|
|
|
This is commonly used for
|
|
[web IDEs](../user-guides/workspace-access/web-ides.md) such as
|
|
[code-server](https://coder.com/docs/code-server), RStudio, and JupyterLab.
|
|
|
|
We installed code-server in the `startup_script` argument. To add code-server to
|
|
the workspace, make it available in the workspace with a `coder_app` resource.
|
|
See [web IDEs](../user-guides/workspace-access/web-ides.md) for more examples:
|
|
|
|
```tf
|
|
resource "coder_app" "code-server" {
|
|
agent_id = coder_agent.main.id
|
|
slug = "code-server"
|
|
display_name = "code-server"
|
|
url = "http://localhost:13337/?folder=/home/${local.username}"
|
|
icon = "/icon/code.svg"
|
|
subdomain = false
|
|
share = "owner"
|
|
|
|
healthcheck {
|
|
url = "http://localhost:13337/healthz"
|
|
interval = 5
|
|
threshold = 6
|
|
}
|
|
}
|
|
```
|
|
|
|
You can also use a `coder_app` resource to link to external apps, such as links
|
|
to wikis or cloud consoles:
|
|
|
|
```tf
|
|
resource "coder_app" "coder-server-doc" {
|
|
agent_id = coder_agent.main.id
|
|
icon = "/emojis/1f4dd.png"
|
|
slug = "getting-started"
|
|
url = "https://coder.com/docs/code-server"
|
|
external = true
|
|
}
|
|
```
|
|
|
|
## 5. Persistent and ephemeral resources
|
|
|
|
Managing the lifecycle of template resources is important. We want to make sure
|
|
that workspaces use computing, storage, and other services efficiently.
|
|
|
|
We want our workspace's home directory to persist after the workspace is stopped
|
|
so that a developer can continue their work when they start the workspace again.
|
|
|
|
We do this in 2 parts:
|
|
|
|
- Our `docker_volume` resource uses the `lifecycle` block with the
|
|
`ignore_changes = all` argument to prevent accidental deletions.
|
|
- To prevent Terraform from destroying persistent Docker volumes in case of a
|
|
workspace name change, we use an immutable parameter, like
|
|
`data.coder_workspace.me.id`.
|
|
|
|
Later, we use the Terraform
|
|
[count](https://developer.hashicorp.com/terraform/language/meta-arguments/count)
|
|
meta-argument to make sure that our Docker container is ephemeral.
|
|
|
|
```tf
|
|
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
|
|
}
|
|
}
|
|
```
|
|
|
|
For details, see
|
|
[Resource persistence](../admin/templates/extending-templates/resource-persistence.md).
|
|
|
|
## 6. Set up the Docker container
|
|
|
|
To set up our Docker container, our template has a `docker_image` resource that
|
|
uses `build/Dockerfile`, which we created earlier:
|
|
|
|
```tf
|
|
resource "docker_image" "main" {
|
|
name = "coder-${data.coder_workspace.me.id}"
|
|
build {
|
|
context = "./build"
|
|
build_args = {
|
|
USER = local.username
|
|
}
|
|
}
|
|
triggers = {
|
|
dir_sha1 = sha1(join("", [for f in fileset(path.module, "build/*") : filesha1(f)]))
|
|
}
|
|
}
|
|
```
|
|
|
|
Our `docker_container` resource uses `coder_workspace` `start_count` to start
|
|
and stop the Docker container:
|
|
|
|
```tf
|
|
resource "docker_container" "workspace" {
|
|
count = data.coder_workspace.me.start_count
|
|
image = docker_image.main.name
|
|
# 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
|
|
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/${local.username}"
|
|
volume_name = docker_volume.home_volume.name
|
|
read_only = false
|
|
}
|
|
}
|
|
```
|
|
|
|
## 7. Create the template in Coder
|
|
|
|
Save `main.tf` and exit the editor.
|
|
|
|
Now that we've created the files for our template, we can add them to our Coder
|
|
deployment.
|
|
|
|
We can do this with the Coder CLI or the Coder dashboard. In this example, we'll
|
|
use the Coder CLI.
|
|
|
|
1. Log in to your Coder deployment from the CLI. This is where you need the URL
|
|
for your deployment:
|
|
|
|
```console
|
|
$ coder login https://coder.example.com
|
|
Attempting to authenticate with config URL: 'https://coder.example.com'
|
|
Open the following in your browser:
|
|
|
|
https://coder.example.com/cli-auth
|
|
|
|
> Paste your token here:
|
|
```
|
|
|
|
1. In your web browser, enter your credentials:
|
|
|
|

|
|
|
|
1. Copy the session token to the clipboard:
|
|
|
|

|
|
|
|
1. Paste it into the CLI:
|
|
|
|
```output
|
|
> Welcome to Coder, marc! You're authenticated.
|
|
$
|
|
```
|
|
|
|
### Add the template files to Coder
|
|
|
|
Add your template files to your Coder deployment. You can upload the template
|
|
through the CLI, or through the Coder dashboard:
|
|
|
|
<div class="tabs">
|
|
|
|
#### CLI
|
|
|
|
1. Run `coder templates push` from the directory with your template files:
|
|
|
|
```console
|
|
$ pwd
|
|
/home/docs/template-tour
|
|
$ coder templates push
|
|
> Upload "."? (yes/no) yes
|
|
```
|
|
|
|
1. The Coder CLI tool gives progress information then prompts you to confirm:
|
|
|
|
```console
|
|
> Confirm create? (yes/no) yes
|
|
|
|
The template-tour template has been created! Developers can provision a workspace with this template using:
|
|
|
|
coder create --template="template-tour" [workspace name]
|
|
```
|
|
|
|
1. In your web browser, log in to your Coder dashboard, select **Templates**.
|
|
|
|
1. Once the upload completes, select **Templates** from the top to deploy it to
|
|
a new workspace.
|
|
|
|

|
|
|
|
#### Dashboard
|
|
|
|
1. Create a `.zip` of the template files.
|
|
|
|
- On Mac or Windows, highlight the files and then right click. A "compress"
|
|
option is available through the right-click context menu.
|
|
|
|
- To zip the files through the command line:
|
|
|
|
```shell
|
|
zip templates.zip Dockerfile main.tf
|
|
```
|
|
|
|
1. Select **Templates** from the top of the Coder dashboard, then **Create
|
|
Template**.
|
|
1. Select **Upload template**:
|
|
|
|

|
|
|
|
1. Drag the `.zip` file into the **Upload template** section and fill out the
|
|
details, then select **Create template**.
|
|
|
|

|
|
|
|
1. Once the upload completes, select **Templates** from the top to deploy it to
|
|
a new workspace.
|
|
|
|

|
|
|
|
</div>
|
|
|
|
### Next steps
|
|
|
|
- [Setting up templates](../admin/templates/index.md)
|
|
- [Customizing templates](../admin/templates/extending-templates/index.md)
|
|
- [Troubleshooting template](../admin/templates/troubleshooting.md)
|