Skip to content

Commit 3fb4a54

Browse files
committed
Add Hetzner Cloud server template example
1 parent 6acded5 commit 3fb4a54

File tree

8 files changed

+314
-0
lines changed

8 files changed

+314
-0
lines changed

.github/typos.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ muc = "muc" # For Munich location code
33
tyo = "tyo" # For Tokyo location code
44
Hashi = "Hashi"
55
HashiCorp = "HashiCorp"
6+
hel = "hel" # For Helsinki location code
67
mavrickrishi = "mavrickrishi" # Username
78
mavrick = "mavrick" # Username
89
inh = "inh" # Option in setpriv command

.icons/hetzner.svg

Lines changed: 5 additions & 0 deletions
Loading
18.2 KB
Binary file not shown.

registry/Excellencedev/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
display_name: "Excellencedev"
3+
bio: "Love to contribute"
4+
avatar: "./.images/avatar.jfif"
5+
support_email: "[email protected]"
6+
status: "community"
7+
---
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
display_name: Hetzner Cloud Server
3+
description: Provision Hetzner Cloud servers as Coder workspaces
4+
icon: ../../../../.icons/hetzner.svg
5+
tags: [vm, linux, hetzner]
6+
---
7+
8+
# Remote Development on Hetzner Cloud (Linux)
9+
10+
Provision Hetzner Cloud servers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
11+
12+
> [!IMPORTANT]
13+
> **Volume Management & Costs:** Hetzner Cloud volumes persist even when workspaces are stopped and will continue to incur storage costs (€0.0476/GB/month). Volumes are only automatically deleted when the workspace is completely deleted. Monitor your volumes in the [Hetzner Cloud Console](https://console.hetzner.cloud/) to manage costs effectively.
14+
15+
## Prerequisites
16+
17+
To deploy workspaces as Hetzner Cloud servers, you'll need:
18+
19+
- Hetzner Cloud [API token](https://console.hetzner.cloud/projects) (create under Security > API Tokens)
20+
21+
### Authentication
22+
23+
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Hetzner Cloud.
24+
25+
Obtain a Hetzner Cloud API token from your [Hetzner Cloud Console](https://console.hetzner.cloud/projects) and provide it as the `hcloud_token` variable when creating a workspace.
26+
For more authentication options, see the [Terraform provider documentation](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs#authentication).
27+
28+
> [!NOTE]
29+
> This template is designed to be a starting point. Edit the Terraform to extend the template to support your use case.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#cloud-config
2+
users:
3+
- name: ${username}
4+
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
5+
groups: sudo
6+
shell: /bin/bash
7+
packages:
8+
- git
9+
%{ if home_volume_label != "" ~}
10+
fs_setup:
11+
- device: /dev/disk/by-id/scsi-0HC_Volume_${volume_id}
12+
filesystem: ext4
13+
label: ${home_volume_label}
14+
overwrite: false # This prevents reformatting the disk on every boot
15+
16+
mounts:
17+
- [
18+
"/dev/disk/by-id/scsi-0HC_Volume_${volume_id}",
19+
"/home/${username}",
20+
auto,
21+
"defaults,uid=1000,gid=1000",
22+
]
23+
%{ endif ~}
24+
write_files:
25+
- path: /opt/coder/init
26+
permissions: "0755"
27+
encoding: b64
28+
content: ${init_script}
29+
- path: /etc/systemd/system/coder-agent.service
30+
permissions: "0644"
31+
content: |
32+
[Unit]
33+
Description=Coder Agent
34+
After=network-online.target
35+
Wants=network-online.target
36+
37+
[Service]
38+
User=${username}
39+
ExecStart=/opt/coder/init
40+
Environment=CODER_AGENT_TOKEN=${coder_agent_token}
41+
Restart=always
42+
RestartSec=10
43+
TimeoutStopSec=90
44+
KillMode=process
45+
46+
OOMScoreAdjust=-900
47+
SyslogIdentifier=coder-agent
48+
49+
[Install]
50+
WantedBy=multi-user.target
51+
runcmd:
52+
%{ if home_volume_label != "" ~}
53+
- |
54+
until [ -e /dev/disk/by-id/scsi-0HC_Volume_${volume_id} ]; do
55+
echo "Waiting for volume device..."
56+
sleep 2
57+
done
58+
%{ endif ~}
59+
- mount -a
60+
- chown ${username}:${username} /home/${username}
61+
- systemctl enable coder-agent
62+
- systemctl start coder-agent
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"type_meta": {
3+
"cx22": { "cores": 2, "memory_gb": 4, "disk_gb": 40 },
4+
"cx32": { "cores": 4, "memory_gb": 8, "disk_gb": 80 },
5+
"cx42": { "cores": 8, "memory_gb": 16, "disk_gb": 160 },
6+
"cx52": { "cores": 16, "memory_gb": 32, "disk_gb": 320 },
7+
"cpx11": { "cores": 2, "memory_gb": 2, "disk_gb": 40 },
8+
"cpx21": { "cores": 3, "memory_gb": 4, "disk_gb": 80 },
9+
"cpx31": { "cores": 4, "memory_gb": 8, "disk_gb": 160 },
10+
"cpx41": { "cores": 8, "memory_gb": 16, "disk_gb": 240 },
11+
"cpx51": { "cores": 16, "memory_gb": 32, "disk_gb": 360 },
12+
"ccx13": { "cores": 2, "memory_gb": 8, "disk_gb": 80 },
13+
"ccx23": { "cores": 4, "memory_gb": 16, "disk_gb": 160 },
14+
"ccx33": { "cores": 8, "memory_gb": 32, "disk_gb": 240 },
15+
"ccx43": { "cores": 16, "memory_gb": 64, "disk_gb": 360 },
16+
"ccx53": { "cores": 32, "memory_gb": 128, "disk_gb": 600 },
17+
"ccx63": { "cores": 48, "memory_gb": 192, "disk_gb": 960 }
18+
},
19+
"availability": {
20+
"fsn1": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
21+
"ash": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
22+
"hel1": ["cx22", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
23+
"hil": ["cpx11", "cpx21", "cpx31", "cpx41", "ccx13", "ccx23", "ccx33"],
24+
"nbg1": ["cx22", "cx32", "cx42", "cx52", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
25+
"sin": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"]
26+
}
27+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
terraform {
2+
required_providers {
3+
hcloud = {
4+
source = "hetznercloud/hcloud"
5+
}
6+
coder = {
7+
source = "coder/coder"
8+
}
9+
}
10+
}
11+
12+
variable "hcloud_token" {
13+
sensitive = true
14+
}
15+
16+
provider "hcloud" {
17+
token = var.hcloud_token
18+
}
19+
20+
# Available locations: https://docs.hetzner.com/cloud/general/locations/
21+
data "coder_parameter" "hcloud_location" {Expand commentComment on line R21ResolvedCode has comments. Press enter to view.
22+
name = "hcloud_location"
23+
display_name = "Hetzner Location"
24+
description = "Select the Hetzner Cloud location for your workspace."
25+
type = "string"
26+
default = "fsn1"
27+
option {
28+
name = "DE Falkenstein"
29+
value = "fsn1"
30+
}
31+
option {
32+
name = "US Ashburn, VA"
33+
value = "ash"
34+
}
35+
option {
36+
name = "US Hillsboro, OR"
37+
value = "hil"
38+
}
39+
option {
40+
name = "SG Singapore"
41+
value = "sin"
42+
}
43+
option {
44+
name = "DE Nuremberg"
45+
value = "nbg1"
46+
}
47+
option {
48+
name = "FI Helsinki"
49+
value = "hel1"
50+
}
51+
}
52+
53+
# Available server types: https://docs.hetzner.com/cloud/servers/overview/
54+
data "coder_parameter" "hcloud_server_type" {Expand commentComment on line R54ResolvedCode has comments. Press enter to view.
55+
name = "hcloud_server_type"
56+
display_name = "Hetzner Server Type"
57+
description = "Select the Hetzner Cloud server type for your workspace."
58+
type = "string"
59+
60+
dynamic "option" {
61+
for_each = local.hcloud_server_type_options_for_selected_location
62+
content {
63+
name = option.value.name
64+
value = option.value.value
65+
}
66+
}
67+
}
68+
69+
resource "hcloud_server" "dev" {
70+
count = data.coder_workspace.me.start_count
71+
name = "coder-${data.coder_workspace.me.name}-dev"
72+
image = "ubuntu-24.04"
73+
server_type = data.coder_parameter.hcloud_server_type.value
74+
location = data.coder_parameter.hcloud_location.value
75+
public_net {
76+
ipv4_enabled = true
77+
ipv6_enabled = true
78+
}
79+
user_data = templatefile("cloud-config.yaml.tftpl", {
80+
username = lower(data.coder_workspace_owner.me.name)
81+
home_volume_label = "coder-${data.coder_workspace.me.id}-home"
82+
volume_id = hcloud_volume.home_volume.id
83+
init_script = base64encode(coder_agent.main.init_script)
84+
coder_agent_token = coder_agent.main.token
85+
})
86+
labels = {
87+
"coder_workspace_name" = data.coder_workspace.me.name,
88+
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
89+
}
90+
}
91+
92+
resource "hcloud_volume" "home_volume" {
93+
name = "coder-${data.coder_workspace.me.id}-home"
94+
size = data.coder_parameter.home_volume_size.value
95+
location = data.coder_parameter.hcloud_location.value
96+
labels = {
97+
"coder_workspace_name" = data.coder_workspace.me.name,
98+
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
99+
}
100+
}
101+
102+
resource "hcloud_volume_attachment" "home_volume_attachment" {
103+
count = data.coder_workspace.me.start_count
104+
volume_id = hcloud_volume.home_volume.id
105+
server_id = hcloud_server.dev[count.index].id
106+
automount = false
107+
}
108+
109+
locals {
110+
username = lower(data.coder_workspace_owner.me.name)
111+
112+
# Data source: local JSON file under the module directory
113+
# Check API for latest server types & availability: https://docs.hetzner.cloud/reference/cloud#server-types
114+
hcloud_server_types_data = jsondecode(file("${path.module}/hetzner_server_types.json"))
115+
hcloud_server_type_meta = local.hcloud_server_types_data.type_meta
116+
hcloud_server_types_by_location = local.hcloud_server_types_data.availability
117+
118+
hcloud_server_type_options_for_selected_location = [
119+
for type_name in lookup(local.hcloud_server_types_by_location, data.coder_parameter.hcloud_location.value, []) : {
120+
name = format("%s (%d vCPU, %dGB RAM, %dGB)", upper(type_name), local.hcloud_server_type_meta[type_name].cores, local.hcloud_server_type_meta[type_name].memory_gb, local.hcloud_server_type_meta[type_name].disk_gb)
121+
value = type_name
122+
}
123+
]
124+
}
125+
126+
data "coder_provisioner" "me" {}
127+
128+
provider "coder" {}
129+
130+
data "coder_workspace" "me" {}
131+
132+
data "coder_workspace_owner" "me" {}
133+
134+
data "coder_parameter" "home_volume_size" {
135+
name = "home_volume_size"
136+
display_name = "Home volume size"
137+
description = "How large would you like your home volume to be (in GB)?"
138+
type = "number"
139+
default = "20"
140+
mutable = false
141+
validation {
142+
min = 1
143+
max = 100 # Adjust the max size as needed
144+
}
145+
}
146+
147+
resource "coder_agent" "main" {
148+
os = "linux"
149+
arch = "amd64"
150+
151+
metadata {
152+
key = "cpu"
153+
display_name = "CPU Usage"
154+
interval = 5
155+
timeout = 5
156+
script = "coder stat cpu"
157+
}
158+
metadata {
159+
key = "memory"
160+
display_name = "Memory Usage"
161+
interval = 5
162+
timeout = 5
163+
script = "coder stat mem"
164+
}
165+
metadata {
166+
key = "home"
167+
display_name = "Home Usage"
168+
interval = 600 # every 10 minutes
169+
timeout = 30 # df can take a while on large filesystems
170+
script = "coder stat disk --path /home/${local.username}"
171+
}
172+
}
173+
174+
module "code-server" {
175+
count = data.coder_workspace.me.start_count
176+
source = "registry.coder.com/coder/code-server/coder"
177+
178+
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
179+
version = "~> 1.0"
180+
181+
agent_id = coder_agent.main.id
182+
order = 1
183+
}

0 commit comments

Comments
 (0)