Forgejo Git and CI
Forgejo Git and CI
This pattern composes Compute, Network, and Block Storage into a self-hosted git forge with a built-in CI runner on infrastructure you control.
What this template does#
Provisions a single instance running Forgejo and one Forgejo Actions runner. Forgejo is an open-source git forge, and Forgejo Actions runs CI from the same workflow syntax as GitHub Actions, so existing .github/workflows files run with little or no change:
- Compute instance that runs the forge and the runner in Docker, sized so the runner has room to build containers
- Private network, subnet, router, port, and security group; a floating IP for public access
- A block volume mounted at
/var/lib/forgejo, so repositories, CI artifacts, and the runner's Docker layers live on a volume you can grow rather than on the boot disk - cloud-init installs Docker, brings up the forge and runner with Docker Compose, creates the admin account, and registers the runner on first boot
No credential ships with this template. The instance generates the admin password and the runner registration token on first boot. The admin password is written to /root/forgejo-credentials (readable only by root); retrieve it over SSH and rotate it after first login.
Forgejo and Gitea#
This template uses Forgejo, the community-run fork of Gitea, licensed GPL-3.0-or-later. The forge and the CI runner are open source. Gitea shares the same configuration shape and remains a valid choice: to run it instead, swap the forge image for docker.gitea.com/gitea and the runner image for the Gitea runner in docker-compose.yml. The variables, ports, and volume layout stay the same.
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist) | No default |
flavor_name | Instance size (headroom is for the runner, not the forge) | s1a.medium |
image_name | Operating system image | Ubuntu-24.04 |
app_name | Display name prefix for resources | forgejo |
admin_username | Admin account username created on first boot | forgejo-admin |
admin_email | Admin account email | [email protected] |
domain | Public domain for the forge; empty uses the floating IP over HTTP | "" |
volume_size | Block volume size in GiB, mounted at /var/lib/forgejo | 40 |
git_ssh_port | Host port forwarded to the forge's in-container SSH | 2222 |
external_network | External network for floating IP allocation | PublicStatic |
private_cidr | CIDR for the private subnet | 10.40.0.0/24 |
Ports and access#
| Port | Purpose |
|---|---|
| 22 | Host SSH for administration and retrieving the admin password |
| 80 | Forgejo web UI and git over HTTP |
| 443 | Forgejo web UI and git over HTTPS, once you put a domain and TLS in front |
git_ssh_port (default 2222) | git over SSH to the forge |
git over SSH uses a non-22 host port so it does not collide with the host's own SSH daemon. Clone URLs take the shape ssh://git@HOST:GIT_SSH_PORT/OWNER/REPO.git; the git_ssh_clone_example output prints the exact prefix.
For anything exposed to the internet, set domain, point its DNS A record at the floating IP, and terminate TLS with Forgejo's built-in ACME support or a reverse proxy on the host. The README in the template directory covers both paths.
How CI runs#
Forgejo Actions reads workflow files from each repository and dispatches jobs to the registered runner. The bundled runner advertises two labels:
docker: the job runs in a container image (the default isnode:20-bookworm), suited to most build and test jobshost: the job runs directly on the runner, for workflows that need the host toolchain
Reference a label in a workflow with runs-on: docker. Raise the runner capacity in config.yml for more concurrent jobs, or add runner instances for more throughput.
When to use this pattern#
Run your own git hosting and CI on a VM you operate, with workflow files that match the GitHub Actions syntax your team already knows. Forgejo maps a push to a pipeline, which makes it a natural companion to the Coolify host for push-to-deploy and the Next.js app template as a build target.
For a managed build-and-deploy dashboard rather than a git forge, use the Coolify host template. For a standalone datastore your CI jobs consume, see self-managed PostgreSQL or the Redis / Valkey cache.
Estimated cost#
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on shared vCPU.
Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.
What each resource is for
Forgejo
s1a.medium · 4 shared vCPU, 4 GiB RAM, 0.5 Gbps
Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.
Included in baseline
s1a.medium
4 shared vCPU, 4 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
4 vCPU + 4 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Block storage (70 GiB)
70 GiB at $0.08/GiB/mo
Public IP (included)
1 included with the custom package
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
Included at no charge
These line items are zero on Quake AI. Many other providers meter them separately.
Data transfer (inbound and outbound)
Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.
AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.
Learn morePrivate networking
Private networks, subnets, Neutron routers, and security groups are included with the plan.
VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.
Control-plane API requests
OpenStack API calls for provisioning and management are included.
Some managed services on other clouds meter API calls or charge for premium control-plane features.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Template source#
Show source (7 files)Hide source
data "openstack_images_image_v2" "os" {
name = var.image_name
most_recent = true
}
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "openstack_networking_network_v2" "private" {
name = "${var.app_name}-net"
admin_state_up = true
}
resource "openstack_networking_subnet_v2" "private" {
name = "${var.app_name}-subnet"
network_id = openstack_networking_network_v2.private.id
cidr = var.private_cidr
ip_version = 4
dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}
resource "openstack_networking_router_v2" "main" {
name = "${var.app_name}-router"
external_network_id = data.openstack_networking_network_v2.external.id
}
resource "openstack_networking_router_interface_v2" "private" {
router_id = openstack_networking_router_v2.main.id
subnet_id = openstack_networking_subnet_v2.private.id
}
resource "openstack_networking_secgroup_v2" "forgejo" {
name = "${var.app_name}-sg"
description = "Admin SSH, web (80/443), and git-over-SSH for the Forgejo forge"
}
# Host SSH for administration and retrieving the generated admin password.
resource "openstack_networking_secgroup_rule_v2" "ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.forgejo.id
}
# Web UI and git-over-HTTP(S). 80 carries the forge directly; 443 carries it once
# you put a domain and TLS in front (see the README).
resource "openstack_networking_secgroup_rule_v2" "http" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.forgejo.id
}
resource "openstack_networking_secgroup_rule_v2" "https" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.forgejo.id
}
# git-over-SSH. Forgejo's in-container SSH is forwarded from this host port so it
# does not collide with the host's own SSH daemon on 22.
resource "openstack_networking_secgroup_rule_v2" "git_ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = var.git_ssh_port
port_range_max = var.git_ssh_port
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.forgejo.id
}
resource "openstack_networking_port_v2" "forgejo" {
name = "${var.app_name}-port"
network_id = openstack_networking_network_v2.private.id
security_group_ids = [openstack_networking_secgroup_v2.forgejo.id]
fixed_ip {
subnet_id = openstack_networking_subnet_v2.private.id
}
depends_on = [openstack_networking_router_interface_v2.private]
}
resource "openstack_blockstorage_volume_v3" "data" {
name = "${var.app_name}-data"
size = var.volume_size
}
resource "openstack_networking_floatingip_v2" "forgejo" {
pool = var.external_network
}
resource "openstack_compute_instance_v2" "forgejo" {
name = var.app_name
flavor_name = var.flavor_name
key_pair = var.key_name
user_data = templatefile("${path.module}/cloud-init/forgejo.yaml.tftpl", {
admin_username = var.admin_username
admin_email = var.admin_email
domain = var.domain
git_ssh_port = var.git_ssh_port
})
block_device {
uuid = data.openstack_images_image_v2.os.id
source_type = "image"
destination_type = "volume"
volume_size = 30
boot_index = 0
delete_on_termination = true
}
network {
port = openstack_networking_port_v2.forgejo.id
}
}
resource "openstack_compute_volume_attach_v2" "data" {
instance_id = openstack_compute_instance_v2.forgejo.id
volume_id = openstack_blockstorage_volume_v3.data.id
}
resource "openstack_networking_floatingip_associate_v2" "forgejo" {
floating_ip = openstack_networking_floatingip_v2.forgejo.address
port_id = openstack_networking_port_v2.forgejo.id
}
variable "key_name" {
description = "SSH keypair name (must already exist in your project)"
type = string
}
variable "flavor_name" {
description = "Instance size. Forgejo itself runs in well under 1 GiB; the headroom is for the Actions runner, which builds containers and runs CI jobs. The default 4 vCPU / 4 GiB leaves room for the forge plus one runner executing a typical build. Size up for heavier CI."
type = string
default = "s1a.medium"
}
variable "image_name" {
description = "Operating system image. cloud-init targets a Debian-family distribution; Ubuntu 24.04 is the recommended base."
type = string
default = "Ubuntu-24.04"
}
variable "app_name" {
description = "Display name prefix for compute and network resources"
type = string
default = "forgejo"
}
variable "admin_username" {
description = "Username for the Forgejo admin account created on first boot. The password is generated on the instance, not shipped in this template; retrieve it over SSH from /root/forgejo-credentials."
type = string
default = "forgejo-admin"
}
variable "admin_email" {
description = "Email address for the Forgejo admin account created on first boot."
type = string
default = "[email protected]"
}
variable "domain" {
description = "Public domain for the forge, used to build the root URL and clone URLs (for example git.example.com). Leave empty to use the floating IP over HTTP. Point the domain's DNS A record at the floating IP and configure TLS as described in the README before relying on the domain."
type = string
default = ""
}
variable "volume_size" {
description = "Block volume size in GiB for repositories, CI artifacts, and the runner's Docker layers. The volume is mounted at /var/lib/forgejo so all growing state lives on a resizable volume rather than the boot disk."
type = number
default = 40
}
variable "git_ssh_port" {
description = "Host port that forwards to Forgejo's in-container SSH for git-over-SSH (git clone ssh://git@HOST:PORT/...). Kept off 22 so it does not collide with the host's own SSH daemon."
type = number
default = 2222
}
variable "external_network" {
description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
type = string
default = "PublicStatic"
}
variable "private_cidr" {
description = "CIDR for the private tenant network the instance lives in"
type = string
default = "10.40.0.0/24"
}
output "instance_id" {
description = "ID of the compute instance running Forgejo and the Actions runner"
value = openstack_compute_instance_v2.forgejo.id
}
output "floating_ip" {
description = "Public floating IP address of the forge"
value = openstack_networking_floatingip_v2.forgejo.address
}
output "private_ip" {
description = "Private IP address of the instance"
value = openstack_compute_instance_v2.forgejo.access_ip_v4
}
output "web_url" {
description = "Forgejo web UI URL. Uses the domain when set, otherwise the floating IP over HTTP. Configure TLS before relying on the domain (see the README)."
value = var.domain != "" ? "https://${var.domain}" : "http://${openstack_networking_floatingip_v2.forgejo.address}"
}
output "git_ssh_clone_example" {
description = "Shape of a git-over-SSH clone URL for this forge (substitute your owner/repo)."
value = "ssh://git@${var.domain != "" ? var.domain : openstack_networking_floatingip_v2.forgejo.address}:${var.git_ssh_port}/OWNER/REPO.git"
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# Required: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"
# Recommended: set a domain so clone URLs and the web UI use a stable name.
# Point its DNS A record at the floating IP and configure TLS (see the README).
# domain = "git.example.com"
# admin_username = "forgejo-admin"
# admin_email = "[email protected]"
# flavor_name = "s1a.medium"
# image_name = "Ubuntu-24.04"
# app_name = "forgejo"
# volume_size = 40
# git_ssh_port = 2222
# external_network = "PublicStatic"
# private_cidr = "10.40.0.0/24"
#cloud-config
package_update: true
packages:
- ca-certificates
- curl
- openssl
write_files:
# Compose stack: the Forgejo forge plus one Actions runner. The runner mounts
# the host Docker socket so `runs-on` jobs labelled docker:// execute in
# containers. Both services keep state on the attached volume mounted at
# /var/lib/forgejo.
- path: /opt/forgejo/docker-compose.yml
permissions: "0644"
content: |
services:
forgejo:
image: codeberg.org/forgejo/forgejo:11
container_name: forgejo
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__server__HTTP_PORT: "3000"
FORGEJO__server__DOMAIN: "${domain != "" ? domain : "127.0.0.1"}"
FORGEJO__server__ROOT_URL: "${domain != "" ? "https://${domain}/" : "http://127.0.0.1/"}"
FORGEJO__server__SSH_DOMAIN: "${domain != "" ? domain : "127.0.0.1"}"
FORGEJO__server__SSH_PORT: "${git_ssh_port}"
FORGEJO__server__SSH_LISTEN_PORT: "22"
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__actions__ENABLED: "true"
volumes:
- /var/lib/forgejo/data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "80:3000"
- "${git_ssh_port}:22"
networks:
- forge
runner:
image: code.forgejo.org/forgejo/runner:6
container_name: forgejo-runner
restart: unless-stopped
depends_on:
- forgejo
command: forgejo-runner daemon --config /data/config.yml
environment:
DOCKER_HOST: unix:///var/run/docker.sock
volumes:
- /var/lib/forgejo/runner:/data
- /var/run/docker.sock:/var/run/docker.sock
networks:
- forge
networks:
forge:
name: forge
# Runner daemon config, staged here because write_files runs before the data
# volume is mounted at /var/lib/forgejo. runcmd copies it onto the mounted
# volume. Jobs labelled `docker` run in the named container image; `host` jobs
# run directly on the runner. Labels are advertised to Forgejo at registration.
- path: /opt/forgejo/runner-config.yml
permissions: "0644"
content: |
log:
level: info
runner:
capacity: 2
timeout: 3h
labels:
- "docker:docker://node:20-bookworm"
- "host:host"
container:
network: forge
- path: /opt/forgejo/bootstrap.sh
permissions: "0755"
content: |
#!/usr/bin/env bash
# First-boot bootstrap. Generates the admin password and the runner
# registration token on the instance: neither is shipped in the template.
# The admin password is written to /root/forgejo-credentials (root-only);
# retrieve it over SSH.
set -euo pipefail
CRED_FILE=/root/forgejo-credentials
COMPOSE_DIR=/opt/forgejo
cd "$COMPOSE_DIR"
# Already bootstrapped? Do nothing (idempotent across reboots).
if [ -f "$CRED_FILE" ]; then
docker compose up -d
exit 0
fi
ADMIN_PASSWORD="$(openssl rand -base64 24)"
# When no custom domain is set, discover the public floating IP and patch compose env.
if [ -z "${domain}" ]; then
PUBLIC_IP="$(curl -fsS --connect-timeout 5 ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}')"
if [ -n "$PUBLIC_IP" ]; then
sed -i "s|FORGEJO__server__DOMAIN: \"127.0.0.1\"|FORGEJO__server__DOMAIN: \"$PUBLIC_IP\"|" docker-compose.yml
sed -i "s|FORGEJO__server__ROOT_URL: \"http://127.0.0.1/\"|FORGEJO__server__ROOT_URL: \"http://$PUBLIC_IP/\"|" docker-compose.yml
sed -i "s|FORGEJO__server__SSH_DOMAIN: \"127.0.0.1\"|FORGEJO__server__SSH_DOMAIN: \"$PUBLIC_IP\"|" docker-compose.yml
fi
fi
docker compose up -d forgejo
# Wait for the Forgejo HTTP endpoint to answer before using the CLI.
for i in $(seq 1 60); do
if curl -fsS http://127.0.0.1:80/api/healthz >/dev/null 2>&1; then break; fi
sleep 5
done
# Create the admin account. INSTALL_LOCK is set, so there is no web
# installer step; the account is created directly through the CLI.
docker compose exec -T -u git forgejo \
forgejo admin user create \
--admin \
--username "${admin_username}" \
--email "${admin_email}" \
--password "$ADMIN_PASSWORD" \
--must-change-password=false
# Generate an instance-level runner registration token and register the
# runner non-interactively, persisting .runner on the data volume.
RUNNER_TOKEN="$(docker compose exec -T -u git forgejo \
forgejo actions generate-runner-token | tr -d '\r\n')"
docker compose run --rm \
-v /var/lib/forgejo/runner:/data \
runner forgejo-runner register \
--no-interactive \
--instance http://forgejo:3000 \
--token "$RUNNER_TOKEN" \
--name "$(hostname)-runner" \
--labels "docker:docker://node:20-bookworm,host:host"
docker compose up -d
umask 077
cat > "$CRED_FILE" <<EOF
Forgejo admin account (generated on first boot)
username: ${admin_username}
password: $ADMIN_PASSWORD
email: ${admin_email}
Rotate this password after first login and delete this file.
EOF
chmod 600 "$CRED_FILE"
runcmd:
- |
set -e
# The data volume attaches as /dev/sdb on this platform (not /dev/vdb).
# Mount it at /var/lib/forgejo before anything writes there, so repositories,
# CI artifacts, and the runner's Docker layers live on the resizable volume.
DEV=/dev/sdb
for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L forgejodata "$DEV"; fi
mkdir -p /var/lib/forgejo
mount "$DEV" /var/lib/forgejo
grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/forgejo ext4 defaults,nofail 0 2" >> /etc/fstab
mkdir -p /var/lib/forgejo/data /var/lib/forgejo/runner
# The runner config was staged under /opt before the mount; copy it onto the
# now-mounted volume where the runner container reads it.
cp /opt/forgejo/runner-config.yml /var/lib/forgejo/runner/config.yml
chown -R 1000:1000 /var/lib/forgejo/data /var/lib/forgejo/runner
# Install Docker Engine (provides docker + compose plugin).
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
/opt/forgejo/bootstrap.sh
# Forgejo git + CI
Single compute instance running [Forgejo](https://forgejo.org) plus a Forgejo Actions runner on infrastructure you control. Forgejo is a self-hosted git forge, and Forgejo Actions is its CI engine, which reads the same workflow syntax as GitHub Actions. After apply, you push repositories to the forge and `.forgejo/workflows/*.yml` (or `.github/workflows/*.yml`) jobs run on the registered runner.
**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
The instance provisions a private network, a floating IP, and a block volume mounted at `/var/lib/forgejo` so repositories, CI artifacts, and the runner's Docker layers live on a resizable volume rather than the boot disk. cloud-init installs Docker, brings up the forge and runner with Docker Compose, creates the admin account, and registers the runner, all on first boot.
## Forgejo, not Gitea
This template uses Forgejo. Forgejo is the community-run hard fork of Gitea created in 2022 and is licensed GPL-3.0-or-later; the project and its CI runner are fully open source. Gitea remains a valid choice and shares the same configuration shape (the official image keeps the `gitea` data-directory name for compatibility). To run Gitea instead, swap the image in `docker-compose.yml` for `docker.gitea.com/gitea` and use the `gitea` runner image; the variables, ports, and volume layout are unchanged.
## Where this fits
A self-hosted git forge plus CI server is the largest CI/CD gap for builders who want the whole loop on infrastructure they own rather than on a third-party SaaS. Forgejo is the lightweight pick: the forge runs in well under 1 GiB, and the GitHub-Actions-compatible runner means existing workflow files run with little or no change. It pairs with the [Coolify host](/resources/iac-templates/coolify-host) (push-to-deploy) and the [Next.js app template](/resources/iac-templates/nextjs-app) (the build target).
## Prerequisites
- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- An existing SSH keypair in your project (the value of `key_name` must match that keypair)
## Resource baseline
Forgejo's own footprint is small (a single Go binary, well under 1 GiB of RAM at rest). The headroom in the default `s1a.medium` flavor (4 shared vCPU, 4 GiB RAM) is for the Actions runner, which builds containers and runs CI jobs. One runner executing a typical build fits comfortably; size up for heavier or more concurrent CI.
## Usage
1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and set `key_name` (and `domain` if you have one)
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`
cloud-init takes a few minutes on first boot to install Docker, pull the images, start the forge, create the admin account, and register the runner. Then open `web_url` from the outputs.
## First-boot accounts and secrets
No credential ships with this template. On first boot the instance:
- Generates the admin password with `openssl rand` and writes it to `/root/forgejo-credentials` (mode 600). Retrieve it over SSH: `ssh user@<floating_ip> sudo cat /root/forgejo-credentials`, then sign in as `admin_username` and rotate it.
- Generates an instance-level runner registration token through the Forgejo CLI and registers the bundled runner with it. The token never leaves the instance.
Open registration is disabled (`DISABLE_REGISTRATION=true`) so the forge starts as a single-admin instance. Create additional users from the admin panel.
## How the instance is provisioned
cloud-init:
1. Mounts the data volume at `/var/lib/forgejo` (formatting it on first boot) and adds an `/etc/fstab` entry so it persists across reboots.
2. Installs Docker Engine with the official convenience script and enables the service.
3. Runs `/opt/forgejo/bootstrap.sh`, which starts the forge with Docker Compose, waits for `/api/healthz`, creates the admin account, generates a runner registration token, registers the runner, and starts the full stack.
The bootstrap script is idempotent: on reboot it brings the stack back up without recreating the account.
## Ports and access
| Port | Purpose | Open to |
| --- | --- | --- |
| 22 | Host SSH for administration and retrieving the admin password | `0.0.0.0/0` |
| 80 | Forgejo web UI and git-over-HTTP | `0.0.0.0/0` |
| 443 | Forgejo web UI and git over HTTPS, once you put a domain and TLS in front | `0.0.0.0/0` |
| `git_ssh_port` (default 2222) | git-over-SSH to the forge | `0.0.0.0/0` |
git-over-SSH uses a non-22 host port so it does not collide with the host's own SSH daemon. Clone URLs take the shape `ssh://git@<host>:<git_ssh_port>/OWNER/REPO.git`; the `git_ssh_clone_example` output prints the exact prefix.
### TLS
Out of the box the forge answers on port 80 over plain HTTP, which is fine for a private network or a quick trial. For anything exposed to the internet, set `domain`, point its DNS A record at `floating_ip`, and terminate TLS one of two ways:
- Enable Forgejo's built-in ACME (Let's Encrypt) by adding the `[server]` ACME settings to `app.ini` and mapping port 443 to the container.
- Put a reverse proxy (Caddy or nginx) on the host in front of port 3000 and let it manage certificates.
## Variables
| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key_name` | string | yes | n/a | SSH keypair name (must already exist in your project) |
| `flavor_name` | string | no | `s1a.medium` | Instance size (headroom is for the runner, not the forge) |
| `image_name` | string | no | `Ubuntu-24.04` | Operating system image |
| `app_name` | string | no | `forgejo` | Display name prefix for resources |
| `admin_username` | string | no | `forgejo-admin` | Admin account username created on first boot |
| `admin_email` | string | no | `[email protected]` | Admin account email |
| `domain` | string | no | `""` | Public domain for the forge; empty uses the floating IP over HTTP |
| `volume_size` | number | no | `40` | Block volume size in GiB, mounted at `/var/lib/forgejo` |
| `git_ssh_port` | number | no | `2222` | Host port forwarded to the forge's in-container SSH |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `private_cidr` | string | no | `10.40.0.0/24` | CIDR for the private subnet |
## Outputs
| Name | Description |
| --- | --- |
| `floating_ip` | Public floating IP assigned to the instance |
| `private_ip` | Private IP address of the instance |
| `web_url` | Forgejo web UI URL (domain when set, otherwise the floating IP) |
| `git_ssh_clone_example` | Shape of a git-over-SSH clone URL for this forge |
| `instance_id` | Compute instance ID |
## Scope
This is a single-VM forge with one bundled runner that you operate, not a managed control plane. It is CPU-only and runs in one region. For more CI throughput, raise the runner `capacity` in `config.yml` or add more runner instances. For high availability, run Forgejo against external Postgres and object storage and place multiple instances behind a load balancer; this template provisions one node with file-backed storage.
## Documentation
See also: [Coolify host template](/resources/iac-templates/coolify-host), [Next.js app template](/resources/iac-templates/nextjs-app)
Resources, parameters, and variables
key_namerequiredflavor_name="s1a.medium"image_name="Ubuntu-24.04"app_name="forgejo"admin_username="forgejo-admin"admin_email="[email protected]"domain=""volume_size=40git_ssh_port=2222external_network="PublicStatic"private_cidr="10.40.0.0/24"
Customize this pattern#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
See also#
Usage Guidelines
The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.
For the full policy, see Usage Guidelines.
Last validated: 29.06.2026
See Also
Terraform and OpenTofu on Quake AI
Prerequisite
Networks
Prerequisite
Authoring IaC templates for Quake AI
Shares: Volumes, Security Groups
Deploy an API gateway with the api-gateway template
Shares: Volumes, Security Groups
Deploy a regional edge cache with the edge-cache template
Shares: Volumes, Security Groups