diff --git a/configs/ci-runner/runner-flatcar.yaml b/configs/ci-runner/runner-flatcar.yaml
new file mode 100644
index 0000000..04b962a
--- /dev/null
+++ b/configs/ci-runner/runner-flatcar.yaml
@@ -0,0 +1,104 @@
+variant: flatcar
+version: 1.1.0
+
+systemd:
+  units:
+    - name: forgejo-runner-install.service
+      enabled: true
+      contents: |
+        [Unit]
+        Description=Run Forgejo runner install script
+        Wants = network-online.target
+        After = network.target network-online.target
+        ConditionPathExists=/opt/forgejo-runner-install.sh
+        ConditionPathExists=!/opt/bin/forgejo-runner
+        [Service]
+        Type=oneshot
+        TimeoutStartSec=180
+        RemainAfterExit=yes
+        KillMode=process
+        ExecStart=/usr/bin/sh -c "/opt/forgejo-runner-install.sh"
+        [Install]
+        WantedBy=multi-user.target
+
+    - name: forgejo-runner.service
+      enabled: false
+      contents: |
+        [Unit]
+        Description=Run Forgejo runner
+        Wants = network-online.target
+        After = network.target network-online.target
+        ConditionPathExists=/opt/bin/forgejo-runner
+        [Service]
+        Type=simple
+        TimeoutStartSec=180
+        KillMode=process
+        ExecStart=/opt/bin/forgejo-runner daemon --config /etc/act/config.yaml
+        [Install]
+        WantedBy=multi-user.target
+
+storage:
+  files:
+    - path: /etc/hostname
+      mode: 0644
+      contents:
+        inline: ${host}
+    - path: /opt/forgejo-runner-install.sh
+      mode: 0777
+      contents:
+        inline: |
+          #!/bin/bash
+          set -ex
+          wget -O /opt/bin/forgejo-runner https://data.forgejo.org/forgejo/runner/releases/download/v${runner_version}/forgejo-runner-${runner_version}-linux-${arch}
+          chmod +x /opt/bin/forgejo-runner
+          sudo -u runner /opt/bin/forgejo-runner register --config /etc/act/config.yaml --no-interactive --token ${runner_secret} --name ${host} --instance ${forgejo_instance_url} --labels docker:docker://code.icb4dc0.de/infrastructure/images/act_runtime:arm64,ubuntu-latest:docker://code.icb4dc0.de/infrastructure/images/act_runtime:arm64,ubuntu-22.04:docker://code.icb4dc0.de/infrastructure/images/act_runtime:arm64,ubuntu-20.04:docker://code.icb4dc0.de/infrastructure/images/act_runtime:20.04-arm64
+
+    - path: /etc/act/config.yaml
+      mode: 0644
+      contents:
+        inline: |
+          # You don't have to copy this file to your instance,
+          # just run `forgejo-runner generate-config > config.yaml` to generate a config file.
+          log:
+            # The level of logging, can be trace, debug, info, warn, error, fatal
+            level: info
+
+          runner:
+            file: /home/runner/.runner
+            capacity: 1
+            timeout: 30m
+            fetch_timeout: 5s
+            fetch_interval: 2s
+            labels:
+              - "docker:docker://code.icb4dc0.de/infrastructure/images/act_runtime:arm64"
+              - "ubuntu-latest:docker://code.icb4dc0.de/infrastructure/images/act_runtime:arm64"
+              - "ubuntu-22.04:docker://code.icb4dc0.de/infrastructure/images/act_runtime:arm64"
+              - "ubuntu-20.04:docker://code.icb4dc0.de/infrastructure/images/act_runtime:20.04-arm64"
+
+          cache:
+            enabled: true
+            dir: ""
+            host: ""
+            port: 0
+            external_server: ""
+
+          container:
+            network: ""
+            enable_ipv6: false
+            privileged: false
+            options:
+            workdir_parent:
+            valid_volumes: []
+            docker_host: "unix:///var/run/docker.sock"
+            force_pull: true
+
+          host:
+            workdir_parent:
+
+passwd:
+  users:
+    - name: runner
+      ssh_authorized_keys: ${ssh_keys}
+      home_dir: /home/runner
+      groups:
+        - docker
diff --git a/dns.tf b/dns.tf
index 20df3d3..950f297 100644
--- a/dns.tf
+++ b/dns.tf
@@ -11,7 +11,7 @@ resource "cloudflare_record" "mx_primary" {
   zone_id  = cloudflare_zone.icb4dc0de.id
   name     = "@"
   type     = "MX"
-  content    = "mx01.mail.icloud.com"
+  content  = "mx01.mail.icloud.com"
   priority = 10
 }
 
@@ -20,7 +20,7 @@ resource "cloudflare_record" "mx_secondary" {
   zone_id  = cloudflare_zone.icb4dc0de.id
   name     = "@"
   type     = "MX"
-  content    = "mx02.mail.icloud.com"
+  content  = "mx02.mail.icloud.com"
   priority = 10
 }
 
@@ -28,26 +28,26 @@ resource "cloudflare_record" "apple_proof" {
   zone_id = cloudflare_zone.icb4dc0de.id
   name    = "@"
   type    = "TXT"
-  content   = "apple-domain=chwbVvzH8hWIgg1l"
+  content = "apple-domain=chwbVvzH8hWIgg1l"
 }
 
 resource "cloudflare_record" "keybase_proof" {
   zone_id = cloudflare_zone.icb4dc0de.id
   name    = "@"
   type    = "TXT"
-  content   = "keybase-site-verification=WDQoLtW22epD7eQnts6rPKJBGA0lD6jSI6m0bGMYWag"
+  content = "keybase-site-verification=WDQoLtW22epD7eQnts6rPKJBGA0lD6jSI6m0bGMYWag"
 }
 
 resource "cloudflare_record" "apple_spf" {
   zone_id = cloudflare_zone.icb4dc0de.id
   name    = "@"
   type    = "TXT"
-  content   = "\"v=spf1 include:icloud.com ~all\""
+  content = "\"v=spf1 include:icloud.com ~all\""
 }
 
 resource "cloudflare_record" "apple_sig_domainkey" {
   zone_id = cloudflare_zone.icb4dc0de.id
   name    = "sig1._domainkey"
   type    = "CNAME"
-  content   = "sig1.dkim.icb4dc0.de.at.icloudmailadmin.com"
+  content = "sig1.dkim.icb4dc0.de.at.icloudmailadmin.com"
 }
\ No newline at end of file
diff --git a/forgejo-runner_machines.tf b/forgejo-runner_machines.tf
new file mode 100644
index 0000000..27a68c4
--- /dev/null
+++ b/forgejo-runner_machines.tf
@@ -0,0 +1,158 @@
+resource "null_resource" "runner-config" {
+  triggers = {
+    version = var.forgejo_runner_version
+  }
+}
+
+resource "null_resource" "runner_generation" {
+  for_each = var.forgejo_runners
+  triggers = {
+    timestamp = "${each.value.generation}"
+  }
+}
+
+resource "hcloud_placement_group" "forgejo_runners" {
+  name = "forgejo-runners"
+  type = "spread"
+  labels = {
+    "cluster" = "forgejo.icb4dc0.de"
+  }
+}
+
+
+resource "hcloud_server" "forgejo_runner" {
+  for_each           = var.forgejo_runners
+  name               = each.key
+  server_type        = each.value.server_type
+  location           = each.value.location
+  image              = "ubuntu-22.04"
+  placement_group_id = hcloud_placement_group.k3s_machines.id
+
+  backups = false
+
+  lifecycle {
+    replace_triggered_by = [
+      null_resource.runner-config,
+      null_resource.runner_generation[each.key]
+    ]
+  }
+
+  ssh_keys = [
+    hcloud_ssh_key.provisioning_key.id,
+    hcloud_ssh_key.default.id
+  ]
+
+  labels = {
+    "node_type" = "forgejo_runner"
+    "cluster"   = "forgejo.icb4dc0.de"
+  }
+
+  network {
+    network_id = hcloud_network.k8s_net.id
+    ip         = each.value.private_ip
+  }
+
+  public_net {
+    ipv4_enabled = true
+    ipv6_enabled = true
+  }
+
+  # boot into rescue OS
+  rescue = "linux64"
+
+  connection {
+    host        = self.ipv4_address
+    agent       = false
+    private_key = tls_private_key.provisioning.private_key_pem
+    timeout     = "5m"
+  }
+
+  provisioner "file" {
+    content     = data.ct_config.forgejo-machine-ignitions[each.key].rendered
+    destination = "/root/ignition.json"
+  }
+
+  provisioner "remote-exec" {
+    inline = [
+      "set -ex",
+      "apt-get install -y gawk",
+      "curl -fsSLO --retry-delay 1 --retry 60 --retry-connrefused --retry-max-time 60 --connect-timeout 20 https://raw.githubusercontent.com/flatcar/init/flatcar-master/bin/flatcar-install",
+      "chmod +x flatcar-install",
+      "./flatcar-install -s -i /root/ignition.json -C ${var.flatcar_release_channel}",
+      "reboot",
+    ]
+    on_failure = continue
+  }
+
+  provisioner "remote-exec" {
+    connection {
+      host        = self.ipv4_address
+      private_key = tls_private_key.provisioning.private_key_pem
+      timeout     = "3m"
+      user        = "core"
+    }
+    inline = [
+      "sudo hostnamectl set-hostname ${self.name}",
+    ]
+  }
+
+  provisioner "file" {
+    connection {
+      host        = self.ipv4_address
+      private_key = tls_private_key.provisioning.private_key_pem
+      timeout     = "3m"
+      user        = "core"
+    }
+    destination = "/tmp/00-eth0.network"
+    content     = <<EOT
+      [Match]
+      Name=eth0
+      
+      [Network]
+      DHCP=ipv4
+      Address=${self.ipv6_address}
+      Gateway=fe80::1
+      DNS=2a01:4ff:ff00::add:2
+      DNS=2a01:4ff:ff00::add:1
+      EOT
+  }
+
+  provisioner "remote-exec" {
+    connection {
+      host        = self.ipv4_address
+      private_key = tls_private_key.provisioning.private_key_pem
+      timeout     = "3m"
+      user        = "core"
+    }
+    inline = [
+      "sudo mv /tmp/00-eth0.network /etc/systemd/network/00-eth0.network",
+      "sudo systemctl restart systemd-networkd",
+      "sudo systemctl enable --now forgejo-runner.service",
+    ]
+  }
+}
+
+data "ct_config" "forgejo-machine-ignitions" {
+  for_each = var.forgejo_runners
+  strict   = true
+  content = templatefile(
+    "${path.module}/configs/ci-runner/runner-flatcar.yaml",
+    {
+      "host"                 = each.key
+      "node_ip"              = each.value.private_ip
+      "runner_version"       = var.forgejo_runner_version
+      "forgejo_instance_url" = var.forgejo_instance_url
+      "runner_secret"        = var.forgejo_runner_secret
+      "arch"                 = startswith(each.value.server_type, "cax") ? "arm64" : "amd64"
+      "ssh_keys"             = jsonencode(concat(var.ssh_keys, [tls_private_key.provisioning.public_key_openssh]))
+    }
+  )
+  snippets = [
+    templatefile(
+      "${path.module}/configs/core-user.yaml.tmpl",
+      {
+        ssh_keys = jsonencode(concat(var.ssh_keys, [tls_private_key.provisioning.public_key_openssh]))
+      }
+    )
+  ]
+}
diff --git a/k8s_flatcar_machines.tf b/k8s_flatcar_machines.tf
index ee49ec9..eed625e 100644
--- a/k8s_flatcar_machines.tf
+++ b/k8s_flatcar_machines.tf
@@ -4,30 +4,6 @@ resource "null_resource" "worker-config" {
   }
 }
 
-resource "tls_private_key" "provisioning" {
-  algorithm = "RSA"
-  rsa_bits  = 4096
-}
-
-resource "hcloud_ssh_key" "provisioning_key" {
-  name       = "Provisioning key for hcloud cluster"
-  public_key = tls_private_key.provisioning.public_key_openssh
-}
-
-resource "local_file" "provisioning_key" {
-  filename             = "${path.module}/.ssh/provisioning_private_key.pem"
-  content              = tls_private_key.provisioning.private_key_pem
-  directory_permission = "0700"
-  file_permission      = "0400"
-}
-
-resource "local_file" "provisioning_key_pub" {
-  filename             = "${path.module}/.ssh/provisioning_key.pub"
-  content              = tls_private_key.provisioning.public_key_openssh
-  directory_permission = "0700"
-  file_permission      = "0440"
-}
-
 resource "null_resource" "machine_generation" {
   for_each = var.k3s_workers
   triggers = {
@@ -123,7 +99,6 @@ resource "hcloud_server" "machine" {
       "sudo hostnamectl set-hostname ${self.name}",
     ]
   }
-
 }
 
 data "ct_config" "machine-ignitions" {
@@ -132,12 +107,12 @@ data "ct_config" "machine-ignitions" {
   content = templatefile(
     "${path.module}/configs/workers/k3s-flatcar.yaml",
     {
-      "host"         = each.key
-      "k3s_token"    = var.k3s_token
-      "node_ip"      = each.value.private_ip
-      "k3s_version"  = var.worker_k3s_version
+      "host"              = each.key
+      "k3s_token"         = var.k3s_token
+      "node_ip"           = each.value.private_ip
+      "k3s_version"       = var.worker_k3s_version
       "spin_shim_version" = var.spin_shim_version
-      "arch" = startswith(each.value.server_type, "cax") ? "aarch64" : "x86_64"
+      "arch"              = startswith(each.value.server_type, "cax") ? "aarch64" : "x86_64"
     }
   )
   snippets = [
diff --git a/provisioning.tf b/provisioning.tf
new file mode 100644
index 0000000..9212b7b
--- /dev/null
+++ b/provisioning.tf
@@ -0,0 +1,23 @@
+resource "tls_private_key" "provisioning" {
+  algorithm = "RSA"
+  rsa_bits  = 4096
+}
+
+resource "hcloud_ssh_key" "provisioning_key" {
+  name       = "Provisioning key for hcloud cluster"
+  public_key = tls_private_key.provisioning.public_key_openssh
+}
+
+resource "local_file" "provisioning_key" {
+  filename             = "${path.module}/.ssh/provisioning_private_key.pem"
+  content              = tls_private_key.provisioning.private_key_pem
+  directory_permission = "0700"
+  file_permission      = "0400"
+}
+
+resource "local_file" "provisioning_key_pub" {
+  filename             = "${path.module}/.ssh/provisioning_key.pub"
+  content              = tls_private_key.provisioning.public_key_openssh
+  directory_permission = "0700"
+  file_permission      = "0440"
+}
diff --git a/tf.sh b/tf.sh
index a2c558b..66049a5 100755
--- a/tf.sh
+++ b/tf.sh
@@ -5,10 +5,11 @@ export AWS_SECRET_KEY=$(rbw get "CloudFlare TFState")
 export HETZNER_DNS_API_TOKEN=$(rbw get -f "API Token" "Hetzner DNS")
 export TF_VAR_hcloud_token="$(rbw get "HCloud API")"
 export TF_VAR_k3s_token="$(rbw get "K3s Token")"
+export TF_VAR_forgejo_runner_secret="$(rbw get "Forgejo Runner Secret")"
 export TF_VAR_k3s_backup_access_key="$(rbw get -f username "K3s Backup")"
 export TF_VAR_k3s_backup_secret_key="$(rbw get "K3s Backup")"
 export TF_VAR_k3s_backup_endpoint="$(rbw get -f Endpoint "K3s Backup")"
 export TF_VAR_cloudflare_api_token="$(rbw get -f "DNS API Token" "CloudFlare")"
 export TF_VAR_cloudflare_account_id="$(rbw get -f "Account ID" "CloudFlare")"
 
-tofu $@
\ No newline at end of file
+tofu $@
diff --git a/vars.tf b/vars.tf
index a021cd5..c4732b9 100644
--- a/vars.tf
+++ b/vars.tf
@@ -52,6 +52,21 @@ variable "worker_k3s_version" {
   default = "v1.31.5+k3s1"
 }
 
+variable "forgejo_runner_secret" {
+  type      = string
+  sensitive = true
+}
+
+variable "forgejo_runner_version" {
+  type    = string
+  default = "6.2.2"
+}
+
+variable "forgejo_instance_url" {
+  type    = string
+  default = "https://code.icb4dc0.de"
+}
+
 variable "k3s_sans" {
   type = list(string)
 }
@@ -68,10 +83,19 @@ variable "k3s_control_plane" {
 
 variable "k3s_workers" {
   type = map(object({
-    server_type  = string
-    generation   = number
-    private_ip   = string
-    location     = string
+    server_type = string
+    generation  = number
+    private_ip  = string
+    location    = string
+  }))
+}
+
+variable "forgejo_runners" {
+  type = map(object({
+    server_type = string
+    generation  = number
+    private_ip  = string
+    location    = string
   }))
 }
 
diff --git a/vms.auto.tfvars b/vms.auto.tfvars
index 0e573cf..fa19d9c 100644
--- a/vms.auto.tfvars
+++ b/vms.auto.tfvars
@@ -37,6 +37,22 @@ k3s_workers = {
   }
 }
 
+forgejo_runners = {
+  "ci-minion-bob" = {
+    server_type = "cax21"
+    generation  = 2
+    private_ip  = "172.23.2.30"
+    location    = "hel1"
+  }
+
+  "ci-minion-stuart" = {
+    server_type = "cax21"
+    generation  = 2
+    private_ip  = "172.23.2.31"
+    location    = "hel1"
+  }
+}
+
 ssh_keys = ["ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDQoNCLuHgcaDn4JTjCeQKJsIsYU0Jmub5PUMzIIZbUBb+TGMh6mCAY/UbYaq/n4jVnskXopzPGJbx4iPBG5HrNzqYZqMjkk8uIeeT0mdIcNv9bXxuCxCH1iHZF8LlzIZCmQ0w3X6VQ1izcJgvjrAYzbHN3gqCHOXtNkqIUkwaadIWCEjg33OVSlM4yrIDElr6+LHzv84VNh/PhemixCVVEMJ83GjhDtpApMg9WWW3es6rpJn4TlYEMV+aPNU4ZZEWFen/DFBKoX+ulkiJ8CwpY3eJxSzlBijs5ZKH89OOk/MXN1lnREElFqli+jE8EbZKQzi59Zmx8ZOb52qVNot8XZT0Un4EttAIEeE8cETqUC4jK+6RbUrsXtclVbU9i57LWRpl65LYSIJEFmkTxvYdkPXqGbvlW024IjgSo8kds121w95+Rpo6419cSYsQWowS8+aXfEv2Q8SE81QH7ObYfWFXsPBAmmNleQNN3E5HOoaxpWQjv3aTUGuxm4PCzKLdP0LsHmTfGJB7Priaj+9i8xLjDWe7zXDde2Gp9FmdedDr06uEkVSRFnS35Dwfd7M7xP6NsilfMOdWzJWWy/BAYxtnWcrEFxhaEr4vgs8Ub+KBtKhr740x3Mr8up+mythConAs4LOj37lWK4kJ8cI7TXjcSJi9nTIPd39us7tp3Aw== cardno:24_781_961"]
 
 flatcar_release_channel = "stable"