Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f66f61d724 | |||
| 631bf027c6 | |||
| eb4c28fc61 | |||
| c551c4d84a | |||
| 4b9da4036a | |||
| 96c5f3219d | |||
| 146540c1e9 | |||
| a85436fdf4 | |||
| aa4890fe62 |
@@ -3,6 +3,7 @@ muc = "muc" # For Munich location code
|
||||
tyo = "tyo" # For Tokyo location code
|
||||
Hashi = "Hashi"
|
||||
HashiCorp = "HashiCorp"
|
||||
hel = "hel" # For Helsinki location code
|
||||
mavrickrishi = "mavrickrishi" # Username
|
||||
mavrick = "mavrick" # Username
|
||||
inh = "inh" # Option in setpriv command
|
||||
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="128" cy="128" r="120" fill="black"/>
|
||||
<polygon
|
||||
points="128,70 178,170 78,170"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 216 B |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 451 KiB After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,7 @@
|
||||
---
|
||||
display_name: "Excellencedev"
|
||||
bio: "Love to contribute"
|
||||
avatar: "./.images/avatar.png"
|
||||
support_email: "ademiluyisuccessandexcellence@gmail.com"
|
||||
status: "community"
|
||||
---
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
display_name: Hetzner Cloud Server
|
||||
description: Provision Hetzner Cloud servers as Coder workspaces
|
||||
icon: ../../../../.icons/hetzner.svg
|
||||
tags: [vm, linux, hetzner]
|
||||
---
|
||||
|
||||
# Remote Development on Hetzner Cloud (Linux)
|
||||
|
||||
Provision Hetzner Cloud servers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
|
||||
|
||||
> [!WARNING]
|
||||
> **Workspace Storage Persistence:** When a workspace is stopped, the Hetzner Cloud server instance is stopped but your home volume and stored data persist. This means your files and data remain intact when you resume the workspace.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **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.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To deploy workspaces as Hetzner Cloud servers, you'll need:
|
||||
|
||||
- Hetzner Cloud [API token](https://console.hetzner.cloud/projects) (create under Security > API Tokens)
|
||||
|
||||
### Authentication
|
||||
|
||||
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Hetzner Cloud.
|
||||
|
||||
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.
|
||||
For more authentication options, see the [Terraform provider documentation](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs#authentication).
|
||||
|
||||
> [!NOTE]
|
||||
> This template is designed to be a starting point. Edit the Terraform to extend the template to support your use case.
|
||||
@@ -0,0 +1,62 @@
|
||||
#cloud-config
|
||||
users:
|
||||
- name: ${username}
|
||||
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
|
||||
groups: sudo
|
||||
shell: /bin/bash
|
||||
packages:
|
||||
- git
|
||||
%{ if home_volume_label != "" ~}
|
||||
fs_setup:
|
||||
- device: /dev/disk/by-id/scsi-0HC_Volume_${volume_id}
|
||||
filesystem: ext4
|
||||
label: ${home_volume_label}
|
||||
overwrite: false # This prevents reformatting the disk on every boot
|
||||
|
||||
mounts:
|
||||
- [
|
||||
"/dev/disk/by-id/scsi-0HC_Volume_${volume_id}",
|
||||
"/home/${username}",
|
||||
ext4,
|
||||
"defaults,uid=1000,gid=1000",
|
||||
]
|
||||
%{ endif ~}
|
||||
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=-900
|
||||
SyslogIdentifier=coder-agent
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
runcmd:
|
||||
%{ if home_volume_label != "" ~}
|
||||
- |
|
||||
until [ -e /dev/disk/by-id/scsi-0HC_Volume_${volume_id} ]; do
|
||||
echo "Waiting for volume device..."
|
||||
sleep 2
|
||||
done
|
||||
%{ endif ~}
|
||||
- mount -a
|
||||
- chown ${username}:${username} /home/${username}
|
||||
- systemctl enable coder-agent
|
||||
- systemctl start coder-agent
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type_meta": {
|
||||
"cx22": { "cores": 2, "memory_gb": 4, "disk_gb": 40 },
|
||||
"cx32": { "cores": 4, "memory_gb": 8, "disk_gb": 80 },
|
||||
"cx42": { "cores": 8, "memory_gb": 16, "disk_gb": 160 },
|
||||
"cx52": { "cores": 16, "memory_gb": 32, "disk_gb": 320 },
|
||||
"cpx11": { "cores": 2, "memory_gb": 2, "disk_gb": 40 },
|
||||
"cpx21": { "cores": 3, "memory_gb": 4, "disk_gb": 80 },
|
||||
"cpx31": { "cores": 4, "memory_gb": 8, "disk_gb": 160 },
|
||||
"cpx41": { "cores": 8, "memory_gb": 16, "disk_gb": 240 },
|
||||
"cpx51": { "cores": 16, "memory_gb": 32, "disk_gb": 360 },
|
||||
"ccx13": { "cores": 2, "memory_gb": 8, "disk_gb": 80 },
|
||||
"ccx23": { "cores": 4, "memory_gb": 16, "disk_gb": 160 },
|
||||
"ccx33": { "cores": 8, "memory_gb": 32, "disk_gb": 240 },
|
||||
"ccx43": { "cores": 16, "memory_gb": 64, "disk_gb": 360 },
|
||||
"ccx53": { "cores": 32, "memory_gb": 128, "disk_gb": 600 },
|
||||
"ccx63": { "cores": 48, "memory_gb": 192, "disk_gb": 960 }
|
||||
},
|
||||
"availability": {
|
||||
"fsn1": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"ash": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hel1": ["cx22", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hil": ["cpx11", "cpx21", "cpx31", "cpx41", "ccx13", "ccx23", "ccx33"],
|
||||
"nbg1": ["cx22", "cx32", "cx42", "cx52", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"sin": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
hcloud = {
|
||||
source = "hetznercloud/hcloud"
|
||||
}
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "hcloud_token" {
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
provider "hcloud" {
|
||||
token = var.hcloud_token
|
||||
}
|
||||
|
||||
# Available locations: https://docs.hetzner.com/cloud/general/locations/
|
||||
data "coder_parameter" "hcloud_location" {
|
||||
name = "hcloud_location"
|
||||
display_name = "Hetzner Location"
|
||||
description = "Select the Hetzner Cloud location for your workspace."
|
||||
type = "string"
|
||||
default = "fsn1"
|
||||
option {
|
||||
name = "DE Falkenstein"
|
||||
value = "fsn1"
|
||||
}
|
||||
option {
|
||||
name = "US Ashburn, VA"
|
||||
value = "ash"
|
||||
}
|
||||
option {
|
||||
name = "US Hillsboro, OR"
|
||||
value = "hil"
|
||||
}
|
||||
option {
|
||||
name = "SG Singapore"
|
||||
value = "sin"
|
||||
}
|
||||
option {
|
||||
name = "DE Nuremberg"
|
||||
value = "nbg1"
|
||||
}
|
||||
option {
|
||||
name = "FI Helsinki"
|
||||
value = "hel1"
|
||||
}
|
||||
}
|
||||
|
||||
# Available server types: https://docs.hetzner.com/cloud/servers/overview/
|
||||
data "coder_parameter" "hcloud_server_type" {
|
||||
name = "hcloud_server_type"
|
||||
display_name = "Hetzner Server Type"
|
||||
description = "Select the Hetzner Cloud server type for your workspace."
|
||||
type = "string"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = local.hcloud_server_type_options_for_selected_location
|
||||
content {
|
||||
name = option.value.name
|
||||
value = option.value.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_server" "dev" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
name = "coder-${data.coder_workspace.me.name}-dev"
|
||||
image = "ubuntu-24.04"
|
||||
server_type = data.coder_parameter.hcloud_server_type.value
|
||||
location = data.coder_parameter.hcloud_location.value
|
||||
public_net {
|
||||
ipv4_enabled = true
|
||||
ipv6_enabled = true
|
||||
}
|
||||
user_data = templatefile("cloud-config.yaml.tftpl", {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
home_volume_label = "coder-${data.coder_workspace.me.id}-home"
|
||||
volume_id = hcloud_volume.home_volume.id
|
||||
init_script = base64encode(coder_agent.main.init_script)
|
||||
coder_agent_token = coder_agent.main.token
|
||||
})
|
||||
labels = {
|
||||
"coder_workspace_name" = data.coder_workspace.me.name,
|
||||
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
size = data.coder_parameter.home_volume_size.value
|
||||
location = data.coder_parameter.hcloud_location.value
|
||||
labels = {
|
||||
"coder_workspace_name" = data.coder_workspace.me.name,
|
||||
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_volume_attachment" "home_volume_attachment" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
volume_id = hcloud_volume.home_volume.id
|
||||
server_id = hcloud_server.dev[count.index].id
|
||||
automount = false
|
||||
}
|
||||
|
||||
locals {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
|
||||
# Data source: local JSON file under the module directory
|
||||
# Check API for latest server types & availability: https://docs.hetzner.cloud/reference/cloud#server-types
|
||||
hcloud_server_types_data = jsondecode(file("${path.module}/hetzner_server_types.json"))
|
||||
hcloud_server_type_meta = local.hcloud_server_types_data.type_meta
|
||||
hcloud_server_types_by_location = local.hcloud_server_types_data.availability
|
||||
|
||||
hcloud_server_type_options_for_selected_location = [
|
||||
for type_name in lookup(local.hcloud_server_types_by_location, data.coder_parameter.hcloud_location.value, []) : {
|
||||
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)
|
||||
value = type_name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
|
||||
provider "coder" {}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
data "coder_parameter" "home_volume_size" {
|
||||
name = "home_volume_size"
|
||||
display_name = "Home volume size"
|
||||
description = "How large would you like your home volume to be (in GB)?"
|
||||
type = "number"
|
||||
default = "20"
|
||||
mutable = false
|
||||
validation {
|
||||
min = 1
|
||||
max = 100 # Adjust the max size as needed
|
||||
}
|
||||
}
|
||||
|
||||
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/${local.username}"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 407 KiB After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 590 KiB |
@@ -13,7 +13,7 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
}
|
||||
@@ -51,7 +51,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
@@ -71,7 +71,7 @@ Customize tool permissions, MCP servers, and Copilot settings:
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
@@ -142,7 +142,7 @@ variable "github_token" {
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
github_token = var.github_token
|
||||
@@ -156,7 +156,7 @@ Run Copilot as a command-line tool without task reporting or web interface. This
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
report_tasks = false
|
||||
|
||||
@@ -3,7 +3,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,7 +242,7 @@ resource "coder_env" "github_token" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
@@ -268,7 +268,7 @@ module "agentapi" {
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
|
||||
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
|
||||
@@ -288,7 +288,7 @@ module "agentapi" {
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
|
||||
|
||||
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
@@ -299,4 +299,8 @@ module "agentapi" {
|
||||
ARG_COPILOT_MODEL='${var.copilot_model}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
|
||||
```tf
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "coder-login" {
|
||||
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("cursor-cli", async () => {
|
||||
"-c",
|
||||
"cat /home/coder/.cursor-cli-module/agentapi-start.log || cat /home/coder/.cursor-cli-module/start.log || true",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`-m ${model}`);
|
||||
expect(startLog.stdout).toContain(`--model ${model}`);
|
||||
expect(startLog.stdout).toContain("-f");
|
||||
expect(startLog.stdout).toContain("test prompt");
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ resource "coder_env" "cursor_api_key" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
@@ -179,3 +179,7 @@ module "agentapi" {
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ ARGS=()
|
||||
|
||||
# global flags
|
||||
if [ -n "$ARG_MODEL" ]; then
|
||||
ARGS+=("-m" "$ARG_MODEL")
|
||||
ARGS+=("--model" "$ARG_MODEL")
|
||||
fi
|
||||
if [ "$ARG_FORCE" = "true" ]; then
|
||||
ARGS+=("-f")
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
|
||||
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
@@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
module "gemini" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
gemini_model = "gemini-2.5-flash"
|
||||
@@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
@@ -225,4 +225,8 @@ module "agentapi" {
|
||||
GEMINI_TASK_PROMPT='${var.task_prompt}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
display_name: Perplexica
|
||||
description: Run Perplexica AI search engine in your workspace via Docker
|
||||
icon: ../../../../.icons/perplexica.svg
|
||||
verified: false
|
||||
tags: [ai, search, docker]
|
||||
---
|
||||
|
||||
# Perplexica
|
||||
|
||||
Run [Perplexica](https://github.com/ItzCrazyKns/Perplexica), a privacy-focused AI search engine, in your Coder workspace. Supports cloud providers (OpenAI, Anthropic Claude) and local LLMs via Ollama.
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
This module uses the full Perplexica image with embedded SearXNG for simpler setup with no external dependencies.
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
This module requires Docker to be available on the host.
|
||||
|
||||
## Examples
|
||||
|
||||
### With API Keys
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
openai_api_key = var.openai_api_key
|
||||
anthropic_api_key = var.anthropic_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### With Local Ollama
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
ollama_api_url = "http://ollama-external-endpoint:11434"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "docker_socket" {
|
||||
type = string
|
||||
description = "(Optional) Docker socket URI"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "The port to run Perplexica on."
|
||||
default = 3000
|
||||
}
|
||||
|
||||
variable "data_path" {
|
||||
type = string
|
||||
description = "Host path to mount for Perplexica data persistence."
|
||||
default = "./perplexica-data"
|
||||
}
|
||||
|
||||
variable "uploads_path" {
|
||||
type = string
|
||||
description = "Host path to mount for Perplexica file uploads."
|
||||
default = "./perplexica-uploads"
|
||||
}
|
||||
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key for Claude models."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "ollama_api_url" {
|
||||
type = string
|
||||
description = "Ollama API URL for local LLM support."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "share" {
|
||||
type = string
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "perplexica" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Perplexica"
|
||||
icon = "/icon/perplexica.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
DOCKER_HOST : var.docker_socket,
|
||||
PORT : var.port,
|
||||
DATA_PATH : var.data_path,
|
||||
UPLOADS_PATH : var.uploads_path,
|
||||
OPENAI_API_KEY : var.openai_api_key,
|
||||
ANTHROPIC_API_KEY : var.anthropic_api_key,
|
||||
OLLAMA_API_URL : var.ollama_api_url,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "perplexica" {
|
||||
agent_id = var.agent_id
|
||||
slug = "perplexica"
|
||||
display_name = "Perplexica"
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = "/icon/perplexica.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
run "plan_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.perplexica.url == "http://localhost:3000"
|
||||
error_message = "Default port should be 3000"
|
||||
}
|
||||
}
|
||||
|
||||
run "plan_custom_port" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
port = 8080
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.perplexica.url == "http://localhost:8080"
|
||||
error_message = "Should use custom port"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "$${BOLD}Starting Perplexica...$${RESET}\n"
|
||||
|
||||
# Set Docker host if provided
|
||||
if [ -n "${DOCKER_HOST}" ]; then
|
||||
export DOCKER_HOST="${DOCKER_HOST}"
|
||||
fi
|
||||
|
||||
# Wait for docker to become ready
|
||||
max_attempts=10
|
||||
delay=2
|
||||
attempt=1
|
||||
|
||||
while ! docker ps; do
|
||||
if [ $attempt -ge $max_attempts ]; then
|
||||
echo "Failed to list containers after $${max_attempts} attempts."
|
||||
exit 1
|
||||
fi
|
||||
echo "Attempt $${attempt} failed, retrying in $${delay}s..."
|
||||
sleep $delay
|
||||
attempt=$(expr "$attempt" + 1)
|
||||
delay=$(expr "$delay" \* 2)
|
||||
done
|
||||
|
||||
# Pull the image
|
||||
IMAGE="itzcrazykns1337/perplexica:latest"
|
||||
docker pull "$${IMAGE}"
|
||||
|
||||
# Build docker run command
|
||||
DOCKER_ARGS="-d --rm --name perplexica -p ${PORT}:3000"
|
||||
|
||||
# Add mounts - convert relative paths to absolute
|
||||
DATA_PATH="${DATA_PATH}"
|
||||
UPLOADS_PATH="${UPLOADS_PATH}"
|
||||
|
||||
mkdir -p "$${DATA_PATH}"
|
||||
mkdir -p "$${UPLOADS_PATH}"
|
||||
|
||||
DATA_PATH_ABS=$(cd "$${DATA_PATH}" && pwd)
|
||||
UPLOADS_PATH_ABS=$(cd "$${UPLOADS_PATH}" && pwd)
|
||||
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -v $${DATA_PATH_ABS}:/home/perplexica/data"
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -v $${UPLOADS_PATH_ABS}:/home/perplexica/uploads"
|
||||
|
||||
# Add environment variables if provided
|
||||
if [ -n "${OPENAI_API_KEY}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e OPENAI_API_KEY=${OPENAI_API_KEY}"
|
||||
fi
|
||||
|
||||
if [ -n "${ANTHROPIC_API_KEY}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}"
|
||||
fi
|
||||
|
||||
if [ -n "${OLLAMA_API_URL}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e OLLAMA_API_URL=${OLLAMA_API_URL}"
|
||||
fi
|
||||
|
||||
# Run container
|
||||
docker run $${DOCKER_ARGS} "$${IMAGE}"
|
||||
|
||||
printf "\n$${BOLD}Perplexica is running on port ${PORT}$${RESET}\n"
|
||||
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
|
||||
```tf
|
||||
module "amp-cli" {
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "2.1.0"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key
|
||||
install_amp = true
|
||||
@@ -48,7 +48,7 @@ variable "amp_api_key" {
|
||||
module "amp-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
amp_version = "2.1.0"
|
||||
amp_version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key # recommended for tasks usage
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -110,6 +110,7 @@ describe("amp", async () => {
|
||||
const { id } = await setup({
|
||||
skipAmpMock: true,
|
||||
moduleVariables: {
|
||||
install_via_npm: "true",
|
||||
amp_version: "0.0.1755964909-g31e083",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
external = {
|
||||
source = "hashicorp/external"
|
||||
@@ -140,7 +140,7 @@ variable "base_amp_config" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Base AMP configuration in JSON format. Can be overridden to customize AMP settings.
|
||||
|
||||
|
||||
If empty, defaults enable thinking and todos for autonomous operation. Additional options include:
|
||||
- "amp.permissions": [] (tool permissions)
|
||||
- "amp.tools.stopTimeout": 600 (extend timeout for long operations)
|
||||
@@ -148,7 +148,7 @@ variable "base_amp_config" {
|
||||
- "amp.tools.disable": ["builtin:open"] (disable tools for containers)
|
||||
- "amp.git.commit.ampThread.enabled": true (link commits to threads)
|
||||
- "amp.git.commit.coauthor.enabled": true (add Amp as co-author)
|
||||
|
||||
|
||||
Reference: https://ampcode.com/manual
|
||||
EOT
|
||||
default = ""
|
||||
@@ -220,7 +220,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
@@ -268,4 +268,6 @@ module "agentapi" {
|
||||
EOT
|
||||
}
|
||||
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 976 KiB After Width: | Height: | Size: 528 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 191 KiB |
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
@@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
enable_boundary = true
|
||||
@@ -72,7 +72,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -92,10 +92,9 @@ module "claude-code" {
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-custom-tool": {
|
||||
"command": "my-tool-server"
|
||||
"command": "my-tool-server",
|
||||
"args": ["--port", "8080"]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
EOF
|
||||
@@ -109,7 +108,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
install_claude_code = true
|
||||
@@ -131,7 +130,7 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
@@ -204,7 +203,7 @@ resource "coder_env" "bedrock_api_key" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -261,7 +260,7 @@ resource "coder_env" "google_application_credentials" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
@@ -39,9 +39,11 @@ interface SetupProps {
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const setup = async (
|
||||
props?: SetupProps,
|
||||
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
const { id, coderEnvVars } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_claude_code: props?.skipClaudeMock ? "true" : "false",
|
||||
@@ -61,7 +63,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
content: await loadTestFile(import.meta.dir, "claude-mock.sh"),
|
||||
});
|
||||
}
|
||||
return { id };
|
||||
return { id, coderEnvVars };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
@@ -79,14 +81,14 @@ describe("claude-code", async () => {
|
||||
|
||||
test("install-claude-code-version", async () => {
|
||||
const version_to_install = "1.0.40";
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
claude_code_version: version_to_install,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
@@ -96,14 +98,14 @@ describe("claude-code", async () => {
|
||||
});
|
||||
|
||||
test("check-latest-claude-code-version-works", async () => {
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
@@ -133,13 +135,13 @@ describe("claude-code", async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
mcp: mcpConfig,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
|
||||
const resp = await readFileContainer(id, "/home/coder/.claude.json");
|
||||
expect(resp).toContain("test-cmd");
|
||||
|
||||
@@ -288,6 +288,12 @@ resource "coder_env" "disable_autoupdater" {
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_binary_path" {
|
||||
agent_id = var.agent_id
|
||||
name = "PATH"
|
||||
value = "$HOME/.local/bin:$PATH"
|
||||
}
|
||||
|
||||
locals {
|
||||
# we have to trim the slash because otherwise coder exp mcp will
|
||||
# set up an invalid claude config
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
@@ -45,11 +40,6 @@ function install_claude_code_cli() {
|
||||
if [ $CURL_EXIT -ne 0 ]; then
|
||||
echo "Claude Code installer failed with exit code $$CURL_EXIT"
|
||||
fi
|
||||
|
||||
# Ensure binaries are discoverable.
|
||||
echo "Creating a symlink for claude"
|
||||
sudo ln -s /home/coder/.local/bin/claude /usr/local/bin/claude
|
||||
|
||||
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
|
||||
else
|
||||
echo "Skipping Claude Code installation as per configuration."
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
|
||||
module "kasmvnc" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kasmvnc/coder"
|
||||
version = "1.2.6"
|
||||
version = "1.2.7"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
subdomain = true
|
||||
|
||||
@@ -31,7 +31,7 @@ variable "desktop_environment" {
|
||||
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
|
||||
|
||||
validation {
|
||||
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
|
||||
condition = contains(["cinnamon", "mate", "lxde", "lxqt", "kde", "gnome", "xfce", "manual"], var.desktop_environment)
|
||||
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 460 B |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 461 KiB After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 631 KiB |