feat: Added automatic script copying functionality

This commit is contained in:
2026-07-10 22:58:56 +01:00
parent 41d93563d0
commit bab789a347
23 changed files with 284 additions and 18 deletions
+3
View File
@@ -61,6 +61,7 @@ module "dev" {
network_ip_range = var.network_ip_range network_ip_range = var.network_ip_range
bootstrap_script = local.bootstrap_script bootstrap_script = local.bootstrap_script
ssh_private_key_path = var.ssh_private_key_path ssh_private_key_path = var.ssh_private_key_path
deploy_user = var.deploy_user
runner_count = var.dev_runner_count runner_count = var.dev_runner_count
# module.network.id alone doesn't guarantee the subnet exists yet, and a server # module.network.id alone doesn't guarantee the subnet exists yet, and a server
@@ -82,6 +83,7 @@ module "prod" {
network_ip_range = var.network_ip_range network_ip_range = var.network_ip_range
bootstrap_script = local.bootstrap_script bootstrap_script = local.bootstrap_script
ssh_private_key_path = var.ssh_private_key_path ssh_private_key_path = var.ssh_private_key_path
deploy_user = var.deploy_user
depends_on = [module.network] depends_on = [module.network]
} }
@@ -96,6 +98,7 @@ module "vpn" {
allowed_ssh_source_ips = var.allowed_ssh_source_ips allowed_ssh_source_ips = var.allowed_ssh_source_ips
bootstrap_script = local.bootstrap_script bootstrap_script = local.bootstrap_script
ssh_private_key_path = var.ssh_private_key_path ssh_private_key_path = var.ssh_private_key_path
deploy_user = var.deploy_user
} }
module "dns" { module "dns" {
+76
View File
@@ -94,6 +94,14 @@ resource "hcloud_volume" "storage" {
} }
} }
locals {
# Hetzner's automount convention (hc-utils' 99-hc-volume-automount.rules) is
# always /mnt/HC_Volume_<id> - deterministic once the volume exists, since the
# id is stable for the volume's lifetime regardless of which server it's
# attached to.
data_dir = "/mnt/HC_Volume_${hcloud_volume.storage.id}"
}
# Renders services/dev/Runners/docker-compose.yml directly into the repo, with # Renders services/dev/Runners/docker-compose.yml directly into the repo, with
# one runner service per var.runner_count. This only touches the local working # one runner service per var.runner_count. This only touches the local working
# tree - deploying the change still goes through the normal scp + spinup.sh flow. # tree - deploying the change still goes through the normal scp + spinup.sh flow.
@@ -101,6 +109,74 @@ resource "local_file" "runners_compose" {
filename = "${path.root}/../services/dev/Runners/docker-compose.yml" filename = "${path.root}/../services/dev/Runners/docker-compose.yml"
content = templatefile("${path.module}/templates/runners-docker-compose.yml.tftpl", { content = templatefile("${path.module}/templates/runners-docker-compose.yml.tftpl", {
runner_count = var.runner_count runner_count = var.runner_count
data_dir = local.data_dir
}) })
file_permission = "0644" file_permission = "0644"
} }
# DATA_DIR is picked up automatically by `docker compose` (which auto-loads
# .env from its working directory) for every dev/*-docker-compose.yml bind
# mount - see services/dev/*.yml. Not committed (see .gitignore): it's tied to
# the live volume's ID, so it's regenerated by tofu apply, not handed off via git.
resource "local_file" "env" {
filename = "${path.root}/../services/dev/.env"
content = "DATA_DIR=${local.data_dir}\n"
file_permission = "0644"
}
locals {
# Everything under services/dev except the two files above: those are
# tracked via their own resource content (reading them back with fileset()/
# filesha1() before they exist would fail during `tofu plan`).
dev_static_files = sort([
for f in fileset("${path.root}/../services/dev", "**") :
f if f != ".env" && f != "Runners/docker-compose.yml"
])
dev_static_files_hash = sha1(join("", [
for f in local.dev_static_files : filesha1("${path.root}/../services/dev/${f}")
]))
}
# Copies services/dev to the server on every apply that changes it (a hand-edited
# compose file, a new runner count, ...) or recreates the server - not just once
# at first creation. Split from hcloud_server.this because it must run after the
# generated files above, which in turn depend on the volume, which depends on
# the server - so it can't be a provisioner on the server resource itself.
resource "null_resource" "deploy_services" {
triggers = {
server_id = hcloud_server.this.id
static_files = local.dev_static_files_hash
env = local_file.env.content
runners = local_file.runners_compose.content
}
connection {
type = "ssh"
host = hcloud_server.this.ipv4_address
user = "root"
private_key = file(var.ssh_private_key_path)
}
provisioner "remote-exec" {
inline = ["mkdir -p /home/${var.deploy_user}/services"]
}
provisioner "file" {
source = "${path.root}/../services/dev"
destination = "/home/${var.deploy_user}/services"
}
provisioner "remote-exec" {
inline = [
"chown -R ${var.deploy_user}:${var.deploy_user} /home/${var.deploy_user}/services",
"chmod +x /home/${var.deploy_user}/services/dev/*.sh",
]
}
depends_on = [
hcloud_server.this,
local_file.env,
local_file.runners_compose,
]
}
+5
View File
@@ -5,3 +5,8 @@ output "ipv4" {
output "private_ipv4" { output "private_ipv4" {
value = var.private_ip value = var.private_ip
} }
output "data_dir" {
description = "Deterministic host path of the auto-mounted volume - see local.data_dir."
value = local.data_dir
}
@@ -6,6 +6,11 @@
# GITEA_RUNNER_REGISTRATION_TOKEN is intentionally left as a compose variable # GITEA_RUNNER_REGISTRATION_TOKEN is intentionally left as a compose variable
# (not baked in here) - services/dev/spinup.sh generates a fresh token and # (not baked in here) - services/dev/spinup.sh generates a fresh token and
# writes it to Runners/.env immediately before starting these containers. # writes it to Runners/.env immediately before starting these containers.
#
# /data lives on dev's volume (${data_dir}) so runner identity survives a
# server rebuild - baked in directly rather than via .env, since Runners/.env
# is already reserved for the registration token above and gets overwritten
# on every deploy.
services: services:
%{ for i in range(runner_count) ~} %{ for i in range(runner_count) ~}
runner-${i + 1}: runner-${i + 1}:
@@ -13,7 +18,7 @@ services:
container_name: gitea_runner_${i + 1} container_name: gitea_runner_${i + 1}
volumes: volumes:
- ./config.yaml:/config.yaml - ./config.yaml:/config.yaml
- ./gitea_runner_${i + 1}:/data - ${data_dir}/gitea_runner_${i + 1}:/data
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
networks: networks:
- proxy - proxy
+5
View File
@@ -57,3 +57,8 @@ variable "runner_count" {
description = "Number of Gitea Actions runner containers to render into services/dev/Runners/docker-compose.yml." description = "Number of Gitea Actions runner containers to render into services/dev/Runners/docker-compose.yml."
type = number type = number
} }
variable "deploy_user" {
description = "Non-root sudo user created by the bootstrap script - services/dev is copied into this user's home directory."
type = string
}
+72
View File
@@ -120,3 +120,75 @@ resource "hcloud_volume" "storage" {
prevent_destroy = true prevent_destroy = true
} }
} }
locals {
# Hetzner's automount convention (hc-utils' 99-hc-volume-automount.rules) is
# always /mnt/HC_Volume_<id> - deterministic once the volume exists, since the
# id is stable for the volume's lifetime regardless of which server it's
# attached to.
data_dir = "/mnt/HC_Volume_${hcloud_volume.storage.id}"
}
# DATA_DIR is picked up automatically by `docker compose` (which auto-loads
# .env from its working directory) for every prod/*-docker-compose.yml bind
# mount - see services/prod/*.yml. Not committed (see .gitignore): it's tied to
# the live volume's ID, so it's regenerated by tofu apply, not handed off via git.
resource "local_file" "env" {
filename = "${path.root}/../services/prod/.env"
content = "DATA_DIR=${local.data_dir}\n"
file_permission = "0644"
}
locals {
# Everything under services/prod except .env: that's tracked via its own
# resource content (reading it back with fileset()/filesha1() before it
# exists would fail during `tofu plan`).
prod_static_files = sort([
for f in fileset("${path.root}/../services/prod", "**") : f if f != ".env"
])
prod_static_files_hash = sha1(join("", [
for f in local.prod_static_files : filesha1("${path.root}/../services/prod/${f}")
]))
}
# Copies services/prod to the server on every apply that changes it (a
# hand-edited compose file, ...) or recreates the server - not just once at
# first creation. Split from hcloud_server.this because it must run after
# local_file.env, which in turn depends on the volume, which depends on the
# server - so it can't be a provisioner on the server resource itself.
resource "null_resource" "deploy_services" {
triggers = {
server_id = hcloud_server.this.id
static_files = local.prod_static_files_hash
env = local_file.env.content
}
connection {
type = "ssh"
host = hcloud_server.this.ipv4_address
user = "root"
private_key = file(var.ssh_private_key_path)
}
provisioner "remote-exec" {
inline = ["mkdir -p /home/${var.deploy_user}/services"]
}
provisioner "file" {
source = "${path.root}/../services/prod"
destination = "/home/${var.deploy_user}/services"
}
provisioner "remote-exec" {
inline = [
"chown -R ${var.deploy_user}:${var.deploy_user} /home/${var.deploy_user}/services",
"chmod +x /home/${var.deploy_user}/services/prod/*.sh",
]
}
depends_on = [
hcloud_server.this,
local_file.env,
]
}
+5
View File
@@ -5,3 +5,8 @@ output "ipv4" {
output "private_ipv4" { output "private_ipv4" {
value = var.private_ip value = var.private_ip
} }
output "data_dir" {
description = "Deterministic host path of the auto-mounted volume - see local.data_dir."
value = local.data_dir
}
+5
View File
@@ -52,3 +52,8 @@ variable "ssh_private_key_path" {
description = "Local path to the private key matching one of var.ssh_key_ids, used to run the bootstrap script over SSH." description = "Local path to the private key matching one of var.ssh_key_ids, used to run the bootstrap script over SSH."
type = string type = string
} }
variable "deploy_user" {
description = "Non-root sudo user created by the bootstrap script - services/prod is copied into this user's home directory."
type = string
}
+44
View File
@@ -58,3 +58,47 @@ resource "hcloud_server" "this" {
] ]
} }
} }
locals {
vpn_static_files = sort(fileset("${path.root}/../services/vpn", "**"))
vpn_static_files_hash = sha1(join("", [
for f in local.vpn_static_files : filesha1("${path.root}/../services/vpn/${f}")
]))
}
# Copies services/vpn to the server on every apply that changes it (a
# hand-edited compose file, ...) or recreates the server - not just once at
# first creation. Split from hcloud_server.this so it's triggered by content
# changes independently of the bootstrap provisioners above.
resource "null_resource" "deploy_services" {
triggers = {
server_id = hcloud_server.this.id
static_files = local.vpn_static_files_hash
}
connection {
type = "ssh"
host = hcloud_server.this.ipv4_address
user = "root"
private_key = file(var.ssh_private_key_path)
}
provisioner "remote-exec" {
inline = ["mkdir -p /home/${var.deploy_user}/services"]
}
provisioner "file" {
source = "${path.root}/../services/vpn"
destination = "/home/${var.deploy_user}/services"
}
provisioner "remote-exec" {
inline = [
"chown -R ${var.deploy_user}:${var.deploy_user} /home/${var.deploy_user}/services",
"chmod +x /home/${var.deploy_user}/services/vpn/*.sh",
]
}
depends_on = [hcloud_server.this]
}
+5
View File
@@ -32,3 +32,8 @@ variable "ssh_private_key_path" {
description = "Local path to the private key matching one of var.ssh_key_ids, used to run the bootstrap script over SSH." description = "Local path to the private key matching one of var.ssh_key_ids, used to run the bootstrap script over SSH."
type = string type = string
} }
variable "deploy_user" {
description = "Non-root sudo user created by the bootstrap script - services/vpn is copied into this user's home directory."
type = string
}
+10
View File
@@ -14,6 +14,16 @@ output "prod_private_ipv4" {
value = module.prod.private_ipv4 value = module.prod.private_ipv4
} }
output "dev_data_dir" {
description = "Deterministic host path of dev's auto-mounted volume - also written to services/dev/.env as DATA_DIR."
value = module.dev.data_dir
}
output "prod_data_dir" {
description = "Deterministic host path of prod's auto-mounted volume - also written to services/prod/.env as DATA_DIR."
value = module.prod.data_dir
}
output "vpn_ipv4" { output "vpn_ipv4" {
value = module.vpn.ipv4 value = module.vpn.ipv4
} }
+4
View File
@@ -10,6 +10,10 @@ terraform {
source = "hashicorp/local" source = "hashicorp/local"
version = "~> 2.5" version = "~> 2.5"
} }
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
} }
} }
+20 -8
View File
@@ -77,11 +77,11 @@ architecture-beta
│ │ └── bootstrap.sh.tftpl # post-install script run on every host right after creation │ │ └── bootstrap.sh.tftpl # post-install script run on every host right after creation
│ └── modules/ │ └── modules/
│ ├── network/ # shared private network + subnet (used by dev and prod) │ ├── network/ # shared private network + subnet (used by dev and prod)
│ ├── dev/ # dev server + firewall + volume + renders services/dev/Runners/docker-compose.yml │ ├── dev/ # dev server + firewall + volume, renders + deploys services/dev/
│ │ └── templates/ │ │ └── templates/
│ │ └── runners-docker-compose.yml.tftpl # shape of each generated runner service │ │ └── runners-docker-compose.yml.tftpl # shape of each generated runner service
│ ├── prod/ # prod server + firewall + volume │ ├── prod/ # prod server + firewall + volume, deploys services/prod/
│ ├── vpn/ # vpn server + firewall (no private network, no volume) │ ├── vpn/ # vpn server + firewall (no private network, no volume), deploys services/vpn/
│ └── dns/ # Hetzner DNS zones + A records for every domain in var.dns_zones │ └── dns/ # Hetzner DNS zones + A records for every domain in var.dns_zones
├── services/ # Docker Compose stacks, grouped by which server they run on ├── services/ # Docker Compose stacks, grouped by which server they run on
│ ├── dev/ # Gitea + CI runner + Traefik (git.luke-else.co.uk, cicd.luke-else.co.uk) │ ├── dev/ # Gitea + CI runner + Traefik (git.luke-else.co.uk, cicd.luke-else.co.uk)
@@ -124,7 +124,7 @@ This creates, via `module.network` / `module.dev` / `module.prod` / `module.vpn`
- one `hcloud_volume` each for `dev` and `prod` (`modules/dev`, `modules/prod`; `vpn` has none) - one `hcloud_volume` each for `dev` and `prod` (`modules/dev`, `modules/prod`; `vpn` has none)
- one Hetzner DNS zone per domain in `var.dns_zones`, plus every A record in [Service inventory](#service-inventory) (`modules/dns` — see [Managing DNS](#managing-dns)) - one Hetzner DNS zone per domain in `var.dns_zones`, plus every A record in [Service inventory](#service-inventory) (`modules/dns` — see [Managing DNS](#managing-dns))
Useful outputs: `tofu output dev_ipv4`, `tofu output prod_ipv4`, `tofu output vpn_ipv4`, `tofu output dns_nameservers`. Useful outputs: `tofu output dev_ipv4`, `tofu output prod_ipv4`, `tofu output vpn_ipv4`, `tofu output dns_nameservers`, `tofu output dev_data_dir`, `tofu output prod_data_dir`.
`terraform.tfvars` and any `*.tfvars` file are gitignored — never commit real values there. Defaults for server sizes, locations, and IP ranges live in `infra/variables.tf` and are passed down into the modules from `infra/main.tf`; override them per-environment via `terraform.tfvars`. `terraform.tfvars` and any `*.tfvars` file are gitignored — never commit real values there. Defaults for server sizes, locations, and IP ranges live in `infra/variables.tf` and are passed down into the modules from `infra/main.tf`; override them per-environment via `terraform.tfvars`.
@@ -139,6 +139,16 @@ Immediately after each `hcloud_server` is created, OpenTofu uploads and runs [`i
After bootstrap, SSH in as `var.deploy_user` for day-to-day work — root key-based login still works as a fallback. After bootstrap, SSH in as `var.deploy_user` for day-to-day work — root key-based login still works as a fallback.
### Persistent data lives on the volumes, not the server disk
`dev` and `prod`'s compose files used to bind-mount container data to paths relative to wherever `services/<host>/` happened to be copied on the server (e.g. `./gitea:/data`) - i.e. the server's local disk, which doesn't survive the server being destroyed and recreated. `dev-storage` and `prod-storage` (the `hcloud_volume`s) do survive that, so all stateful bind mounts now point there instead: `${DATA_DIR}/gitea:/data`, `${DATA_DIR}/bitwarden/:/data/`, and so on across `services/dev/*.yml` and `services/prod/*.yml`. `vpn` has no volume, so it's untouched - see the architecture table above.
`DATA_DIR` needing to resolve to the right path is what makes this work, and that's answered deterministically: Hetzner's automount tooling always mounts an auto-mounted volume at `/mnt/HC_Volume_<volume-id>`, and since the volume is a Terraform resource, `modules/<host>/main.tf` already knows its `id` the moment it's created - `local.data_dir = "/mnt/HC_Volume_${hcloud_volume.storage.id}"`. Every `tofu apply` writes that value straight into `services/dev/.env` / `services/prod/.env` as `DATA_DIR=...`, which `docker compose` auto-loads for variable substitution in any compose file run from that directory (the same mechanism the Gitea runner token already uses). Query it directly with `tofu output dev_data_dir` / `tofu output prod_data_dir`.
These `.env` files are deliberately not committed (see `.gitignore`): the path is tied to the *live* volume's ID, so it's regenerated by `tofu apply`, not handed off via git. Run `tofu apply` at least once before the first deploy, and again if a volume is ever destroyed and recreated (new ID); `spinup.sh` on both hosts refuses to start anything if `.env` is missing, rather than silently falling back to a relative path.
The Gitea Actions runners are the one exception to the `.env` mechanism: their `/data` path is baked directly into the generated `Runners/docker-compose.yml` at render time (via the same `templatefile()` call as `dev_runner_count`), because `Runners/.env` is already reserved for the registration token and gets overwritten on every deploy.
### Scaling Gitea Actions runners ### Scaling Gitea Actions runners
The number of Gitea Actions runner containers on `dev` is an OpenTofu input, `var.dev_runner_count` (default `3`). On every `tofu apply`, `modules/dev`'s `local_file.runners_compose` resource renders [`modules/dev/templates/runners-docker-compose.yml.tftpl`](infra/modules/dev/templates/runners-docker-compose.yml.tftpl) straight into [`services/dev/Runners/docker-compose.yml`](services/dev/Runners/docker-compose.yml) in your working tree — one `runner-N` service per count, each with its own container name and `/data` volume so their registrations don't collide. That file is generated: change `dev_runner_count` in `terraform.tfvars` and re-run `tofu apply` rather than hand-editing it. The number of Gitea Actions runner containers on `dev` is an OpenTofu input, `var.dev_runner_count` (default `3`). On every `tofu apply`, `modules/dev`'s `local_file.runners_compose` resource renders [`modules/dev/templates/runners-docker-compose.yml.tftpl`](infra/modules/dev/templates/runners-docker-compose.yml.tftpl) straight into [`services/dev/Runners/docker-compose.yml`](services/dev/Runners/docker-compose.yml) in your working tree — one `runner-N` service per count, each with its own container name and `/data` volume so their registrations don't collide. That file is generated: change `dev_runner_count` in `terraform.tfvars` and re-run `tofu apply` rather than hand-editing it.
@@ -151,9 +161,9 @@ Registration tokens are handled automatically, not baked into the generated file
`dev` and `prod`'s firewalls only accept SSH from `vpn`'s public IP (see [Security notes](#security-notes)), but `vpn`'s own OpenVPN service isn't running until you deploy it — so on a from-scratch `tofu apply`, the `dev`/`prod` bootstrap provisioners can't connect yet. Bring the estate up in this order: `dev` and `prod`'s firewalls only accept SSH from `vpn`'s public IP (see [Security notes](#security-notes)), but `vpn`'s own OpenVPN service isn't running until you deploy it — so on a from-scratch `tofu apply`, the `dev`/`prod` bootstrap provisioners can't connect yet. Bring the estate up in this order:
1. `tofu apply -target=module.vpn` — creates `vpn` only; its firewall still allows SSH from `var.allowed_ssh_source_ips`, so its own bootstrap provisioner runs fine. 1. `tofu apply -target=module.vpn` — creates `vpn` only; its firewall still allows SSH from `var.allowed_ssh_source_ips`, so its bootstrap provisioner and `null_resource.deploy_services` (which copies `services/vpn` over) both run fine.
2. Deploy and start the VPN service on `vpn` (see [Deploying the services](#deploying-the-services-services)), then connect to it with an OpenVPN client. 2. SSH in as `var.deploy_user` and run `~/services/vpn/spinup.sh` to start the VPN service, then connect to it with an OpenVPN client.
3. `tofu apply` — creates `network`, `dev`, `prod`. Now that your machine is tunneled through `vpn`, its NATed egress IP matches the firewall rule and the `dev`/`prod` bootstrap provisioners can connect. 3. `tofu apply` — creates `network`, `dev`, `prod`. Now that your machine is tunneled through `vpn`, its NATed egress IP matches the firewall rule and the `dev`/`prod` bootstrap + deploy provisioners can connect.
If step 3 is run before you're connected to the VPN, the `dev`/`prod` provisioners will fail and Terraform will mark those servers tainted — re-running `tofu apply` once connected will recreate and re-bootstrap them. If step 3 is run before you're connected to the VPN, the `dev`/`prod` provisioners will fail and Terraform will mark those servers tainted — re-running `tofu apply` once connected will recreate and re-bootstrap them.
@@ -169,7 +179,7 @@ Only A records for IPv4 are managed here — none of the modules currently track
## Deploying the services (`services/`) ## Deploying the services (`services/`)
Once a server exists, copy the relevant `services/<host>/` directory to it (e.g. `scp -r services/prod user@<prod_ipv4>:~/services`) and run the matching script from inside that directory: `tofu apply` already puts `services/<host>/` on the matching server for you — a `null_resource.deploy_services` in each `modules/<host>/main.tf` copies it to `/home/<var.deploy_user>/services/<host>` and `chown`s it to `var.deploy_user`, re-running whenever the server is recreated or the directory's contents change (a hand-edited compose file, a new `dev_runner_count`, a refreshed `DATA_DIR`, ...) - not just once at first creation. So after applying, just SSH in as `var.deploy_user` and run the matching script from inside that directory:
```sh ```sh
# on dev # on dev
@@ -185,6 +195,8 @@ Once a server exists, copy the relevant `services/<host>/` directory to it (e.g.
./spindown.sh ./spindown.sh
``` ```
If you ever need to force a re-sync without going through Terraform (e.g. testing a local edit before committing it), a manual `scp -r services/prod deploy@<prod_ipv4>:~/services` still works fine - `tofu apply` will just overwrite it again next time triggers change.
Each stack can also be managed individually with plain Compose, e.g.: Each stack can also be managed individually with plain Compose, e.g.:
```sh ```sh
+6 -1
View File
@@ -6,13 +6,18 @@
# GITEA_RUNNER_REGISTRATION_TOKEN is intentionally left as a compose variable # GITEA_RUNNER_REGISTRATION_TOKEN is intentionally left as a compose variable
# (not baked in here) - services/dev/spinup.sh generates a fresh token and # (not baked in here) - services/dev/spinup.sh generates a fresh token and
# writes it to Runners/.env immediately before starting these containers. # writes it to Runners/.env immediately before starting these containers.
#
# /data lives on dev's volume (/mnt/HC_Volume_PENDING_TOFU_APPLY) so runner
# identity survives a server rebuild - baked in directly rather than via .env,
# since Runners/.env is already reserved for the registration token above and
# gets overwritten on every deploy.
services: services:
runner-1: runner-1:
image: gitea/act_runner:latest image: gitea/act_runner:latest
container_name: gitea_runner_1 container_name: gitea_runner_1
volumes: volumes:
- ./config.yaml:/config.yaml - ./config.yaml:/config.yaml
- ./gitea_runner_1:/data - /mnt/HC_Volume_PENDING_TOFU_APPLY/gitea_runner_1:/data
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
networks: networks:
- proxy - proxy
+1 -1
View File
@@ -4,7 +4,7 @@ services:
image: gitea/gitea:latest image: gitea/gitea:latest
container_name: gitea container_name: gitea
volumes: volumes:
- ./gitea:/data - ${DATA_DIR}/gitea:/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
networks: networks:
+5
View File
@@ -3,6 +3,11 @@
set -e set -e
cd "$(dirname "$0")" cd "$(dirname "$0")"
if [ ! -f .env ]; then
echo "Missing .env (expected DATA_DIR=/mnt/HC_Volume_...) - run 'tofu apply' in infra/ first, then re-copy this directory." >&2
exit 1
fi
docker compose -f traefik-docker-compose.yml pull && docker compose -f traefik-docker-compose.yml up -d docker compose -f traefik-docker-compose.yml pull && docker compose -f traefik-docker-compose.yml up -d
docker compose -f gitea-docker-compose.yml pull && docker compose -f gitea-docker-compose.yml up -d docker compose -f gitea-docker-compose.yml pull && docker compose -f gitea-docker-compose.yml up -d
+1 -1
View File
@@ -15,7 +15,7 @@ services:
- "80:80" - "80:80"
- "443:443" - "443:443"
volumes: volumes:
- "./letsencrypt:/letsencrypt" - "${DATA_DIR}/letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
networks: networks:
- proxy - proxy
+1 -1
View File
@@ -5,7 +5,7 @@ services:
image: "vaultwarden/server:latest" image: "vaultwarden/server:latest"
container_name: vaultwarden container_name: vaultwarden
volumes: volumes:
- ./bitwarden/:/data/ - ${DATA_DIR}/bitwarden/:/data/
networks: networks:
- proxy - proxy
labels: labels:
+2 -2
View File
@@ -6,7 +6,7 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./data:/root - ${DATA_DIR}/rustdesk:/root
networks: networks:
- proxy - proxy
@@ -39,7 +39,7 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./data:/root - ${DATA_DIR}/rustdesk:/root
networks: networks:
- proxy - proxy
+5
View File
@@ -3,6 +3,11 @@
set -e set -e
cd "$(dirname "$0")" cd "$(dirname "$0")"
if [ ! -f .env ]; then
echo "Missing .env (expected DATA_DIR=/mnt/HC_Volume_...) - run 'tofu apply' in infra/ first, then re-copy this directory." >&2
exit 1
fi
docker compose -f traefik-docker-compose.yml pull && docker compose -f traefik-docker-compose.yml up -d docker compose -f traefik-docker-compose.yml pull && docker compose -f traefik-docker-compose.yml up -d
sleep 20 # Allow Traefik + registry auth to settle before starting the rest of the services sleep 20 # Allow Traefik + registry auth to settle before starting the rest of the services
+1 -1
View File
@@ -3,7 +3,7 @@ services:
image: louislam/uptime-kuma:latest image: louislam/uptime-kuma:latest
container_name: status container_name: status
volumes: volumes:
- ./uptime-kuma/data:/app/data - ${DATA_DIR}/uptime-kuma/data:/app/data
networks: networks:
- proxy - proxy
labels: labels:
+1 -1
View File
@@ -20,7 +20,7 @@ services:
- "443:443" - "443:443"
- "27017:27017" - "27017:27017"
volumes: volumes:
- "./letsencrypt:/letsencrypt" - "${DATA_DIR}/letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
networks: networks:
- proxy - proxy
+1 -1
View File
@@ -81,7 +81,7 @@ services:
image: "php:apache" image: "php:apache"
container_name: snexo container_name: snexo
volumes: volumes:
- ./snexo.co.uk/:/var/www/html - ${DATA_DIR}/snexo.co.uk/:/var/www/html
networks: networks:
- proxy - proxy
labels: labels: