diff --git a/.sops.yaml b/.sops.yaml
index 83a51dca8a5a93e1b98bb41dfe640ef049ea296b..b05213c370bbef80d711a58be13eb8721490cc5e 100644
--- a/.sops.yaml
+++ b/.sops.yaml
@@ -4,3 +4,6 @@ creation_rules:
     pgp: >-
       286791FB6648539775DB31B8FCB98C2A3EC6F601,
       B137EE1549DFAF960DD1E2B15147025FB9F09E07
+  - path_regex: terraform/.*\.tfvars
+    pgp: >-
+      286791FB6648539775DB31B8FCB98C2A3EC6F601
diff --git a/renovate.json b/renovate.json
index 58190e3170942e20c78027ebdafa8cffb6459343..dfe6c34698c040c46f36fd633b402049d5619989 100644
--- a/renovate.json
+++ b/renovate.json
@@ -31,6 +31,14 @@
       "datasourceTemplate": "docker",
       "versioningTemplate": "docker"
     },
+    {
+      "fileMatch": ["terraform/hedgedoc-demo/templates/docker-compose.tpl$"],
+      "matchStrings": [
+        "# renovate:\\n\\s+image: \"?'?(?<depName>[^:\\s]+?):(?<currentValue>[^\"]*?)\"?'?\\s"
+      ],
+      "datasourceTemplate": "docker",
+      "versioningTemplate": "docker"
+    },
     {
       "fileMatch": ["Chart\\.yaml$"],
       "matchStrings": [
diff --git a/terraform/hedgedoc-demo/.terraform.lock.hcl b/terraform/hedgedoc-demo/.terraform.lock.hcl
new file mode 100644
index 0000000000000000000000000000000000000000..dc7101c95c6e588038a4c69133a4450bfe92603d
--- /dev/null
+++ b/terraform/hedgedoc-demo/.terraform.lock.hcl
@@ -0,0 +1,104 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/cloudflare/cloudflare" {
+  version     = "3.34.0"
+  constraints = "3.34.0"
+  hashes = [
+    "h1:4jOI2ZUyp/EvIVE9rNvCMTqrIaG7j7XxUD1i43xD0Jg=",
+    "zh:03729b0fcf189e732aca54452a105d82fec839580cb5d0137317af9163e0e4dd",
+    "zh:121b16a779e9f2fe8c96e98f32514ee9228346fc240ce12c3fb440958b93d127",
+    "zh:14509f521845eedd57a8791d76958e50bea4928760a152cd853e43f2c81a329b",
+    "zh:273336ec2bc59ab90916706c074be27f3fe6ab42addc61a354a0ef5e10c2efa5",
+    "zh:54931c30f71666856c5d749698264c15196103667c87d961f3d293ff8a5c3237",
+    "zh:58a35eea3b9e1d2f39d7b5b1c6cf107b70eacdf5891017d6667902903db3bd94",
+    "zh:5ec958afe392a76a1fea262d9070df839c4d811fc6ffd613a37f8b939ab159ef",
+    "zh:7c24c0572aa9beee20a33cb18ac54d5088a09653e94664a9f74a9af2ae0e3554",
+    "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
+    "zh:9248c43f795dbe54e07c6dbc2fb8e2f20aeac8f21ec91373d52b9975f285ba7e",
+    "zh:b09abd506601b7c3e0b3bfde0b8b9e1aed7f52b5ad629ef2865b8321852409c7",
+    "zh:e00032df4cd4aad12adf3b7955fca3d1baa8bff9436c775588417da171a4e1d9",
+    "zh:e4a8812770914d6ce9d1f8399d702e3fb0ecc4bfd6220ba015fcb3884b243c69",
+    "zh:f2ad0991ef0820b3fc5bd0a500be4dceffe0b5b2ac6c9c5fd17cbb350f2f1209",
+    "zh:fea3a9dfb1e752dc2864028049a4af05fabf7b62eb57fff26d139a424e3476fd",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/local" {
+  version     = "2.3.0"
+  constraints = "2.3.0"
+  hashes = [
+    "h1:+l9ZTDGmGdwnuYI5ftUjwP8UgoLw4f4V9xoCzal4LW0=",
+    "zh:1f1920b3f78c31c6b69cdfe1e016a959667c0e2d01934e1a084b94d5a02cd9d2",
+    "zh:550a3cdae0ddb350942624e7b2e8b31d28bc15c20511553432413b1f38f4b214",
+    "zh:68d1d9ccbfce2ce56b28a23b22833a5369d4c719d6d75d50e101a8a8dbe33b9b",
+    "zh:6ae3ad6d865a906920c313ec2f413d080efe32c230aca711fd106b4cb9022ced",
+    "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+    "zh:a0f413d50f54124057ae3dcd9353a797b84e91dc34bcf85c34a06f8aef1f9b12",
+    "zh:a2ac6d4088ceddcd73d88505e18b8226a6e008bff967b9e2d04254ef71b4ac6b",
+    "zh:a851010672e5218bdd4c4ea1822706c9025ef813a03da716d647dd6f8e2cffb0",
+    "zh:aa797561755041ef2fad99ee9ffc12b5e724e246bb019b21d7409afc2ece3232",
+    "zh:c6afa960a20d776f54bb1fc260cd13ead17280ebd87f05b9abcaa841ed29d289",
+    "zh:df0975e86b30bb89717b8c8d6d4690b21db66de06e79e6d6cfda769f3304afe6",
+    "zh:f0d3cc3da72135efdbe8f4cfbfb0f2f7174827887990a5545e6db1981f0d3a7c",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/random" {
+  version = "3.4.3"
+  hashes = [
+    "h1:xZGZf18JjMS06pFa4NErzANI98qi59SEcBsOcS2P2yQ=",
+    "zh:41c53ba47085d8261590990f8633c8906696fa0a3c4b384ff6a7ecbf84339752",
+    "zh:59d98081c4475f2ad77d881c4412c5129c56214892f490adf11c7e7a5a47de9b",
+    "zh:686ad1ee40b812b9e016317e7f34c0d63ef837e084dea4a1f578f64a6314ad53",
+    "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+    "zh:84103eae7251384c0d995f5a257c72b0096605048f757b749b7b62107a5dccb3",
+    "zh:8ee974b110adb78c7cd18aae82b2729e5124d8f115d484215fd5199451053de5",
+    "zh:9dd4561e3c847e45de603f17fa0c01ae14cae8c4b7b4e6423c9ef3904b308dda",
+    "zh:bb07bb3c2c0296beba0beec629ebc6474c70732387477a65966483b5efabdbc6",
+    "zh:e891339e96c9e5a888727b45b2e1bb3fcbdfe0fd7c5b4396e4695459b38c8cb1",
+    "zh:ea4739860c24dfeaac6c100b2a2e357106a89d18751f7693f3c31ecf6a996f8d",
+    "zh:f0c76ac303fd0ab59146c39bc121c5d7d86f878e9a69294e29444d4c653786f8",
+    "zh:f143a9a5af42b38fed328a161279906759ff39ac428ebcfe55606e05e1518b93",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/template" {
+  version     = "2.2.0"
+  constraints = "2.2.0"
+  hashes = [
+    "h1:94qn780bi1qjrbC3uQtjJh3Wkfwd5+tTtJHOb7KTg9w=",
+    "zh:01702196f0a0492ec07917db7aaa595843d8f171dc195f4c988d2ffca2a06386",
+    "zh:09aae3da826ba3d7df69efeb25d146a1de0d03e951d35019a0f80e4f58c89b53",
+    "zh:09ba83c0625b6fe0a954da6fbd0c355ac0b7f07f86c91a2a97849140fea49603",
+    "zh:0e3a6c8e16f17f19010accd0844187d524580d9fdb0731f675ffcf4afba03d16",
+    "zh:45f2c594b6f2f34ea663704cc72048b212fe7d16fb4cfd959365fa997228a776",
+    "zh:77ea3e5a0446784d77114b5e851c970a3dde1e08fa6de38210b8385d7605d451",
+    "zh:8a154388f3708e3df5a69122a23bdfaf760a523788a5081976b3d5616f7d30ae",
+    "zh:992843002f2db5a11e626b3fc23dc0c87ad3729b3b3cff08e32ffb3df97edbde",
+    "zh:ad906f4cebd3ec5e43d5cd6dc8f4c5c9cc3b33d2243c89c5fc18f97f7277b51d",
+    "zh:c979425ddb256511137ecd093e23283234da0154b7fa8b21c2687182d9aea8b2",
+  ]
+}
+
+provider "registry.terraform.io/hetznercloud/hcloud" {
+  version     = "1.36.2"
+  constraints = "1.36.2"
+  hashes = [
+    "h1:VO/dl+g5NfJd436hmT+9NOMQk6oRU4Z9TSJJJrNlN0M=",
+    "zh:0498ef4209924b30ce7b4a232dd6aee08feab2ebbc90064db699adc10c16707e",
+    "zh:292e3c0c55d320cf164cdd431ee31580dd86f435aec99721597204bab5de3970",
+    "zh:3ce8558658baa7c4b9f1eeb92427665b4b930e5b157fbf352977778c90e11aaa",
+    "zh:46abd0bdeeba46b86832ed31338ad837b584f7b2152f8a9bfa6c3802f481a6da",
+    "zh:5804e71d411577f06abc0986c8c2e475c49042a192efce5936e4d5bdd874fc22",
+    "zh:7cef0782e8198346bfe7b61601e1cf8f2158280a5cf665140b72838545ca3127",
+    "zh:be81782af391ff4cc0859d976637aa00e6fe34061fe4f1df1f5ab5d62ef94f82",
+    "zh:bf2660e70edf758305085698fc9d05306b174b99559cd0f3f61c0b705ba22275",
+    "zh:caf727b0a378dc8c9c3594bbf176865f87aa732077820ff045eb352f5a48aeed",
+    "zh:cf95fc3121b358c7b7b667193ab36b8cb6140e2f6dfbf6f1b4c55b7fec1bb6ef",
+    "zh:d6d3119f8b971e982b6421dfa3b86314ccaeceaf047a3b6505f79e1a30f8301e",
+    "zh:e6f7f65dced2e88e3082c57ddcd118412595678cf3c7289bc7e12c724b3bd892",
+    "zh:f41f59ca511ab1a591d5abdc7f6d32d2e03a1d6087d206a741f95b7b0dd2ea17",
+    "zh:fbe59fbb5f272a6b206a380f6dbf49837b199960dd038afca2e89b11f72fdfda",
+  ]
+}
diff --git a/terraform/hedgedoc-demo/README.md b/terraform/hedgedoc-demo/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2349a9295a39c076777666a00173040ba8b39ead
--- /dev/null
+++ b/terraform/hedgedoc-demo/README.md
@@ -0,0 +1,55 @@
+Hedgedoc Demo Terraform Repository
+===
+
+This directory contains the deployment of the [Hedgedoc](https://hedgedoc.org) [Demo instance](https://demo.hedgedoc.org).
+
+The demo instance is hosted on [Hetzner Cloud](https://hetzner.cloud) in combination with [Cloudflare](https://cloudflare.com) as DNS, CDN and WAF provider and [Backblaze](https://backblaze.com) as Backup storage.
+
+The Terraform setup itself is slightly opinionated and allows reproduction of the instance by replacing the existing one, which includes a minor downtime, until the new instance has downloaded all images as started the containers.
+
+Features
+---
+
+This setup provides with some features that weren't available or limited before:
+
+1. Fully reproducible instance creation
+2. Automated update installation
+3. Automatic nightly deployment
+4. Automatic nightly backups including monitoring
+5. Fully self-contained setup -> All components (hedgedoc, s3, postgresql, reverse-proxy) of the demo instance run on the host
+
+To be done
+---
+
+- [x] Automated installation of Hedgedoc
+- [x] Automated docker-compose revisions
+- [x] Automated updates to latest nightly image
+- [x] Fully restricted network access to backend host
+- [x] Automated backup of Hedgedoc
+- [x] Automated backup monitoring
+- [ ] Automated restore of Hedgedoc
+- [ ] Add safeguard preventing volume deletion
+- [ ] Add safeguard preventing destruction while backup process in progress
+
+Usage
+---
+
+Be aware, that without the `.tfstate` this repository will produce another demo instance, not modify the existing one.
+
+1. Initialise terraform using `terraform init -upgrade`
+2. Decrypt or fill/adjust the `terraform.tfvars` file to your needs. (all variables are described in the `variables.tf` file, if you want to start from scratch)
+3. Run `terraform apply` and review the changes
+4. Initiate deployment of the changes by typing `yes`.
+
+Technology reference
+---
+
+- [terraform](https://developer.hashicorp.com/terraform/intro)
+- [rockylinux](https://docs.rockylinux.org/)
+- [docker](https://docs.docker.com)
+- [restic](https://restic.readthedocs.io/en/stable/)
+- [hedgedoc](https://docs.hedgedoc.org)
+- [traefik](https://docs.traefik.io/)
+- [postgresql](https://www.postgresql.org/docs/)
+- [minio](https://min.io/docs/minio/container/index.html)
+- [watchtower](https://containrrr.dev/watchtower/)
\ No newline at end of file
diff --git a/terraform/hedgedoc-demo/dns.tf b/terraform/hedgedoc-demo/dns.tf
new file mode 100644
index 0000000000000000000000000000000000000000..d0fd2d343e0aa9d2b066652955969a880fa134a2
--- /dev/null
+++ b/terraform/hedgedoc-demo/dns.tf
@@ -0,0 +1,17 @@
+resource "cloudflare_record" "demo-instance" {
+  count   = 1
+  zone_id = var.dns_zone_id
+  name    = "demo"
+  value   = module.hedgedoc.server_names[0]
+  type    = "CNAME"
+  proxied = true
+}
+
+resource "cloudflare_record" "s3-storage" {
+  count   = 1
+  zone_id = var.dns_zone_id
+  name    = "s3"
+  value   = module.hedgedoc.server_names[0]
+  type    = "CNAME"
+  proxied = true
+}
\ No newline at end of file
diff --git a/terraform/hedgedoc-demo/firewall.tf b/terraform/hedgedoc-demo/firewall.tf
new file mode 100644
index 0000000000000000000000000000000000000000..43ecaca490870315b45aca38d234d338165cd827
--- /dev/null
+++ b/terraform/hedgedoc-demo/firewall.tf
@@ -0,0 +1,62 @@
+data "cloudflare_ip_ranges" "cloudflare" {}
+
+
+resource "hcloud_firewall" "ingress-web" {
+  name = "ingress-web"
+  apply_to {
+    label_selector = "firewall.hetzner.si-infra.de/web=true"
+  }
+
+  # ICMP is always a good idea
+  #
+  # Network reachability tests
+  rule {
+   direction = "in"
+   protocol  = "icmp"
+   source_ips = [
+      "0.0.0.0/0",
+      "::/0"
+   ]
+  }
+
+  # Allow HTTP and HTTPS traffic in
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "80"
+      source_ips      = data.cloudflare_ip_ranges.cloudflare.cidr_blocks
+  }
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "443"
+      source_ips      = data.cloudflare_ip_ranges.cloudflare.cidr_blocks
+  }
+}
+
+resource "hcloud_firewall" "ingress-ssh" {
+  name = "ingress-ssh"
+  apply_to {
+    label_selector = "firewall.hetzner.si-infra.de/ssh=true"
+  }
+
+  # ICMP is always a good idea
+  #
+  # Network reachability tests
+  rule {
+   direction = "in"
+   protocol  = "icmp"
+   source_ips = [
+      "0.0.0.0/0",
+      "::/0"
+   ]
+  }
+
+   # Allow SSH traffic in
+  rule {
+      direction       = "in"
+      protocol        = "tcp"
+      port            = "22"
+      source_ips      = var.ssh_firewall_cidr
+  }
+}
diff --git a/terraform/hedgedoc-demo/main.tf b/terraform/hedgedoc-demo/main.tf
new file mode 100644
index 0000000000000000000000000000000000000000..845c89820863c49ba3173379ebedf8346023a8a6
--- /dev/null
+++ b/terraform/hedgedoc-demo/main.tf
@@ -0,0 +1,41 @@
+module "hedgedoc" {
+  source         = "../modules/hcloud_instance"
+  instance_count = 1
+  location       = var.location
+  name           = "srv"
+  dns_domain     = var.dns_domain
+  dns_zone_id    = var.dns_zone_id
+  dns_record_aaaa = false
+  image          = "rocky-9"
+  user_data = templatefile("templates/cloud-init.tpl", {
+                      docker_compose_file = templatefile("templates/docker-compose.tpl", {
+                            hedgedoc_db_user = "${var.hedgedoc_db_user}",
+                            hedgedoc_db_password = "${var.hedgedoc_db_password}",
+
+                            hedgedoc_session_secret = "${var.hedgedoc_session_secret}",
+
+                            hedgedoc_minio_accesskey = "${var.hedgedoc_minio_accesskey}",
+                            hedgedoc_minio_secretkey = "${var.hedgedoc_minio_secretkey}",
+                            hedgedoc_minio_endpoint = "${var.hedgedoc_minio_endpoint}",
+                            hedgedoc_minio_bucketname = "${var.hedgedoc_minio_bucketname}",
+
+                            hedgedoc_github_clientid = "${var.hedgedoc_github_clientid}",
+                            hedgedoc_github_clientsecret = "${var.hedgedoc_github_clientsecret}",
+                            letsencrypt_email = "${var.letsencrypt_email}",
+                        }),
+                      restic_b2_account_id = "${var.restic_b2_account_id}",
+                      restic_b2_account_key = "${var.restic_b2_account_key}",
+                      restic_repo_url = "${var.restic_repo_url}",
+                      restic_repo_password = "${var.restic_repo_password}",
+                      restic_monitoring_url = "${var.restic_monitoring_url}",
+                      hedgedoc_db_user = "${var.hedgedoc_db_user}",
+                })
+  ssh_keys       = data.hcloud_ssh_keys.all_keys.ssh_keys.*.name
+  server_type    = "cx11"
+  labels         = {
+    "firewall.hetzner.si-infra.de/web" = "true",
+    "firewall.hetzner.si-infra.de/ssh" = "true",
+  }
+  volume = true
+  volume_size = 45
+}
diff --git a/terraform/hedgedoc-demo/provider.tf b/terraform/hedgedoc-demo/provider.tf
new file mode 100644
index 0000000000000000000000000000000000000000..657bd302699a2536e7b7c210583f7f685667ff9c
--- /dev/null
+++ b/terraform/hedgedoc-demo/provider.tf
@@ -0,0 +1,16 @@
+provider "cloudflare" {
+    api_token = var.cloudflare_api_token
+}
+
+provider "hcloud" {
+    token = var.hcloud_token
+}
+
+provider "template" {
+}
+
+provider "local" {
+}
+
+provider "random" {
+}
diff --git a/terraform/hedgedoc-demo/ssh.tf b/terraform/hedgedoc-demo/ssh.tf
new file mode 100644
index 0000000000000000000000000000000000000000..db794f29d0e7b7cc56cb41a7ce3a1eedfa203cd5
--- /dev/null
+++ b/terraform/hedgedoc-demo/ssh.tf
@@ -0,0 +1,2 @@
+data "hcloud_ssh_keys" "all_keys" {
+}
diff --git a/terraform/hedgedoc-demo/templates/cloud-init.tpl b/terraform/hedgedoc-demo/templates/cloud-init.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..66c3d2de053c405bb6d491c0d439339ca83afe75
--- /dev/null
+++ b/terraform/hedgedoc-demo/templates/cloud-init.tpl
@@ -0,0 +1,169 @@
+#!/bin/bash
+
+set -e
+
+dnf upgrade -y
+
+# Enable automatic updates
+dnf install -y dnf-automatic
+
+systemctl enable --now dnf-automatic-install.timer
+
+# Install docker
+dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
+dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
+systemctl enable --now docker
+
+# Setup volume
+echo "/dev/sdb /srv xfs discard,nofail,defaults 0 0" >> /etc/fstab
+mount /dev/sdb /srv
+
+mountpoint /srv
+
+# Create docker-compose backup
+if -e /srv/docker-compose.yaml; then
+  mv /srv/docker-compose.yaml /srv/docker-compose.yaml.1
+fi
+
+CUR=1
+for i in $(seq 2 10); do
+    if -e "/srv/docker-compose.yaml.$${CUR}"; then
+        mv "/srv/docker-compose.yaml.$${CUR}" "/srv/docker-compose.yaml.$${i}"
+    else
+        break
+    fi
+    CUR="$${i}"
+done
+
+cat > /srv/docker-compose.yaml <<EOF
+${docker_compose_file}
+EOF
+
+cd /srv
+
+docker compose up -d
+
+dnf install -y epel-release
+dnf install -y restic
+
+mkdir -p /root/.cache/
+
+cat > /etc/sysconfig/restic <<EOF
+BACKUP_PATHS="/srv"
+B2_ACCOUNT_ID=${restic_b2_account_id}
+B2_ACCOUNT_KEY=${restic_b2_account_key}
+RETENTION_DAYS=7
+RETENTION_WEEKS=2
+RETENTION_MONTHS=3
+RETENTION_YEARS=1
+RESTIC_REPOSITORY=${restic_repo_url}
+RESTIC_PASSWORD=${restic_repo_password}
+RESTIC_CACHE_DIR=/root/.cache/restic
+EOF
+
+set -a
+source /etc/sysconfig/restic
+set +a
+
+if ! restic snapshots; then
+    restic init
+fi
+
+# Inspired by https://fedoramagazine.org/automate-backups-with-restic-and-systemd/
+# Inspired by https://www.mavjs.org/post/automatic-backup-restic-systemd-service/
+cat > /etc/systemd/system/offsite-backup.service <<EOF
+[Unit]
+Description=Offsite backup with restic
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+# Low CPU priority
+CPUShares=512
+EnvironmentFile=/etc/sysconfig/restic
+ExecStart=/usr/local/libexec/restic-backup-hedgedoc
+EOF
+
+# Inspired by https://github.com/mhw/restic-backup-scripts/blob/e3fe81ba25acc5ae3ccdb28807eea8b0be38ce20/postgresql-backup.sh
+cat > /usr/local/libexec/restic-backup-hedgedoc <<EOF
+#!/bin/bash
+set -e
+set -o pipefail
+
+# Run general backup, excluding the database
+/usr/bin/restic backup /srv \
+    --exclude=/srv/database/* \
+    --one-file-system \
+    --tag systemd.timer \
+    --tag persistentvolume
+
+cd /srv
+
+# Run database backup separately by dumping the database for consistency
+docker compose exec database pg_dump --create --clean --if-exists --no-owner --no-privileges -U ${hedgedoc_db_user} hedgedoc | \
+		gzip --rsyncable | \
+		restic backup \
+			--stdin --stdin-filename postgresql/hedgedoc.sql.gz \
+			--tag "systemd.timer" \
+			--tag "hedgedoc-database"
+
+# Notify monitoring about success
+curl --retry-all-errors --retry 10 '${restic_monitoring_url}'
+EOF
+chmod +x /usr/local/libexec/restic-backup-hedgedoc
+
+cat > /etc/systemd/system/offsite-backup-prune.service <<EOF
+[Unit]
+Description=Restic backup service (data pruning)
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=oneshot
+EnvironmentFile=/etc/sysconfig/restic
+ExecStart=/usr/bin/restic forget --prune \
+    --tag systemd.timer \
+    --group-by "paths,tags" \
+    --keep-daily \$RETENTION_DAYS \
+    --keep-weekly \$RETENTION_WEEKS \
+    --keep-monthly \$RETENTION_MONTHS \
+    --keep-yearly \$RETENTION_YEARS
+EOF
+
+cat > /etc/systemd/system/offsite-backup.timer <<EOF
+[Unit]
+Description=Offsite backup with restic
+After=network-online.target
+Wants=network-online.target
+
+[Timer]
+OnCalendar=daily
+Persistent=true
+RandomizedDelaySec=600
+
+[Install]
+WantedBy=timers.target
+EOF
+
+cat > /etc/systemd/system/offsite-backup-prune.timer <<EOF
+[Unit]
+Description=Prune data from the restic repository monthly
+After=network-online.target
+Wants=network-online.target
+
+[Timer]
+OnCalendar=*-*-* 4:00:00
+Persistent=true
+RandomizedDelaySec=600
+
+[Install]
+WantedBy=timers.target
+EOF
+
+systemctl daemon-reload
+systemctl enable --now offsite-backup.timer offsite-backup-prune.timer
+
+sleep 60
+
+reboot
diff --git a/terraform/hedgedoc-demo/templates/docker-compose.tpl b/terraform/hedgedoc-demo/templates/docker-compose.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..67b411ac2c14689b4dda35eed9dde52690e7da07
--- /dev/null
+++ b/terraform/hedgedoc-demo/templates/docker-compose.tpl
@@ -0,0 +1,165 @@
+version: '2'
+services:
+  database:
+    # renovate:
+    image: docker.io/library/postgres:13
+    mem_limit: 896mb
+    memswap_limit: 1024mb
+    read_only: true
+    tmpfs:
+      - /run/postgresql:size=512K
+      - /tmp:size=128K
+    stop_grace_period: 2m
+    stop_signal: SIGINT
+    environment:
+      - POSTGRES_USER=${hedgedoc_db_user}
+      - POSTGRES_PASSWORD=${hedgedoc_db_password}
+      - POSTGRES_DB=hedgedoc
+    volumes:
+      - ./database:/var/lib/postgresql/data:z
+    networks:
+      backend:
+    restart: always
+
+  hedgedoc:
+    # renovate:
+    image: quay.io/hedgedoc/hedgedoc-nightly:debian
+    mem_limit: 1gb
+    memswap_limit: 1536mb
+    read_only: true
+    tmpfs:
+      - /tmp:size=10M
+    depends_on:
+      - database
+    environment:
+      - "CMD_DB_URL=postgres://${hedgedoc_db_user}:${hedgedoc_db_password}@database:5432/hedgedoc"
+      - "CMD_DOMAIN=demo.hedgedoc.org"
+      - "CMD_PROTOCOL_USESSL=true"
+      - "CMD_URL_ADDPORT=false"
+      - "CMD_SESSION_SECRET=${hedgedoc_session_secret}"
+      - "CMD_MINIO_PORT=443"
+      - "CMD_MINIO_ENDPOINT=${hedgedoc_minio_endpoint}"
+      - "CMD_MINIO_ACCESS_KEY=${hedgedoc_minio_accesskey}"
+      - "CMD_MINIO_SECRET_KEY=${hedgedoc_minio_secretkey}"
+      - "CMD_MINIO_SECURE=true"
+      - "CMD_IMAGE_UPLOAD_TYPE=minio"
+      - "CMD_S3_BUCKET=${hedgedoc_minio_bucketname}"
+      - "CMD_GITHUB_CLIENTID=${hedgedoc_github_clientid}"
+      - "CMD_GITHUB_CLIENTSECRET=${hedgedoc_github_clientsecret}"
+      - "CMD_USECDN=false"
+      - "CMD_ALLOW_FREE_URL=false"
+      - "CMD_EMAIL=true"
+      - "CMD_ALLOW_PDF_EXPORT=false"
+      - "CMD_CSP_REPORTURI=https://hedgedoc.report-uri.com/r/d/csp/enforce"
+      - "CMD_OPENID=true"
+      - "CMD_TOOBUSY_LAG=250"
+      - "CMD_CSP_ALLOW_FRAMING=false"
+      - "CMD_CSP_ADD_DISQUS=false"
+      - "CMD_CSP_ADD_GOOGLE_ANALYTICS=false"
+
+    labels:
+      - "traefik.http.routers.hedgedoc_demo.rule=Host(\`demo.hedgedoc.org\`) && PathPrefix(\`/\`) && ! PathPrefix(\`/metrics\`)"
+      - "traefik.http.routers.hedgedoc_demo.entrypoints=websecure"
+      - "traefik.http.routers.hedgedoc_demo.tls=true"
+      - "traefik.http.routers.hedgedoc_demo.tls.certresolver=letsencrypt_http"
+      - "traefik.http.routers.hedgedoc_demo.middlewares=hedgedoc_demo"
+      - "traefik.http.routers.hedgedoc_demo.service=hedgedoc_demo"
+      - "traefik.http.services.hedgedoc_demo.loadbalancer.server.port=3000"
+      - "traefik.http.middlewares.hedgedoc_demo.headers.sslredirect=true"
+      - "traefik.http.middlewares.hedgedoc_demo.headers.stsSeconds=63072000"
+      - "traefik.http.middlewares.hedgedoc_demo.headers.browserXssFilter=true"
+      - "traefik.http.middlewares.hedgedoc_demo.headers.contentTypeNosniff=true"
+
+      - "traefik.enable=true"
+      - "traefik.port=3000"
+      - "traefik.docker.network=proxy"
+
+
+    volumes:
+      - './privacy.md:/hedgedoc/public/docs/privacy.md:ro,z'
+
+    networks:
+      backend:
+      proxy:
+    restart: always
+
+  minio:
+    # renovate:
+    image: docker.io/minio/minio:latest
+    mem_limit: 256mb
+    memswap_limit: 512mb
+    read_only: true
+    tmpfs:
+      - /root/.minio:size=128K
+    environment:
+      - "MINIO_ACCESS_KEY=${hedgedoc_minio_accesskey}"
+      - "MINIO_SECRET_KEY=${hedgedoc_minio_secretkey}"
+      - "MINIO_BROWSER=off"
+      - "MINIO_DOMAIN=${hedgedoc_minio_endpoint}"
+    volumes:
+      - "./s3-storage:/data:z"
+    command: ["server", "/data"]
+    restart: always
+    labels:
+      - "traefik.http.routers.minio_hedgedoc.rule=Host(\`${hedgedoc_minio_endpoint}\`) && PathPrefix(\`/\`)"
+      - "traefik.http.routers.minio_hedgedoc.entrypoints=websecure"
+      - "traefik.http.routers.minio_hedgedoc.tls=true"
+      - "traefik.http.routers.minio_hedgedoc.tls.certresolver=letsencrypt_http"
+      - "traefik.http.routers.minio_hedgedoc.middlewares=minio_hedgedoc"
+      - "traefik.http.routers.minio_hedgedoc.service=minio_hedgedoc"
+      - "traefik.http.services.minio_hedgedoc.loadbalancer.server.port=9000"
+      - "traefik.http.middlewares.minio_hedgedoc.headers.sslredirect=true"
+      - "traefik.http.middlewares.minio_hedgedoc.headers.stsSeconds=63072000"
+      - "traefik.http.middlewares.minio_hedgedoc.headers.referrerPolicy=no-referrer"
+      - "traefik.http.middlewares.minio_hedgedoc.headers.contentTypeNosniff=true"
+      - "traefik.http.middlewares.minio_hedgedoc.headers.browserXssFilter=true"
+      - "traefik.enable=true"
+      - "traefik.port=9000"
+      - "traefik.docker.network=proxy"
+
+    networks:
+      proxy:
+
+  proxy:
+    # renovate:
+    image: docker.io/library/traefik:v2.9
+    security_opt:
+      - "label:disable"
+    read_only: true
+    restart: always
+
+    ports:
+      - "80:80"
+      - "443:443"
+
+    volumes:
+      - "./acme:/etc/traefik/acme:z"
+      - "/var/run/docker.sock:/var/run/docker.sock"
+
+    command:
+      - "--entryPoints.web.address=:80"
+      - "--entryPoints.websecure.address=:443"
+      - "--accesslog=false"
+      - "--providers.docker=true"
+      - "--certificatesResolvers.letsencrypt_http.acme.email=${letsencrypt_email}"
+      - "--certificatesResolvers.letsencrypt_http.acme.storage=/etc/traefik/acme/acme-v2.json"
+      - "--certificatesResolvers.letsencrypt_http.acme.httpChallenge.entrypoint=web"
+
+    networks:
+      proxy:
+
+  watchtower:
+    # renovate:
+    image: docker.io/containrrr/watchtower:latest
+    restart: always
+    security_opt:
+      - "label:disable"
+    environment:
+      - "WATCHTOWER_CLEANUP=true"
+    volumes:
+      - "/var/run/docker.sock:/var/run/docker.sock"
+
+networks:
+  proxy:
+    name: proxy
+  backend:
\ No newline at end of file
diff --git a/terraform/hedgedoc-demo/terraform.enc.tfvars b/terraform/hedgedoc-demo/terraform.enc.tfvars
new file mode 100644
index 0000000000000000000000000000000000000000..7a71db85abe6b52f4d2af3c3b8603ca3b7a292f7
--- /dev/null
+++ b/terraform/hedgedoc-demo/terraform.enc.tfvars
@@ -0,0 +1,21 @@
+{
+	"data": "ENC[AES256_GCM,data:AvfL4Gvv1+/YJi/7Qcq42x24lK9irGWnTmo8cvazsYgMOciIyCz7Jru6LItoCMrALgXSfuT6v2ezgQJ++shTBllfOnGfvyk4nIjNeeJDwellVoardgiwBO+xxUSyHG6exbuODFw0xE0m3N1hSOyyy1wbOpG6pFGDfkZYtzD6F5MVzyGBpgg+py3/nggIMtWwrm03frSxWESggJ+bLQWsOmVqXVq9fWEAsB4tXfsaR3QaALcyAEeyVNB+111abhxuSLn/+jQHuzovQky8ORpLBGRlR7VtWj2Z6xnJ+KsSWtONJn1qdZddi24ybrJ33R8OCDX2ISQx0wV/XG5APAquAxeNsSik3nOBIqhHjWM9gTNrZohyfGc044WO9xF6zKqSKpuZ8zqnEqd/0qcWUvvFeFL4UEXtYugAQm1LbSj21ulYLEkO0KSMwHTLdeysa53KECBL9rkRh2FjXFtxva7ctDjE5uyfE4sqEB3VpdRu3Frd3nDTrKbI5qtf99Ujbv8fwCkrxKMEkYmHXGBRtBndabafknugUPTCfWk0eLLzxAJ/hSWJ/tFciALFPujqEdtM78zpBvYbmr68DuqO3zq8i2r+7tFhZKHYk6Sk/+kWB202I4RoO5d/MV1tx7ZSvFVeYxJJfRb+ZHR7RfEofLv+EE/Ku7YCb8ABzFPvnTZpqaLxc8BLnl5ahoZzvyhrngu5ZIwVjW9s/SkjynslPC0l2PH7gHpVLXH6rz3ggUU8siOW1885Khwp/Par3vG2C7wXGjXSVitzVM+ssRSI55TVr81ar2DJtriujPBR7vUJnQ1yexOOyPMwg0Q804MjXsFE0N4POB0zIOU390LscyEzLKblqUhi843yrZX+Y4n0AbmZpOUMsDgqqa9BhdrHtMGRGWd6xT+bDwc4mcysHS1VHr+oDaAU4oNh/Px4168DFsLhXexPBtneCV3AEZ1LscL0DjGxVlaNKD6hcl1JqIGwIM+6QbDmpAhjIPNFHR2PYBmYJ9Q92WYUkQw3/CubZ7amY7ItANERBPyYQ5uB1Oq/2LniL4QmLiBFZTOse6amunzwJMMkfEaKqHs+ELjPVSl3qCJjGtHVKDOybbRTX+k4Q22xMI8Vbk71ix60zkUNxnxxpwTaOPE/HhNsPWF3KuXJwXC+xlnG4SPjQAgk0qKyIGNBs0YFV8WVw5mKbSUTxNPndbU2Y1KYnU9L6vyKYBsNST9wg0ehiZPpZlmaRdHrnS7hTX6220nXuUM59ef0cBzZqrbGmVVTgxjI1KA+Cw7xJwr9iMf0JY6vb+s3V7gw9XZ5xJY20JRZsgrAzOAMBWBCtFjEt7UqqcloGQ6zDIsGhnMUdJC/Xxaf/PmxuD/RjUBk8mlaSfIAX43xKVJsRpcX96k+3GtF/lRGnu+AOprwsdqtx0mmdi1/GL8ph7/le+WZHvmyTZ00Y5VQ9mgX/BloY2dK+Yem6zCN/cxlwzTX0ZTdOZ93COs=,iv:wl3rX28j9AxiIldid/fnnbLUEW22WI+KpyH7i+GGJnM=,tag:aDwZAuHYWXhA3gpVIHG25g==,type:str]",
+	"sops": {
+		"kms": null,
+		"gcp_kms": null,
+		"azure_kv": null,
+		"hc_vault": null,
+		"age": null,
+		"lastmodified": "2023-03-01T00:12:49Z",
+		"mac": "ENC[AES256_GCM,data:0ooMWpnFkMGj2qI+ZrStExZoxRCkKLa00BqQLFIiG09Qq3JKrbixpR2a/AP7zloauB8TtSyl5DvXHSvpRvWMjvMhA61aUk6w3WQ0I3mdTZv7TLzP172lPo6DzvG35e8TAujH+fEUNfFW0zutMff/Z/9zWJJ5H9f6Z6IusrD1qkA=,iv:4YdQREC5g6chGJjFBKLW0RzZXIpsRHS3SxVEKixCgJU=,tag:AVNiRyZLwLmdesdyWwqp1g==,type:str]",
+		"pgp": [
+			{
+				"created_at": "2023-03-01T00:12:49Z",
+				"enc": "-----BEGIN PGP MESSAGE-----\n\nwcFMA7kpg2bgzVHcAQ/+MdRgsdX6mCEGY3Tuqy4Kf2HrCpV1eNLJIgEjMJXupTVZ\nqSVhT5TRDt1kSW7D0czHJTS/Nm9nD6Y0+MFY+UMK2VqFkLPatfkyMHli5JIskpR1\nyi+UsZ5o3UQBjccM7m91kaF3me/+92h2EE2monVFPYNsABlXmNjS9yqei2Jm63ry\nXVCWCpKPLmDDOpCZk7CnJggKO2f+gdgT3JGapkSU0j8to3JoleLuszSDxmiE8S7/\nrmMNTzRzOynN6lLZkS6nQQpYBM9fYzpyxN1AexeX2qzKT18oajcu7uws9Rq5Nr7Z\naS4sPmnJfkIwG5PVBlDg/5GKt4QiMIPHXFl86QW64Du2Roe4T01G3af2FzniDrxi\npL3CgXx+HpcievUI9+fs3as77r0DTIpHZOyTmTW4PDczqdNjviTIlCse/2GiYPhJ\ngHlBkGUoY1CWpJgU4bYZjeJ9DFWSVSkYvjLUO146TyiK+AmNCykff9ipXjH5TKto\nktT80Tgd2rgzgq+536aswr7a86X7PuYXjqJ4qXYM3F+7sRJF5ZEVcuOnsWzpZ8Rh\nAi5tuwWUmCR7bfJHShp381iZbkfT6TWwa4hOx9sXNLtebgf8DOynhPMP9itCIUnE\nJxg/1EtxOp4AvyjqLv0LgRv/tZa58HEGVSKBdWyR96Vq4JQqkVNF1S4PLUHuA+TS\nUQEBoS5+5Zo8nJp/LnZ7k1rzodu7MjZnwbnbkaXTjMZKl0gi2n2NLeITghfgWyQE\nqMUiOZAp+wM676u1r8Mnj8wILofk2EejikMK45bDp7BGQw==\n=xjMK\n-----END PGP MESSAGE-----",
+				"fp": "286791FB6648539775DB31B8FCB98C2A3EC6F601"
+			}
+		],
+		"unencrypted_suffix": "_unencrypted",
+		"version": "3.7.3"
+	}
+}
\ No newline at end of file
diff --git a/terraform/hedgedoc-demo/variables.tf b/terraform/hedgedoc-demo/variables.tf
new file mode 100644
index 0000000000000000000000000000000000000000..5192f852ca98d6a27cf127d3653ffda2f08e283d
--- /dev/null
+++ b/terraform/hedgedoc-demo/variables.tf
@@ -0,0 +1,137 @@
+variable "dns_domain" {
+  type        = string
+  description = "Name of the Cloudflare domain"
+  nullable = false
+}
+
+variable "dns_zone_id" {
+  type        = string
+  description = "Zone ID of the Cloudflare domain"
+  nullable = false
+}
+
+variable "location" {
+  type        = string
+  description = "Region used to deploy the Hetzner Cloud instance. See: https://docs.hetzner.cloud/#locations"
+  default     = "nbg1"
+  nullable = false
+}
+
+variable "ssh_firewall_cidr" {
+  type        = list(string)
+  description = "List of CIDRs addresses that are allow to SSH into the host"
+  default     = ["127.0.0.1/32"]
+  nullable = false
+}
+
+variable "hedgedoc_db_user" {
+  type = string
+  description = "Username used to authenticate to the PostgreSQL database. See: https://docs.hedgedoc.org/configuration/#hedgedoc-basics"
+  nullable = false
+}
+
+variable "hedgedoc_db_password" {
+  type = string
+  description = "Password used to authenticate to the PostgreSQL database. See: https://docs.hedgedoc.org/configuration/#hedgedoc-basics"
+  sensitive = true
+  nullable = false
+}
+
+variable "hedgedoc_session_secret" {
+  type = string
+  description = "Secret used for securing the Hedgedoc session token, this ensures after a restart users stay authenticated. See: https://docs.hedgedoc.org/configuration/#users-and-privileges"
+  sensitive = true
+  nullable = false
+}
+
+variable "hedgedoc_minio_accesskey" {
+  type = string
+  description = "S3/Minio access key/ id for authentication. See: https://docs.hedgedoc.org/guides/minio-image-upload/"
+  sensitive = true
+  nullable = false
+}
+
+variable "hedgedoc_minio_secretkey" {
+  type = string
+  description = "S3/Minio secret key for authentication. See: https://docs.hedgedoc.org/guides/minio-image-upload/"
+  sensitive = true
+  nullable = false
+}
+
+variable "hedgedoc_minio_endpoint" {
+  type = string
+  description = "Connection Endpoint used for uploading files to S3/Minio backend. See: https://docs.hedgedoc.org/guides/minio-image-upload/"
+  nullable = false
+}
+
+variable "hedgedoc_minio_bucketname" {
+  type = string
+  description = "Name of the S3/Minio Bucket. See: https://docs.hedgedoc.org/guides/minio-image-upload/"
+  nullable = false
+}
+
+variable "hedgedoc_github_clientid" {
+  type = string
+  description = "GitHub Client ID used for GitHub SSO. See: https://docs.hedgedoc.org/guides/auth/github/"
+  sensitive = true
+  nullable = false
+}
+
+variable "hedgedoc_github_clientsecret" {
+  type = string
+  description = "GitHub Client Secret used for GitHub SSO. See: https://docs.hedgedoc.org/guides/auth/github/"
+  sensitive = true
+  nullable = false
+}
+
+variable "restic_b2_account_id" {
+  type = string
+  description = "ID to access B2 backup storage. See: https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#backblaze-b2"
+  sensitive = true
+  nullable = false
+}
+
+variable "restic_b2_account_key" {
+  type = string
+  description = "Token to access B2 backup storage. See: https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#backblaze-b2"
+  sensitive = true
+  nullable = false
+}
+
+variable "restic_repo_url" {
+  type = string
+  description = "URL for restric backup location, should start with b2:. See: https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#backblaze-b2"
+  nullable = false
+}
+
+variable "restic_repo_password" {
+  type = string
+  description = "Password used to secure the restic backups. See: https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html"
+  sensitive = true
+  nullable = false
+}
+
+variable "restic_monitoring_url" {
+  type = string
+  description = "URL that curl will call after finishing a backup, allows push monitoring"
+  default = "http://example.com"
+  sensitive = true
+  nullable = false
+}
+
+variable "letsencrypt_email" {
+  description = "E-Mail address used for Let's Encrypt certificates"
+  nullable = false
+}
+
+variable "cloudflare_api_token" {
+  description = "API token for Cloudflare authentication. See: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/"
+  sensitive = true
+  nullable = false
+}
+
+variable "hcloud_token" {
+  description = "API token for Hetzner Cloud authentication. See: https://docs.hetzner.cloud/#getting-started"
+  sensitive = true
+  nullable = false
+}
diff --git a/terraform/hedgedoc-demo/versions.tf b/terraform/hedgedoc-demo/versions.tf
new file mode 120000
index 0000000000000000000000000000000000000000..8bd0ff1400c92ad24a8bebe8b67a5fbcbae252b2
--- /dev/null
+++ b/terraform/hedgedoc-demo/versions.tf
@@ -0,0 +1 @@
+../versions.tf
\ No newline at end of file