diff --git a/.github/typos.toml b/.github/typos.toml
index fdb747483..aefbbfd43 100644
--- a/.github/typos.toml
+++ b/.github/typos.toml
@@ -5,6 +5,13 @@ Hashi = "Hashi"
HashiCorp = "HashiCorp"
mavrickrishi = "mavrickrishi" # Username
mavrick = "mavrick" # Username
+melmathari = "melmathari" # Username
+fsn1 = "fsn1" # Hetzner Falkenstein datacenter code
+nbg1 = "nbg1" # Hetzner Nuremberg datacenter code
+hel1 = "hel1" # Hetzner Helsinki datacenter code
+hel = "hel" # Hetzner Helsinki short code
+hcloud = "hcloud" # Hetzner Cloud CLI/API
+vcpus = "vcpus" # Virtual CPUs
[files]
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
\ No newline at end of file
diff --git a/.icons/hetzner.svg b/.icons/hetzner.svg
new file mode 100644
index 000000000..74bb87c1a
--- /dev/null
+++ b/.icons/hetzner.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/registry/melmathari/.images/melmathari.jpeg b/registry/melmathari/.images/melmathari.jpeg
new file mode 100644
index 000000000..b50b558d4
Binary files /dev/null and b/registry/melmathari/.images/melmathari.jpeg differ
diff --git a/registry/melmathari/README.md b/registry/melmathari/README.md
new file mode 100644
index 000000000..92faa46de
--- /dev/null
+++ b/registry/melmathari/README.md
@@ -0,0 +1,16 @@
+---
+display_name: "Mohamed El Mathari"
+bio: "Software engineer, no-code nerd, teacher, and always ready to try something new"
+avatar: "./.images/melmathari.jpeg"
+github: "melmathari"
+linkedin: "https://www.linkedin.com/in/melmathari/"
+website: "https://melmathari.dev"
+support_email: "info@nocodeventure.com"
+status: community
+---
+
+# About Me
+
+👨‍💻 Learning by contributing in Open Source.
+
+Software engineer, no-code nerd, teacher, and always ready to try something new. Based in the Netherlands, I'm a no-code advocate who loves building tools and launching products that make a difference. Whether I'm coding, mentoring, or experimenting, I aim to keep things simple and impactful. I'm a big fan of breaking down complex problems into straightforward solutions—one idea at a time.
diff --git a/registry/melmathari/templates/hetzner-cloud/README.md b/registry/melmathari/templates/hetzner-cloud/README.md
new file mode 100644
index 000000000..b1e7e1fac
--- /dev/null
+++ b/registry/melmathari/templates/hetzner-cloud/README.md
@@ -0,0 +1,263 @@
+---
+display_name: Hetzner Cloud Server (Linux)
+description: Provision Hetzner Cloud servers as Coder workspaces with networking and volumes
+icon: ../../../../.icons/hetzner.svg
+verified: false
+tags: [vm, linux, hetzner, cloud, germany]
+---
+
+# Remote Development on Hetzner Cloud
+
+Provision Hetzner Cloud servers as [Coder workspaces](https://coder.com/docs/workspaces) with this template.
+
+This template provides a comprehensive Hetzner Cloud setup with:
+
+- **Dynamic Configuration**: Server types, locations, and images loaded from JSON
+- **Location-Aware Filtering**: Available server types automatically filter based on selected location
+- **Multiple Server Types**: ARM, Intel, AMD shared, and dedicated instances
+- **Global Locations**: Europe, USA, and Asia datacenters
+- **Persistent Storage**: Home volumes that survive workspace restarts
+- **Secure Networking**: Private networks with firewall rules
+- **Clean Architecture**: Region-based availability in JSON for easy maintenance
+
+## Prerequisites
+
+To deploy workspaces as Hetzner Cloud servers, you'll need:
+
+- Hetzner Cloud [API token](https://docs.hetzner.cloud/#authentication)
+- Hetzner Cloud project (create one in the [Hetzner Cloud Console](https://console.hetzner.cloud/))
+- **SSH Keys**: Upload your SSH public keys to your Hetzner Cloud account (the template will use all available keys)
+
+### Authentication
+
+This template assumes that the Coder Provisioner is run in an environment that is authenticated with Hetzner Cloud.
+
+Set the `HCLOUD_TOKEN` environment variable to your Hetzner Cloud API token, or provide it via the `hcloud_token` variable in your `terraform.tfvars` file.
+
+For other authentication methods, consult the [Hetzner Cloud Terraform provider documentation](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs).
+
+### Image Name Verification
+
+The template uses Hetzner's official image names. To verify current available images:
+
+```bash
+# Set your API token
+export HCLOUD_TOKEN="your-hetzner-cloud-api-token"
+
+# List all available images
+curl -s -H "Authorization: Bearer $HCLOUD_TOKEN" \
+ "https://api.hetzner.cloud/v1/images" \
+ | jq '.images[] | select(.type=="system") | .name'
+```
+
+If you encounter image-related errors, check that the image names in `hetzner-config.json` match the official names exactly (some may include architecture suffixes like `-amd64`).
+
+## Architecture
+
+This template provisions the following resources:
+
+- **Hetzner Cloud server** (ephemeral, deleted on workspace stop)
+- **Persistent volume** (mounted to `/home/`, survives workspace restarts)
+- **Private network** with subnet for secure communication
+- **Firewall** with rules for SSH, HTTP, HTTPS, and development ports
+- **SSH keys** automatically loaded from your Hetzner Cloud account
+
+### Lifecycle Management
+
+- **Workspace start**: Server and volume are created, volume is attached
+- **Workspace stop**: Server is destroyed, but volume persists
+- **Workspace restart**: New server is created and existing volume is reattached
+
+This means that when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace, modify the server image or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).
+
+## Server Types
+
+The template supports multiple Hetzner Cloud server types across four categories:
+
+- **ARM-based (CAX)**: Energy-efficient ARM architecture instances
+- **Intel CPU-Optimized (CPX)**: High-performance Intel processors
+- **AMD Shared (CX)**: Cost-effective AMD shared instances
+- **Dedicated vCPU (CCX)**: Dedicated CPU resources for consistent performance
+
+Server types are automatically filtered based on your selected location. The specific availability is managed in `hetzner-config.json`.
+
+## Locations
+
+Available locations:
+
+- **Falkenstein, Germany** (fsn1) - Europe
+- **Nuremberg, Germany** (nbg1) - Europe
+- **Helsinki, Finland** (hel1) - Europe
+- **Ashburn, Virginia, USA** (ash) - US East Coast
+- **Hillsboro, Oregon, USA** (hil) - US West Coast
+- **Singapore** (sin) - Asia Pacific
+
+## Supported Operating Systems
+
+- Ubuntu 24.04 LTS
+- Ubuntu 22.04 LTS (default)
+- Ubuntu 20.04 LTS
+- Debian 12
+- Debian 11
+- CentOS Stream 9
+- Fedora 39
+- Rocky Linux 9
+- AlmaLinux 9
+
+## Configuration
+
+### Required Variables
+
+```hcl
+# terraform.tfvars
+hcloud_token = "your-hetzner-cloud-api-token"
+```
+
+### Maintaining Configuration
+
+The template uses `hetzner-config.json` for dynamic configuration:
+
+- **Server Types**: Add new server types with their specifications
+- **Locations**: Add new Hetzner datacenters as they become available
+- **Images**: Update with current Hetzner image names (verify with API)
+- **Availability**: Map server type restrictions per location (only shown server types that are available)
+
+**How it works**: When a user selects a location, the template automatically filters the server type dropdown to only show instances available in that location. This prevents configuration errors by design.
+
+**Example**: Adding a new server type:
+
+```json
+"cx62": { "name": "CX62 (16 vCPU, 64 GB RAM, AMD)", "vcpus": 16, "memory": 64 }
+```
+
+The `availability_by_location` section maps which server types are available in each region:
+
+```json
+"availability_by_location": {
+ "fsn1": ["cax11", "cpx11", "cx22", "ccx13", ...], // Europe: All types
+ "ash": ["cpx11", "ccx13", ...], // USA: Intel + Dedicated only
+ "sin": ["cpx11", "ccx13", ...] // Asia: Intel + Dedicated only
+}
+```
+
+**Important**: Always verify image names match Hetzner's official names exactly to avoid provisioning errors.
+
+### Optional Variables
+
+All other parameters can be configured through the Coder workspace creation interface:
+
+- **Location**: Choose the datacenter location
+- **Server Type**: Select from available server configurations
+- **Operating System**: Choose your preferred Linux distribution from the curated list
+- **Custom Image Override**: Optionally specify a custom Hetzner Cloud image name (overrides the OS selection)
+- **Home Volume Size**: Set the size of persistent storage (10-1000 GB)
+
+### Custom Images
+
+You can use custom images in two ways:
+
+1. **Override Field**: Leave the "Custom Image Override" field empty to use the selected OS, or enter a custom image name to override it
+2. **Examples**:
+ - `my-custom-snapshot` - Your own Hetzner Cloud snapshot
+ - `debian-12-amd64` - Specific architecture variant
+ - `ubuntu-24.04` - Newer image not yet in the dropdown list
+
+The custom override takes precedence over the dropdown selection, allowing you to use any valid Hetzner Cloud image name.
+
+## Security
+
+The template includes:
+
+- Private networking for secure inter-service communication
+- Firewall rules allowing only necessary ports (22, 80, 443, 8080)
+- SSH key authentication
+- User isolation through cloud-init configuration
+
+## Cost Optimization
+
+- Servers are destroyed when workspaces stop, minimizing compute costs
+- Volumes persist but are only charged for storage when servers are stopped
+- Choose appropriate server types based on workload requirements
+- Consider using shared vCPU instances for development workloads
+
+## Troubleshooting
+
+### Server Type Options Change When Selecting Location
+
+This is expected behavior! The template dynamically filters server types based on regional availability:
+
+- **Europe (fsn1, nbg1, hel1)**: Shows all server types including ARM (CAX) and AMD (CX)
+- **USA/Asia (ash, hil, sin)**: Shows only Intel (CPX) and Dedicated (CCX) servers
+
+This prevents configuration errors by only showing what's actually available in your selected region.
+
+### Image Not Found Errors
+
+If you get errors like "image not found" or "invalid image name":
+
+1. **Verify Image Names**: Check current available images using the API:
+
+ ```bash
+ curl -s -H "Authorization: Bearer $HCLOUD_TOKEN" \
+ "https://api.hetzner.cloud/v1/images" \
+ | jq '.images[] | select(.type=="system") | .name' | sort
+ ```
+
+2. **Update JSON Configuration**: Edit `hetzner-config.json` to match exact image names from Hetzner
+3. **Common Issues**:
+ - Some images may have architecture suffixes (e.g., `debian-12` vs `debian-12-amd64`)
+ - Image names may change over time as new versions are released
+ - Deprecated images are removed from the available list
+
+4. **Test Locally**: Before using in Coder, test image names with basic Terraform:
+ ```hcl
+ resource "hcloud_server" "test" {
+ name = "test"
+ server_type = "cx11"
+ image = "ubuntu-22.04" # Test this image name
+ location = "fsn1"
+ }
+ ```
+
+### Volume Mount Issues
+
+If the home directory doesn't mount properly:
+
+1. Check that the volume is attached to the server
+2. Verify the cloud-init configuration is applied correctly
+3. Ensure the filesystem is formatted as ext4
+
+### Network Connectivity Issues
+
+If you can't connect to development servers:
+
+1. Verify firewall rules allow the required ports
+2. Check that the private network is configured correctly
+3. Ensure the server has a public IP address
+
+## Local Testing
+
+To test this template locally, create a `terraform.tfvars` file with:
+
+```hcl
+hcloud_token = "your-hetzner-cloud-api-token"
+```
+
+Then run:
+
+```bash
+terraform init
+terraform validate
+terraform plan
+```
+
+## Notes
+
+> [!NOTE]
+> This template is designed to be a starting point! Edit the Terraform configuration to extend the template to support your specific use case.
+
+> [!IMPORTANT]
+> The SSH key parameter defaults to 0 (no SSH key). To enable SSH access, set `ssh_key_id` to your actual SSH key ID from Hetzner Cloud.
+
+> [!WARNING]
+> Server types are automatically filtered based on location availability. If you don't see a specific server type in the dropdown, it's not available in your selected location.
diff --git a/registry/melmathari/templates/hetzner-cloud/cloud-config.yaml.tftpl b/registry/melmathari/templates/hetzner-cloud/cloud-config.yaml.tftpl
new file mode 100644
index 000000000..eb14bb537
--- /dev/null
+++ b/registry/melmathari/templates/hetzner-cloud/cloud-config.yaml.tftpl
@@ -0,0 +1,57 @@
+#cloud-config
+hostname: ${hostname}
+users:
+ - name: ${username}
+ sudo: ["ALL=(ALL) NOPASSWD:ALL"]
+ groups: sudo
+ shell: /bin/bash
+packages:
+ - git
+ - curl
+ - wget
+ - unzip
+ - htop
+disk_setup:
+ ${volume_device}:
+ table_type: "gpt"
+ layout: true
+ overwrite: false
+fs_setup:
+ - label: coder-home
+ filesystem: ext4
+ device: ${volume_device}
+ partition: auto
+mounts:
+ - ["${volume_device}", "/home/${username}", "ext4", "defaults", "0", "2"]
+write_files:
+ - path: /opt/coder/init
+ permissions: "0755"
+ encoding: b64
+ content: ${init_script}
+ - path: /etc/systemd/system/coder-agent.service
+ permissions: "0644"
+ content: |
+ [Unit]
+ Description=Coder Agent
+ After=network-online.target
+ Wants=network-online.target
+
+ [Service]
+ User=${username}
+ ExecStart=/opt/coder/init
+ Environment=CODER_AGENT_TOKEN=${coder_agent_token}
+ Restart=always
+ RestartSec=10
+ TimeoutStopSec=90
+ KillMode=process
+
+ OOMScoreAdjust=-1000
+ SyslogIdentifier=coder-agent
+
+ [Install]
+ WantedBy=multi-user.target
+runcmd:
+ - mkdir -p /home/${username}
+ - chown ${username}:${username} /home/${username}
+ - systemctl enable coder-agent
+ - systemctl start coder-agent
diff --git a/registry/melmathari/templates/hetzner-cloud/hetzner-config.json b/registry/melmathari/templates/hetzner-cloud/hetzner-config.json
new file mode 100644
index 000000000..5bbd38624
--- /dev/null
+++ b/registry/melmathari/templates/hetzner-cloud/hetzner-config.json
@@ -0,0 +1,226 @@
+{
+ "_comment": "Image names should match Hetzner's official Terraform provider names exactly. Verify with: curl -H 'Authorization: Bearer $HCLOUD_TOKEN' 'https://api.hetzner.cloud/v1/images' | jq '.images[] | .name'",
+ "type_meta": {
+ "locations": {
+ "fsn1": { "name": "Falkenstein, Germany", "zone": "eu-central" },
+ "nbg1": { "name": "Nuremberg, Germany", "zone": "eu-central" },
+ "hel1": { "name": "Helsinki, Finland", "zone": "eu-central" },
+ "ash": { "name": "Ashburn, VA, USA", "zone": "us-east" },
+ "hil": { "name": "Hillsboro, OR, USA", "zone": "us-west" },
+ "sin": { "name": "Singapore", "zone": "ap-southeast" }
+ },
+ "server_types": {
+ "cax11": {
+ "name": "CAX11 (2 vCPU, 4 GB RAM, ARM)",
+ "vcpus": 2,
+ "memory": 4
+ },
+ "cax21": {
+ "name": "CAX21 (4 vCPU, 8 GB RAM, ARM)",
+ "vcpus": 4,
+ "memory": 8
+ },
+ "cax31": {
+ "name": "CAX31 (8 vCPU, 16 GB RAM, ARM)",
+ "vcpus": 8,
+ "memory": 16
+ },
+ "cax41": {
+ "name": "CAX41 (16 vCPU, 32 GB RAM, ARM)",
+ "vcpus": 16,
+ "memory": 32
+ },
+ "cpx11": { "name": "CPX11 (2 vCPU, 2 GB RAM)", "vcpus": 2, "memory": 2 },
+ "cpx21": { "name": "CPX21 (3 vCPU, 4 GB RAM)", "vcpus": 3, "memory": 4 },
+ "cpx31": { "name": "CPX31 (4 vCPU, 8 GB RAM)", "vcpus": 4, "memory": 8 },
+ "cpx41": {
+ "name": "CPX41 (8 vCPU, 16 GB RAM)",
+ "vcpus": 8,
+ "memory": 16
+ },
+ "cpx51": {
+ "name": "CPX51 (16 vCPU, 32 GB RAM)",
+ "vcpus": 16,
+ "memory": 32
+ },
+ "cx22": {
+ "name": "CX22 (2 vCPU, 4 GB RAM, AMD)",
+ "vcpus": 2,
+ "memory": 4
+ },
+ "cx32": {
+ "name": "CX32 (4 vCPU, 8 GB RAM, AMD)",
+ "vcpus": 4,
+ "memory": 8
+ },
+ "cx42": {
+ "name": "CX42 (8 vCPU, 16 GB RAM, AMD)",
+ "vcpus": 8,
+ "memory": 16
+ },
+ "cx52": {
+ "name": "CX52 (16 vCPU, 32 GB RAM, AMD)",
+ "vcpus": 16,
+ "memory": 32
+ },
+ "ccx13": {
+ "name": "CCX13 (2 vCPU, 8 GB RAM, Dedicated)",
+ "vcpus": 2,
+ "memory": 8
+ },
+ "ccx23": {
+ "name": "CCX23 (4 vCPU, 16 GB RAM, Dedicated)",
+ "vcpus": 4,
+ "memory": 16
+ },
+ "ccx33": {
+ "name": "CCX33 (8 vCPU, 32 GB RAM, Dedicated)",
+ "vcpus": 8,
+ "memory": 32
+ },
+ "ccx43": {
+ "name": "CCX43 (16 vCPU, 64 GB RAM, Dedicated)",
+ "vcpus": 16,
+ "memory": 64
+ },
+ "ccx53": {
+ "name": "CCX53 (32 vCPU, 128 GB RAM, Dedicated)",
+ "vcpus": 32,
+ "memory": 128
+ },
+ "ccx63": {
+ "name": "CCX63 (48 vCPU, 192 GB RAM, Dedicated)",
+ "vcpus": 48,
+ "memory": 192
+ }
+ },
+ "images": {
+ "ubuntu-24.04": {
+ "name": "Ubuntu 24.04 LTS",
+ "icon": "/icon/ubuntu.svg"
+ },
+ "ubuntu-22.04": {
+ "name": "Ubuntu 22.04 LTS",
+ "icon": "/icon/ubuntu.svg"
+ },
+ "ubuntu-20.04": {
+ "name": "Ubuntu 20.04 LTS",
+ "icon": "/icon/ubuntu.svg"
+ },
+ "debian-12": { "name": "Debian 12", "icon": "/icon/debian.svg" },
+ "debian-11": { "name": "Debian 11", "icon": "/icon/debian.svg" },
+ "centos-stream-9": {
+ "name": "CentOS Stream 9",
+ "icon": "/icon/centos.svg"
+ },
+ "fedora-39": { "name": "Fedora 39", "icon": "/icon/fedora.svg" },
+ "rocky-9": { "name": "Rocky Linux 9", "icon": "/icon/rockylinux.svg" },
+ "alma-9": { "name": "AlmaLinux 9", "icon": "/icon/almalinux.svg" }
+ }
+ },
+ "availability_by_location": {
+ "_comment": "Europe has ARM (CAX) + AMD (CX). USA/Asia only have Intel (CPX) + Dedicated (CCX).",
+ "fsn1": [
+ "cax11",
+ "cax21",
+ "cax31",
+ "cax41",
+ "cpx11",
+ "cpx21",
+ "cpx31",
+ "cpx41",
+ "cpx51",
+ "cx22",
+ "cx32",
+ "cx42",
+ "cx52",
+ "ccx13",
+ "ccx23",
+ "ccx33",
+ "ccx43",
+ "ccx53",
+ "ccx63"
+ ],
+ "nbg1": [
+ "cax11",
+ "cax21",
+ "cax31",
+ "cax41",
+ "cpx11",
+ "cpx21",
+ "cpx31",
+ "cpx41",
+ "cpx51",
+ "cx22",
+ "cx32",
+ "cx42",
+ "cx52",
+ "ccx13",
+ "ccx23",
+ "ccx33",
+ "ccx43",
+ "ccx53",
+ "ccx63"
+ ],
+ "hel1": [
+ "cax11",
+ "cax21",
+ "cax31",
+ "cax41",
+ "cpx11",
+ "cpx21",
+ "cpx31",
+ "cpx41",
+ "cpx51",
+ "cx22",
+ "cx32",
+ "cx42",
+ "cx52",
+ "ccx13",
+ "ccx23",
+ "ccx33",
+ "ccx43",
+ "ccx53",
+ "ccx63"
+ ],
+ "ash": [
+ "cpx11",
+ "cpx21",
+ "cpx31",
+ "cpx41",
+ "cpx51",
+ "ccx13",
+ "ccx23",
+ "ccx33",
+ "ccx43",
+ "ccx53",
+ "ccx63"
+ ],
+ "hil": [
+ "cpx11",
+ "cpx21",
+ "cpx31",
+ "cpx41",
+ "cpx51",
+ "ccx13",
+ "ccx23",
+ "ccx33",
+ "ccx43",
+ "ccx53",
+ "ccx63"
+ ],
+ "sin": [
+ "cpx11",
+ "cpx21",
+ "cpx31",
+ "cpx41",
+ "cpx51",
+ "ccx13",
+ "ccx23",
+ "ccx33",
+ "ccx43",
+ "ccx53",
+ "ccx63"
+ ]
+ }
+}
diff --git a/registry/melmathari/templates/hetzner-cloud/main.tf b/registry/melmathari/templates/hetzner-cloud/main.tf
new file mode 100644
index 000000000..a9e5d0390
--- /dev/null
+++ b/registry/melmathari/templates/hetzner-cloud/main.tf
@@ -0,0 +1,383 @@
+terraform {
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ }
+ hcloud = {
+ source = "hetznercloud/hcloud"
+ }
+ }
+}
+
+provider "coder" {}
+
+# Variable for Hetzner Cloud API token
+variable "hcloud_token" {
+ description = "Hetzner Cloud API token for authentication"
+ type = string
+ sensitive = true
+}
+
+# Configure the Hetzner Cloud Provider
+provider "hcloud" {
+ token = var.hcloud_token
+}
+
+data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
+
+# Load Hetzner Cloud configuration from JSON
+locals {
+ hetzner_config = jsondecode(file("${path.module}/hetzner-config.json"))
+
+ # Generate server type options filtered by selected location
+ server_type_options_for_selected_location = [
+ for type_key in lookup(local.hetzner_config.availability_by_location, data.coder_parameter.location.value, []) : {
+ name = local.hetzner_config.type_meta.server_types[type_key].name
+ value = type_key
+ }
+ ]
+}
+
+# Hetzner Cloud locations parameter (dynamically generated from JSON)
+data "coder_parameter" "location" {
+ name = "location"
+ display_name = "Location"
+ description = "This is the location where your workspace will be created."
+ icon = "/emojis/1f30e.png"
+ type = "string"
+ default = "fsn1"
+ mutable = false
+
+ dynamic "option" {
+ for_each = local.hetzner_config.type_meta.locations
+ content {
+ name = option.value.name
+ value = option.key
+ icon = "/emojis/1f30e.png"
+ }
+ }
+}
+
+
+# Hetzner Cloud server types parameter (dynamically filtered based on selected location)
+data "coder_parameter" "server_type" {
+ name = "server_type"
+ display_name = "Server Type"
+ description = "Which Hetzner Cloud server type would you like to use?"
+ default = "cx22"
+ type = "string"
+ icon = "/icon/memory.svg"
+ mutable = false
+
+ # Filter server types based on the selected location
+ dynamic "option" {
+ for_each = local.server_type_options_for_selected_location
+ content {
+ name = option.value.name
+ value = option.value.value
+ }
+ }
+}
+
+# Server image parameter (dynamically generated from JSON)
+data "coder_parameter" "server_image" {
+ name = "server_image"
+ display_name = "Server Image"
+ description = "Which operating system image would you like to use?"
+ default = "ubuntu-22.04"
+ type = "string"
+ mutable = false
+
+ dynamic "option" {
+ for_each = local.hetzner_config.type_meta.images
+ content {
+ name = option.value.name
+ value = option.key
+ icon = option.value.icon
+ }
+ }
+
+}
+
+# Optional custom image override
+data "coder_parameter" "custom_image_override" {
+ name = "custom_image_override"
+ display_name = "Custom Image Override (optional)"
+ description = "Leave empty to use the selected image above, or enter a custom Hetzner Cloud image name to override (e.g., 'my-custom-snapshot', 'debian-12-amd64')"
+ type = "string"
+ default = ""
+ mutable = false
+}
+
+# Determine which image to use - custom override takes precedence
+locals {
+ final_image = data.coder_parameter.custom_image_override.value != "" ? data.coder_parameter.custom_image_override.value : data.coder_parameter.server_image.value
+}
+
+# Home volume size parameter
+data "coder_parameter" "volume_size" {
+ name = "volume_size"
+ display_name = "Home Volume Size (GB)"
+ description = "How large would you like your home volume to be (in GB)?"
+ type = "number"
+ default = 20
+ mutable = true
+
+ validation {
+ min = 10
+ max = 1000
+ monotonic = "increasing"
+ }
+}
+
+locals {
+ # Ensure unique names by including workspace ID
+ server_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-${substr(data.coder_workspace.me.id, 0, 8)}"
+ volume_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-${substr(data.coder_workspace.me.id, 0, 8)}-home"
+ network_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-${substr(data.coder_workspace.me.id, 0, 8)}-net"
+ firewall_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-${substr(data.coder_workspace.me.id, 0, 8)}-fw"
+
+ # Get selected server type and location configuration
+ selected_server_type = local.hetzner_config.type_meta.server_types[data.coder_parameter.server_type.value]
+ selected_location = local.hetzner_config.type_meta.locations[data.coder_parameter.location.value]
+ network_zone = local.selected_location.zone
+}
+
+resource "coder_agent" "main" {
+ os = "linux"
+ arch = "amd64"
+
+ metadata {
+ key = "cpu"
+ display_name = "CPU Usage"
+ interval = 5
+ timeout = 5
+ script = "coder stat cpu"
+ }
+ metadata {
+ key = "memory"
+ display_name = "Memory Usage"
+ interval = 5
+ timeout = 5
+ script = "coder stat mem"
+ }
+ metadata {
+ key = "home"
+ display_name = "Home Usage"
+ interval = 600 # every 10 minutes
+ timeout = 30 # df can take a while on large filesystems
+ script = "coder stat disk --path /home/${lower(data.coder_workspace_owner.me.name)}"
+ }
+}
+
+# See https://registry.coder.com/modules/coder/code-server
+module "code-server" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/code-server/coder"
+
+ # 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.
+ version = "~> 1.0"
+
+ agent_id = coder_agent.main.id
+ order = 1
+}
+
+# See https://registry.coder.com/modules/coder/jetbrains
+module "jetbrains" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/jetbrains/coder"
+ version = "~> 1.0"
+ agent_id = coder_agent.main.id
+ folder = "/home/coder"
+}
+
+variable "ssh_key_id" {
+ type = number
+ description = <<-EOF
+ Hetzner Cloud SSH key ID (obtain via the Hetzner Cloud Console or CLI):
+
+ Can be set to "0" for no SSH key.
+
+ $ hcloud ssh-key list
+ EOF
+ sensitive = true
+
+ validation {
+ condition = var.ssh_key_id >= 0
+ error_message = "Invalid Hetzner Cloud SSH key ID, a number is required."
+ }
+}
+
+# Create private network
+resource "hcloud_network" "workspace" {
+ name = local.network_name
+ ip_range = "10.0.0.0/16"
+
+ labels = {
+ "coder.workspace" = data.coder_workspace.me.name
+ "coder.owner" = data.coder_workspace_owner.me.name
+ "coder.resource" = "network"
+ }
+}
+
+# Create network subnet
+resource "hcloud_network_subnet" "workspace" {
+ network_id = hcloud_network.workspace.id
+ type = "cloud"
+ network_zone = local.network_zone
+ ip_range = "10.0.1.0/24"
+}
+
+# Create firewall
+resource "hcloud_firewall" "workspace" {
+ name = local.firewall_name
+
+ labels = {
+ "coder.workspace" = data.coder_workspace.me.name
+ "coder.owner" = data.coder_workspace_owner.me.name
+ "coder.resource" = "firewall"
+ }
+
+ rule {
+ direction = "in"
+ port = "22"
+ protocol = "tcp"
+ source_ips = ["0.0.0.0/0", "::/0"]
+ }
+
+ rule {
+ direction = "in"
+ port = "80"
+ protocol = "tcp"
+ source_ips = ["0.0.0.0/0", "::/0"]
+ }
+
+ rule {
+ direction = "in"
+ port = "443"
+ protocol = "tcp"
+ source_ips = ["0.0.0.0/0", "::/0"]
+ }
+
+ rule {
+ direction = "in"
+ port = "8080"
+ protocol = "tcp"
+ source_ips = ["0.0.0.0/0", "::/0"]
+ }
+}
+
+# Create volume for home directory
+resource "hcloud_volume" "home_volume" {
+ name = local.volume_name
+ size = data.coder_parameter.volume_size.value
+ location = data.coder_parameter.location.value
+ format = "ext4"
+
+ labels = {
+ "coder.workspace" = data.coder_workspace.me.name
+ "coder.owner" = data.coder_workspace_owner.me.name
+ "coder.resource" = "home-volume"
+ }
+
+ # Protect the volume from being deleted due to changes in attributes
+ lifecycle {
+ ignore_changes = all
+ }
+}
+
+# Create the server
+resource "hcloud_server" "workspace" {
+ count = data.coder_workspace.me.start_count
+ name = local.server_name
+ server_type = data.coder_parameter.server_type.value
+ image = local.final_image
+ location = data.coder_parameter.location.value
+ ssh_keys = var.ssh_key_id > 0 ? [var.ssh_key_id] : []
+ firewall_ids = [hcloud_firewall.workspace.id]
+
+ labels = {
+ "coder.workspace" = data.coder_workspace.me.name
+ "coder.owner" = data.coder_workspace_owner.me.name
+ "coder.resource" = "workspace-server"
+ }
+
+ public_net {
+ ipv4_enabled = true
+ ipv6_enabled = true
+ }
+
+ network {
+ network_id = hcloud_network.workspace.id
+ ip = "10.0.1.5"
+ }
+
+ user_data = templatefile("${path.module}/cloud-config.yaml.tftpl", {
+ hostname = local.server_name
+ username = lower(data.coder_workspace_owner.me.name)
+ volume_device = "/dev/sdb"
+ init_script = base64encode(coder_agent.main.init_script)
+ coder_agent_token = coder_agent.main.token
+ })
+
+ depends_on = [
+ hcloud_network_subnet.workspace
+ ]
+
+ # Proper lifecycle: server is destroyed when workspace stops, but volume persists
+ lifecycle {
+ ignore_changes = [ssh_keys, user_data]
+ }
+}
+
+# Attach volume to server
+resource "hcloud_volume_attachment" "home_volume" {
+ count = data.coder_workspace.me.start_count
+ volume_id = hcloud_volume.home_volume.id
+ server_id = hcloud_server.workspace[0].id
+ automount = true
+}
+
+resource "coder_metadata" "workspace_info" {
+ count = data.coder_workspace.me.start_count
+ resource_id = hcloud_server.workspace[0].id
+
+ item {
+ key = "location"
+ value = "${local.selected_location.name} (${hcloud_server.workspace[0].location})"
+ }
+ item {
+ key = "server_type"
+ value = "${local.selected_server_type.name} (${hcloud_server.workspace[0].server_type})"
+ }
+ item {
+ key = "vcpus"
+ value = local.selected_server_type.vcpus
+ }
+ item {
+ key = "memory"
+ value = "${local.selected_server_type.memory} GB"
+ }
+ item {
+ key = "image"
+ value = data.coder_parameter.custom_image_override.value != "" ? data.coder_parameter.custom_image_override.value : local.hetzner_config.type_meta.images[data.coder_parameter.server_image.value].name
+ }
+ item {
+ key = "public_ipv4"
+ value = hcloud_server.workspace[0].ipv4_address
+ }
+}
+
+resource "coder_metadata" "volume_info" {
+ resource_id = hcloud_volume.home_volume.id
+
+ item {
+ key = "size"
+ value = "${hcloud_volume.home_volume.size} GB"
+ }
+ item {
+ key = "location"
+ value = hcloud_volume.home_volume.location
+ }
+}