Add Template to deploy forgejo.

This template allows deploying a forgejo en either Scaleway or Hetzner
(untested) without much knowledge about them.
It DOES require knowledge about Terragrunt and ansible. A wizard of
sorts is provided but it will not guarantee success without some
knowledge about the underlying technology.
This commit is contained in:
Horacio Duran 2026-01-09 16:07:44 +01:00
parent a9f546f92a
commit 822e42dbb8
48 changed files with 6846 additions and 2 deletions

View file

@ -0,0 +1,12 @@
# Hetzner Forgejo Server Configuration
# Copy this file to terraform.tfvars and update with your values:
# cp terraform.tfvars.example terraform.tfvars
# Server type (cpx21 = 4 vCPU, 8GB RAM | cpx31 = 8 vCPU, 16GB RAM)
server_type = "cpx21"
# SSH keys to add (leave empty to use all keys in account)
# ssh_keys = ["my-key-name"]
# Domain name for Forgejo
# domain_name = "git.example.com"

View file

@ -0,0 +1,304 @@
# Include root configuration
include "root" {
path = find_in_parent_folders("root.hcl")
}
# Terragrunt configuration
terraform {
source = "."
}
# Generate the main Terraform configuration
generate "main" {
path = "main.tf"
if_exists = "overwrite"
contents = <<EOF
# Hetzner Cloud Infrastructure for Forgejo
variable "project_name" {
description = "Project name"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
variable "location" {
description = "Hetzner location"
type = string
}
variable "common_labels" {
description = "Common labels for all resources"
type = map(string)
}
variable "server_type" {
description = "Server type for Forgejo"
type = string
default = "cpx21" # 4 vCPU, 8GB RAM, 80GB SSD
}
variable "ssh_keys" {
description = "List of SSH key IDs or names to add to server"
type = list(string)
default = []
}
variable "domain_name" {
description = "Domain name for Forgejo"
type = string
default = ""
}
# Data sources
data "hcloud_ssh_keys" "all" {
count = length(var.ssh_keys) > 0 ? 0 : 1
}
# Network for private communication
resource "hcloud_network" "forgejo" {
name = "$${var.project_name}-$${var.environment}-network"
ip_range = "10.0.0.0/16"
labels = var.common_labels
}
resource "hcloud_network_subnet" "forgejo" {
network_id = hcloud_network.forgejo.id
type = "cloud"
network_zone = "eu-central"
ip_range = "10.0.1.0/24"
}
# Firewall
resource "hcloud_firewall" "forgejo" {
name = "$${var.project_name}-$${var.environment}-firewall"
labels = var.common_labels
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = [
"0.0.0.0/0",
"::/0"
]
description = "SSH access"
}
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = [
"0.0.0.0/0",
"::/0"
]
description = "HTTP"
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = [
"0.0.0.0/0",
"::/0"
]
description = "HTTPS"
}
rule {
direction = "in"
protocol = "tcp"
port = "2222"
source_ips = [
"0.0.0.0/0",
"::/0"
]
description = "SSH alternative port"
}
rule {
direction = "in"
protocol = "icmp"
source_ips = [
"0.0.0.0/0",
"::/0"
]
description = "ICMP (ping)"
}
}
# Placement Group for better availability
resource "hcloud_placement_group" "forgejo" {
name = "$${var.project_name}-$${var.environment}"
type = "spread"
labels = var.common_labels
}
# Cloud-init configuration
data "cloudinit_config" "forgejo" {
gzip = false
base64_encode = false
part {
content_type = "text/cloud-config"
content = yamlencode({
package_update = true
package_upgrade = true
packages = [
"apt-transport-https",
"ca-certificates",
"curl",
"gnupg",
"lsb-release",
"python3",
"python3-pip",
"ufw"
]
write_files = [
{
path = "/etc/sysctl.d/99-forgejo.conf"
content = <<-SYSCTL
# Forgejo optimizations
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.ip_forward = 1
vm.swappiness = 10
fs.file-max = 65535
SYSCTL
}
]
runcmd = [
"sysctl -p /etc/sysctl.d/99-forgejo.conf",
"systemctl enable ssh",
"ufw --force enable",
"ufw allow 22/tcp",
"ufw allow 80/tcp",
"ufw allow 443/tcp",
"ufw allow 2222/tcp"
]
})
}
}
# Server
resource "hcloud_server" "forgejo" {
name = "$${var.project_name}-$${var.environment}"
server_type = var.server_type
image = "ubuntu-24.04"
location = var.location
ssh_keys = length(var.ssh_keys) > 0 ? var.ssh_keys : data.hcloud_ssh_keys.all[0].ssh_keys[*].id
placement_group_id = hcloud_placement_group.forgejo.id
user_data = data.cloudinit_config.forgejo.rendered
labels = var.common_labels
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
firewall_ids = [hcloud_firewall.forgejo.id]
# Attach to private network
network {
network_id = hcloud_network.forgejo.id
ip = "10.0.1.5"
}
depends_on = [hcloud_network_subnet.forgejo]
}
# Volume for data persistence
resource "hcloud_volume" "forgejo_data" {
name = "$${var.project_name}-$${var.environment}-data"
size = 50
location = var.location
format = "ext4"
labels = var.common_labels
}
resource "hcloud_volume_attachment" "forgejo_data" {
volume_id = hcloud_volume.forgejo_data.id
server_id = hcloud_server.forgejo.id
automount = false # We'll mount via Ansible for better control
}
# Outputs
output "server_id" {
description = "Server ID"
value = hcloud_server.forgejo.id
}
output "server_name" {
description = "Server name"
value = hcloud_server.forgejo.name
}
output "server_ipv4" {
description = "Server IPv4 address"
value = hcloud_server.forgejo.ipv4_address
}
output "server_ipv6" {
description = "Server IPv6 address"
value = hcloud_server.forgejo.ipv6_address
}
output "server_private_ip" {
description = "Server private IP"
value = hcloud_server.forgejo.network[0].ip
}
output "volume_id" {
description = "Data volume ID"
value = hcloud_volume.forgejo_data.id
}
output "volume_device" {
description = "Volume device path"
value = "/dev/disk/by-id/scsi-0HC_Volume_$${hcloud_volume.forgejo_data.id}"
}
output "network_id" {
description = "Network ID"
value = hcloud_network.forgejo.id
}
output "ssh_command" {
description = "SSH command to connect"
value = "ssh root@$${hcloud_server.forgejo.ipv4_address}"
}
output "dns_records" {
description = "DNS records to create"
value = var.domain_name != "" ? {
ipv4 = "$${var.domain_name} IN A $${hcloud_server.forgejo.ipv4_address}"
ipv6 = "$${var.domain_name} IN AAAA $${hcloud_server.forgejo.ipv6_address}"
} : {}
}
EOF
}
# Generate cloudinit provider
generate "cloudinit_provider" {
path = "cloudinit.tf"
if_exists = "overwrite"
contents = <<EOF
terraform {
required_providers {
cloudinit = {
source = "hashicorp/cloudinit"
version = "~> 2.3"
}
}
}
EOF
}

View file

@ -0,0 +1,63 @@
# Hetzner Root Configuration
# This file contains common configuration for all Hetzner resources
locals {
# Project configuration
project_name = "forgejo"
environment = "production"
# Hetzner configuration
location = "nbg1" # Nuremberg, Germany
# Labels for resource organization
common_labels = {
project = "forgejo"
environment = "production"
managed-by = "terragrunt"
}
}
# Generate provider configuration
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
terraform {
required_version = ">= 1.5.0"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
provider "hcloud" {
# Token should be set via environment variable:
# HCLOUD_TOKEN
}
EOF
}
# Remote state configuration
remote_state {
backend = "local"
config = {
path = "${get_parent_terragrunt_dir()}/terraform.tfstate"
}
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
}
# Input values available to all child configurations
inputs = {
project_name = local.project_name
environment = local.environment
location = local.location
common_labels = local.common_labels
}

View file

View file

@ -0,0 +1,12 @@
# Scaleway Forgejo Instance Configuration
# Copy this file to terraform.tfvars and update with your values:
# cp terraform.tfvars.example terraform.tfvars
# Instance type (DEV1-M = 4GB RAM, DEV1-L = 8GB RAM)
instance_type = "DEV1-M"
# Your SSH public key (contents of your ~/.ssh/id_ed25519.pub or similar)
# ssh_public_key = "ssh-ed25519 AAAA... your-email@example.com"
# Your domain name for Forgejo
# domain_name = "git.example.com"

View file

@ -0,0 +1,224 @@
# Include root configuration
include "root" {
path = find_in_parent_folders("root.hcl")
}
# Terragrunt configuration
terraform {
source = "."
}
# Generate the main Terraform configuration
generate "main" {
path = "main.tf"
if_exists = "overwrite"
contents = <<EOF
# Scaleway Compute Instance for Forgejo
variable "project_name" {
description = "Project name"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
variable "region" {
description = "Scaleway region"
type = string
}
variable "zone" {
description = "Scaleway zone"
type = string
}
variable "common_tags" {
description = "Common tags for all resources"
type = map(string)
}
variable "instance_type" {
description = "Instance type for Forgejo server"
type = string
default = "DEV1-M" # 4GB RAM, 2 vCPUs, 40GB SSD
}
variable "ssh_public_key" {
description = "SSH public key for server access"
type = string
default = "" # Will use SSH agent keys if not provided
}
variable "domain_name" {
description = "Domain name for Forgejo (e.g., git.example.com)"
type = string
default = ""
}
# Data sources
data "scaleway_instance_image" "ubuntu" {
architecture = "x86_64"
name = "Ubuntu 24.04 Noble Numbat"
latest = true
}
# Security Group
resource "scaleway_instance_security_group" "forgejo" {
name = "$${var.project_name}-$${var.environment}-sg"
inbound_default_policy = "drop"
outbound_default_policy = "accept"
inbound_rule {
action = "accept"
port = "22"
protocol = "TCP"
ip_range = "0.0.0.0/0"
}
inbound_rule {
action = "accept"
port = "80"
protocol = "TCP"
ip_range = "0.0.0.0/0"
}
inbound_rule {
action = "accept"
port = "443"
protocol = "TCP"
ip_range = "0.0.0.0/0"
}
# SSH alternative port (optional, configured via Ansible)
inbound_rule {
action = "accept"
port = "2222"
protocol = "TCP"
ip_range = "0.0.0.0/0"
}
tags = [for k, v in var.common_tags : "$${k}:$${v}"]
}
# SSH Key (using IAM SSH key for Scaleway provider v2.x)
resource "scaleway_iam_ssh_key" "forgejo" {
count = var.ssh_public_key != "" ? 1 : 0
name = "$${var.project_name}-$${var.environment}-key"
public_key = var.ssh_public_key
}
# Block Volume for data persistence (create before instance)
resource "scaleway_block_volume" "forgejo_data" {
name = "$${var.project_name}-$${var.environment}-data"
size_in_gb = 50
iops = 5000
tags = [for k, v in var.common_tags : "$${k}:$${v}"]
}
# Reserved IP (create before instance for stability)
resource "scaleway_instance_ip" "forgejo" {}
# Compute Instance
resource "scaleway_instance_server" "forgejo" {
name = "$${var.project_name}-$${var.environment}"
type = var.instance_type
image = data.scaleway_instance_image.ubuntu.id
security_group_id = scaleway_instance_security_group.forgejo.id
ip_id = scaleway_instance_ip.forgejo.id
tags = [for k, v in var.common_tags : "$${k}:$${v}"]
# Ensure SSH key is registered before instance is created
depends_on = [scaleway_iam_ssh_key.forgejo]
# Cloud-init for initial setup (user_data is a map in v2.x)
user_data = {
cloud-init = <<-CLOUDINIT
#cloud-config
package_update: true
package_upgrade: true
packages:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- python3
- python3-pip
write_files:
- path: /etc/sysctl.d/99-forgejo.conf
content: |
# Forgejo optimizations
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 2048
vm.swappiness = 10
runcmd:
- sysctl -p /etc/sysctl.d/99-forgejo.conf
- systemctl enable ssh
- ufw --force enable
- ufw allow 22/tcp
- ufw allow 80/tcp
- ufw allow 443/tcp
CLOUDINIT
}
}
# Attach block volume to instance
resource "scaleway_block_snapshot" "forgejo_data" {
count = 0 # Only create if you need snapshots
name = "$${var.project_name}-$${var.environment}-snapshot"
volume_id = scaleway_block_volume.forgejo_data.id
}
# Outputs
output "server_id" {
description = "Forgejo server ID"
value = scaleway_instance_server.forgejo.id
}
output "server_ip" {
description = "Forgejo server public IP"
value = scaleway_instance_ip.forgejo.address
}
output "server_private_ip" {
description = "Forgejo server private IP"
value = length(scaleway_instance_server.forgejo.private_ips) > 0 ? scaleway_instance_server.forgejo.private_ips[0].address : null
}
output "security_group_id" {
description = "Security group ID"
value = scaleway_instance_security_group.forgejo.id
}
output "volume_id" {
description = "Data volume ID"
value = scaleway_block_volume.forgejo_data.id
}
output "ssh_command" {
description = "SSH command to connect to server"
value = "ssh root@$${scaleway_instance_ip.forgejo.address}"
}
output "dns_record" {
description = "DNS A record to create"
value = var.domain_name != "" ? "$${var.domain_name} IN A $${scaleway_instance_ip.forgejo.address}" : "No domain configured"
}
EOF
}
# Dependencies
dependency "storage" {
config_path = "../storage"
skip_outputs = true
mock_outputs = {
bucket_name = "forgejo-storage"
}
}

View file

@ -0,0 +1,70 @@
# Scaleway Root Configuration
# This file contains common configuration for all Scaleway resources
locals {
# Project configuration
project_name = "forgejo"
environment = "production"
# Scaleway configuration
region = "fr-par"
zone = "fr-par-1"
# Tags for resource organization
common_tags = {
Project = "forgejo"
Environment = "production"
ManagedBy = "terragrunt"
}
}
# Generate provider configuration
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
terraform {
required_version = ">= 1.5.0"
required_providers {
scaleway = {
source = "scaleway/scaleway"
version = "~> 2.31"
}
}
}
provider "scaleway" {
region = "${local.region}"
zone = "${local.zone}"
# Credentials should be set via environment variables:
# SCW_ACCESS_KEY
# SCW_SECRET_KEY
# SCW_DEFAULT_PROJECT_ID
}
EOF
}
# Remote state configuration (adjust for your backend)
remote_state {
backend = "local"
config = {
path = "${get_parent_terragrunt_dir()}/terraform.tfstate"
}
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
}
# Input values available to all child configurations
inputs = {
project_name = local.project_name
environment = local.environment
region = local.region
zone = local.zone
common_tags = local.common_tags
}

View file

@ -0,0 +1,154 @@
# Include root configuration
include "root" {
path = find_in_parent_folders("root.hcl")
}
# Terragrunt configuration
terraform {
source = "."
}
# Generate the storage Terraform configuration
generate "main" {
path = "main.tf"
if_exists = "overwrite"
contents = <<EOF
# Scaleway Object Storage for Forgejo
variable "project_name" {
description = "Project name"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
variable "region" {
description = "Scaleway region"
type = string
}
variable "common_tags" {
description = "Common tags for all resources"
type = map(string)
}
# Object Storage Bucket for Git LFS and Artifacts
resource "scaleway_object_bucket" "forgejo_lfs" {
name = "$${var.project_name}-$${var.environment}-lfs"
region = var.region
tags = var.common_tags
# Enable versioning for data protection
versioning {
enabled = true
}
# Lifecycle rules to manage storage costs
lifecycle_rule {
id = "delete-old-versions"
enabled = true
expiration {
days = 90
}
noncurrent_version_expiration {
days = 30
}
}
}
# Object Storage Bucket for Backups
resource "scaleway_object_bucket" "forgejo_backups" {
name = "$${var.project_name}-$${var.environment}-backups"
region = var.region
tags = var.common_tags
versioning {
enabled = true
}
# Keep backups for 30 days
lifecycle_rule {
id = "expire-old-backups"
enabled = true
expiration {
days = 30
}
}
}
# Access Key for application usage
resource "scaleway_iam_application" "forgejo" {
name = "$${var.project_name}-$${var.environment}"
description = "Application credentials for Forgejo object storage"
tags = [for k, v in var.common_tags : "$${k}=$${v}"]
}
resource "scaleway_iam_api_key" "forgejo" {
application_id = scaleway_iam_application.forgejo.id
description = "API key for Forgejo object storage access"
}
# Policy for bucket access
resource "scaleway_iam_policy" "forgejo_storage" {
name = "$${var.project_name}-$${var.environment}-storage-policy"
description = "Policy for Forgejo storage buckets"
application_id = scaleway_iam_application.forgejo.id
rule {
project_ids = [data.scaleway_account_project.main.id]
permission_set_names = ["ObjectStorageFullAccess"]
}
}
data "scaleway_account_project" "main" {
name = var.project_name
}
# Outputs
output "lfs_bucket_name" {
description = "LFS bucket name"
value = scaleway_object_bucket.forgejo_lfs.name
}
output "lfs_bucket_endpoint" {
description = "LFS bucket endpoint"
value = scaleway_object_bucket.forgejo_lfs.endpoint
}
output "backup_bucket_name" {
description = "Backup bucket name"
value = scaleway_object_bucket.forgejo_backups.name
}
output "backup_bucket_endpoint" {
description = "Backup bucket endpoint"
value = scaleway_object_bucket.forgejo_backups.endpoint
}
output "access_key" {
description = "Access key for object storage"
value = scaleway_iam_api_key.forgejo.access_key
sensitive = true
}
output "secret_key" {
description = "Secret key for object storage"
value = scaleway_iam_api_key.forgejo.secret_key
sensitive = true
}
output "s3_region" {
description = "S3-compatible region"
value = var.region
}
EOF
}