From 870bb5517462b359da4b007aac695db1703375c7 Mon Sep 17 00:00:00 2001 From: quinn Date: Mon, 29 Jun 2026 18:20:43 -0400 Subject: [PATCH] feat(tf-services): shared services droplet (3 Forgejo + Verdaccio) module DO droplet (nyc3 s-2vcpu-4gb + swap) running 3 co-located Forgejo (ct/mc/quinn) + Verdaccio via docker-compose. HTTP+token (built-in SSH disabled). Provisioned 165.227.191.38; state local (gitignored). Co-Authored-By: Claude Opus 4.8 --- .gitignore | 5 +++ cloud-init.yaml | 72 ++++++++++++++++++++++++++++++++++++++++ main.tf | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ variables.tf | 29 ++++++++++++++++ versions.tf | 13 ++++++++ 5 files changed, 207 insertions(+) create mode 100644 .gitignore create mode 100644 cloud-init.yaml create mode 100644 main.tf create mode 100644 variables.tf create mode 100644 versions.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5f8d1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.terraform/ +*.tfstate +*.tfstate.backup +.terraform.lock.hcl +terraform.tfvars diff --git a/cloud-init.yaml b/cloud-init.yaml new file mode 100644 index 0000000..a5fdc46 --- /dev/null +++ b/cloud-init.yaml @@ -0,0 +1,72 @@ +#cloud-config +package_update: true +packages: + - docker.io + - docker-compose-v2 + +write_files: + - path: /opt/services/docker-compose.yml + permissions: "0644" + content: | + services: + forgejo-ct: + image: codeberg.org/forgejo/forgejo:10 + restart: always + environment: + USER_UID: "1000" + USER_GID: "1000" + FORGEJO__server__HTTP_PORT: "3000" + FORGEJO__server__SSH_PORT: "2222" + FORGEJO__security__INSTALL_LOCK: "true" + FORGEJO__service__DISABLE_REGISTRATION: "true" + volumes: + - /opt/services/ct:/data + ports: + - "3000:3000" + - "2222:22" + forgejo-mc: + image: codeberg.org/forgejo/forgejo:10 + restart: always + environment: + USER_UID: "1000" + USER_GID: "1000" + FORGEJO__server__HTTP_PORT: "3000" + FORGEJO__server__SSH_PORT: "2223" + FORGEJO__security__INSTALL_LOCK: "true" + FORGEJO__service__DISABLE_REGISTRATION: "true" + volumes: + - /opt/services/mc:/data + ports: + - "3001:3000" + - "2223:22" + forgejo-quinn: + image: codeberg.org/forgejo/forgejo:10 + restart: always + environment: + USER_UID: "1000" + USER_GID: "1000" + FORGEJO__server__HTTP_PORT: "3000" + FORGEJO__server__SSH_PORT: "2224" + FORGEJO__security__INSTALL_LOCK: "true" + FORGEJO__service__DISABLE_REGISTRATION: "true" + volumes: + - /opt/services/quinn:/data + ports: + - "3002:3000" + - "2224:22" + verdaccio: + image: verdaccio/verdaccio:6 + restart: always + ports: + - "4873:4873" + volumes: + - /opt/services/verdaccio:/verdaccio/storage + +runcmd: + # 2GB swap (safety on the 4GB box) + - [ bash, -c, "fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile && echo '/swapfile none swap sw 0 0' >> /etc/fstab" ] + - [ bash, -c, "mkdir -p /opt/services/ct /opt/services/mc /opt/services/quinn /opt/services/verdaccio && chown -R 1000:1000 /opt/services" ] + - [ systemctl, enable, --now, docker ] + - [ bash, -c, "cd /opt/services && docker compose up -d" ] + +final_message: "services droplet up: 3 Forgejo (ct:3000 mc:3001 quinn:3002) + Verdaccio:4873" diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..9fb2da0 --- /dev/null +++ b/main.tf @@ -0,0 +1,88 @@ +############################################################################### +# Shared services droplet — 3 Forgejo instances (ct/mc/quinn) + Verdaccio, +# co-located. Public-facing (like the current ct-forge). 4GB + swap (tight but +# fine for low-traffic git + npm). PyPI / SwiftPM / DNS / Caddy are fast-follow. +############################################################################### + +resource "digitalocean_droplet" "services" { + name = var.name + image = "ubuntu-24-04-x64" + size = var.droplet_size + region = var.region + ssh_keys = var.ssh_key_fingerprints + tags = ["services", "forge", "quinn-infra"] + + user_data = file("${path.module}/cloud-init.yaml") + + lifecycle { + # Forgejo/Verdaccio data lives in /opt/services volumes; never let a + # user_data tweak silently rebuild and wipe it. + ignore_changes = [user_data] + } +} + +resource "digitalocean_firewall" "services" { + name = "services-fw" + droplet_ids = [digitalocean_droplet.services.id] + + inbound_rule { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + } + # Forgejo HTTP (ct 3000 / mc 3001 / quinn 3002) + git-SSH (2222/2223/2224) + Verdaccio 4873 + inbound_rule { + protocol = "tcp" + port_range = "3000-3002" + source_addresses = ["0.0.0.0/0", "::/0"] + } + inbound_rule { + protocol = "tcp" + port_range = "2222-2224" + source_addresses = ["0.0.0.0/0", "::/0"] + } + inbound_rule { + protocol = "tcp" + port_range = "4873" + source_addresses = ["0.0.0.0/0", "::/0"] + } + # 80/443 for future Caddy/TLS + inbound_rule { + protocol = "tcp" + port_range = "80" + source_addresses = ["0.0.0.0/0", "::/0"] + } + inbound_rule { + protocol = "tcp" + port_range = "443" + source_addresses = ["0.0.0.0/0", "::/0"] + } + + outbound_rule { + protocol = "tcp" + port_range = "1-65535" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + outbound_rule { + protocol = "udp" + port_range = "1-65535" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + outbound_rule { + protocol = "icmp" + destination_addresses = ["0.0.0.0/0", "::/0"] + } +} + +output "services_ip" { + value = digitalocean_droplet.services.ipv4_address +} + +output "forge_urls" { + value = { + ct = "http://${digitalocean_droplet.services.ipv4_address}:3000" + mc = "http://${digitalocean_droplet.services.ipv4_address}:3001" + quinn = "http://${digitalocean_droplet.services.ipv4_address}:3002" + npm = "http://${digitalocean_droplet.services.ipv4_address}:4873" + } +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..c786585 --- /dev/null +++ b/variables.tf @@ -0,0 +1,29 @@ +variable "do_token" { + type = string + sensitive = true + description = "DigitalOcean PAT (ct project)." +} + +variable "region" { + type = string + default = "nyc3" +} + +variable "droplet_size" { + type = string + default = "s-2vcpu-4gb" +} + +variable "ssh_key_fingerprints" { + type = list(string) + description = "DO SSH key fingerprints authorized on the box." + default = [ + "00:b5:2c:23:67:43:e5:39:c9:c2:43:31:6e:5c:03:10", # plum-natalie (operator laptop) + "b2:7e:66:b1:9b:61:ac:69:c5:96:a9:97:34:5c:9b:db", # cocotte-fleet + ] +} + +variable "name" { + type = string + default = "services" +} diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..158de68 --- /dev/null +++ b/versions.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.5" + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.40" + } + } +} + +provider "digitalocean" { + token = var.do_token +}