From 808cd16abbb515d1c10b23b3316ebeac4b1440ad Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 24 Aug 2026 18:02:27 +1000 Subject: [PATCH 1/3] feat(deployment): support custom control plane data volume --- deployment/.env.example | 3 ++ deployment/README.md | 21 ++++++++ deployment/compose.postgres.yml | 12 ++--- deployment/compose.production.yml | 10 ++-- deployment/install.sh | 84 ++++++++++++++++++++++++++++++- docs/installation.mdx | 34 +++++++++++++ 6 files changed, 152 insertions(+), 12 deletions(-) diff --git a/deployment/.env.example b/deployment/.env.example index 24b926a3..87c9ea41 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -42,6 +42,9 @@ INNGEST_EVENT_KEY=xxx # Control plane deployment # Use compose.production.yml for external PostgreSQL. COMPOSE_FILE=compose.production.yml +# Optional for fresh installs only. Point to an existing directory on a +# persistent, non-root mounted filesystem. The installer prepares subdirectories. +# TECHULUS_CLOUD_DATA_DIR=/mnt/HC_Volume_123/control-plane # Omit TECHULUS_CLOUD_VERSION to install GitHub's latest release. # Set it to a specific vX.Y.Z release to install that release. # TECHULUS_CLOUD_VERSION=vX.Y.Z diff --git a/deployment/README.md b/deployment/README.md index 7ffdcfd8..2c13e801 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -36,6 +36,27 @@ installer, which writes bounded `json-file` log settings on fresh Docker hosts. Prefer versioned or digest-pinned image references over mutable tags when you operate a long-lived deployment. +### Separate data volume + +On a fresh install, the installer can place persistent control plane data on an +existing mounted filesystem instead of Docker-managed volumes. Choose the custom +storage option interactively, or set this in the installer's `--env-file`: + +```env +TECHULUS_CLOUD_DATA_DIR=/mnt/HC_Volume_123/control-plane +``` + +The directory must already exist on a persistent mount separate from `/`. The +installer creates `letsencrypt`, `postgres`, `registry`, `victoria-logs`, +`victoria-metrics`, and `inngest` beneath it and configures Docker to start after +the backing mount. Docker images, container layers, and container logs remain in +Docker's data root. + +This is an install-time choice. Do not add or change the setting on an existing +deployment: the installer does not migrate existing named-volume data. Back up +the attached volume independently because server snapshots or backups may not +include it. + Health checks in these Compose files are for visibility. Plain Compose reports unhealthy containers but does not restart them automatically. diff --git a/deployment/compose.postgres.yml b/deployment/compose.postgres.yml index 010167e5..cdc6dc04 100644 --- a/deployment/compose.postgres.yml +++ b/deployment/compose.postgres.yml @@ -40,7 +40,7 @@ services: - "80:80" - "443:443" volumes: - - letsencrypt:/letsencrypt + - "${TECHULUS_CLOUD_DATA_DIR:-letsencrypt}${TECHULUS_CLOUD_DATA_DIR:+/letsencrypt}:/letsencrypt${TECHULUS_CLOUD_DATA_DIR:+:Z}" depends_on: docker-socket-proxy: condition: service_healthy @@ -60,7 +60,7 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} volumes: - - postgres-data:/var/lib/postgresql + - "${TECHULUS_CLOUD_DATA_DIR:-postgres-data}${TECHULUS_CLOUD_DATA_DIR:+/postgres}:/var/lib/postgresql${TECHULUS_CLOUD_DATA_DIR:+:Z}" healthcheck: test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] interval: 30s @@ -167,7 +167,7 @@ services: env_file: - ./.env volumes: - - registry-data:/var/lib/registry + - "${TECHULUS_CLOUD_DATA_DIR:-registry-data}${TECHULUS_CLOUD_DATA_DIR:+/registry}:/var/lib/registry${TECHULUS_CLOUD_DATA_DIR:+:Z}" labels: - "traefik.enable=true" - "traefik.http.routers.registry.rule=Host(`registry.${ROOT_DOMAIN}`)" @@ -186,7 +186,7 @@ services: env_file: - ./.env volumes: - - victoria-logs-data:/vlogs + - "${TECHULUS_CLOUD_DATA_DIR:-victoria-logs-data}${TECHULUS_CLOUD_DATA_DIR:+/victoria-logs}:/vlogs${TECHULUS_CLOUD_DATA_DIR:+:Z}" command: - "-storageDataPath=/vlogs" - "-retentionPeriod=${VL_RETENTION:-7d}" @@ -210,7 +210,7 @@ services: env_file: - ./.env volumes: - - victoria-metrics-data:/vmdata + - "${TECHULUS_CLOUD_DATA_DIR:-victoria-metrics-data}${TECHULUS_CLOUD_DATA_DIR:+/victoria-metrics}:/vmdata${TECHULUS_CLOUD_DATA_DIR:+:Z}" command: - "-storageDataPath=/vmdata" - "-retentionPeriod=${VM_RETENTION:-30d}" @@ -237,7 +237,7 @@ services: INNGEST_LOG_LEVEL: info INNGEST_SQLITE_DIR: "/data" volumes: - - inngest-data:/data + - "${TECHULUS_CLOUD_DATA_DIR:-inngest-data}${TECHULUS_CLOUD_DATA_DIR:+/inngest}:/data${TECHULUS_CLOUD_DATA_DIR:+:Z}" command: - "inngest" - "start" diff --git a/deployment/compose.production.yml b/deployment/compose.production.yml index 746bc1b9..fd4973ab 100644 --- a/deployment/compose.production.yml +++ b/deployment/compose.production.yml @@ -40,7 +40,7 @@ services: - "80:80" - "443:443" volumes: - - letsencrypt:/letsencrypt + - "${TECHULUS_CLOUD_DATA_DIR:-letsencrypt}${TECHULUS_CLOUD_DATA_DIR:+/letsencrypt}:/letsencrypt${TECHULUS_CLOUD_DATA_DIR:+:Z}" depends_on: docker-socket-proxy: condition: service_healthy @@ -146,7 +146,7 @@ services: env_file: - ./.env volumes: - - registry-data:/var/lib/registry + - "${TECHULUS_CLOUD_DATA_DIR:-registry-data}${TECHULUS_CLOUD_DATA_DIR:+/registry}:/var/lib/registry${TECHULUS_CLOUD_DATA_DIR:+:Z}" labels: - "traefik.enable=true" - "traefik.http.routers.registry.rule=Host(`registry.${ROOT_DOMAIN}`)" @@ -165,7 +165,7 @@ services: env_file: - ./.env volumes: - - victoria-logs-data:/vlogs + - "${TECHULUS_CLOUD_DATA_DIR:-victoria-logs-data}${TECHULUS_CLOUD_DATA_DIR:+/victoria-logs}:/vlogs${TECHULUS_CLOUD_DATA_DIR:+:Z}" command: - "-storageDataPath=/vlogs" - "-retentionPeriod=${VL_RETENTION:-7d}" @@ -189,7 +189,7 @@ services: env_file: - ./.env volumes: - - victoria-metrics-data:/vmdata + - "${TECHULUS_CLOUD_DATA_DIR:-victoria-metrics-data}${TECHULUS_CLOUD_DATA_DIR:+/victoria-metrics}:/vmdata${TECHULUS_CLOUD_DATA_DIR:+:Z}" command: - "-storageDataPath=/vmdata" - "-retentionPeriod=${VM_RETENTION:-30d}" @@ -216,7 +216,7 @@ services: INNGEST_LOG_LEVEL: info INNGEST_SQLITE_DIR: "/data" volumes: - - inngest-data:/data + - "${TECHULUS_CLOUD_DATA_DIR:-inngest-data}${TECHULUS_CLOUD_DATA_DIR:+/inngest}:/data${TECHULUS_CLOUD_DATA_DIR:+:Z}" command: - "inngest" - "start" diff --git a/deployment/install.sh b/deployment/install.sh index 6e2602d5..f6998a55 100755 --- a/deployment/install.sh +++ b/deployment/install.sh @@ -394,6 +394,31 @@ configure_interactive() { esac done + echo "" + log_info "Control plane storage" + echo -e " ${BOLD}1)${NC} Docker-managed volumes" + echo -e " ${BOLD}2)${NC} Existing mounted host directory" + echo "" + + local storage_choice + while true; do + read -rp "$(echo -e "${CYAN}Choose storage option [1]: ${NC}")" storage_choice + storage_choice="${storage_choice:-1}" + case "$storage_choice" in + 1) + unset TECHULUS_CLOUD_DATA_DIR + break + ;; + 2) + prompt_value TECHULUS_CLOUD_DATA_DIR "Enter the mounted data directory (e.g. /mnt/HC_Volume_123/control-plane)" + break + ;; + *) + log_warn "Please enter 1 or 2" + ;; + esac + done + BETTER_AUTH_SECRET="$(openssl rand -hex 32)" echo "" @@ -549,10 +574,62 @@ POSTGRES_DB=${POSTGRES_DB} EOF fi + if [[ -n "${TECHULUS_CLOUD_DATA_DIR:-}" ]]; then + printf '\nTECHULUS_CLOUD_DATA_DIR=%s\n' "$TECHULUS_CLOUD_DATA_DIR" >> "${DEPLOY_DIR}/.env" + fi + chmod 600 "${DEPLOY_DIR}/.env" log_success "Configuration written" } +prepare_data_directory() { + local data_dir mount_target mount_unit + data_dir="$(grep -E '^TECHULUS_CLOUD_DATA_DIR=' "${DEPLOY_DIR}/.env" | tail -1 | cut -d= -f2- || true)" + + if [[ -z "$data_dir" ]]; then + unset TECHULUS_CLOUD_DATA_DIR + return + fi + if [[ ! "$data_dir" =~ ^/[A-Za-z0-9._/-]+$ || "$data_dir" == "/" ]]; then + log_error "TECHULUS_CLOUD_DATA_DIR must be an absolute path containing only letters, numbers, '.', '_', '-', and '/'." + exit 1 + fi + if [[ ! -d "$data_dir" ]]; then + log_error "Data directory does not exist: ${data_dir}" + log_error "Attach and mount the volume, then create this directory before running the installer." + exit 1 + fi + + mount_target="$(findmnt -n -o TARGET --target "$data_dir" 2>/dev/null || true)" + if [[ -z "$mount_target" || "$mount_target" == "/" ]]; then + log_error "Data directory must be backed by a mounted filesystem separate from /: ${data_dir}" + exit 1 + fi + + mount_unit="$(systemd-escape --path --suffix=mount "$mount_target")" + if ! findmnt --fstab --mountpoint "$mount_target" >/dev/null 2>&1 && \ + ! systemctl is-enabled --quiet "$mount_unit" >/dev/null 2>&1; then + log_error "The filesystem mounted at ${mount_target} is not configured to persist after reboot." + log_error "Add it to /etc/fstab or enable ${mount_unit}, then run the installer again." + exit 1 + fi + + export TECHULUS_CLOUD_DATA_DIR="$data_dir" + install -m 0755 -d \ + "${data_dir}/letsencrypt" \ + "${data_dir}/postgres" \ + "${data_dir}/registry" \ + "${data_dir}/victoria-logs" \ + "${data_dir}/victoria-metrics" \ + "${data_dir}/inngest" + + install -m 0755 -d /etc/systemd/system/docker.service.d + printf '[Unit]\nRequiresMountsFor=%s\n' "$mount_target" > /etc/systemd/system/docker.service.d/techulus-cloud-storage.conf + systemctl daemon-reload + + log_success "Control plane data directory prepared at ${data_dir}" +} + build_and_start() { log_header "Starting Services" @@ -564,8 +641,9 @@ build_and_start() { echo "" log_header "Deployment Complete" - local root_domain + local root_domain data_dir root_domain="$(grep "^ROOT_DOMAIN=" "${DEPLOY_DIR}/.env" | cut -d'=' -f2)" + data_dir="$(grep -E '^TECHULUS_CLOUD_DATA_DIR=' "${DEPLOY_DIR}/.env" | tail -1 | cut -d= -f2- || true)" echo -e "${GREEN}${BOLD}Services are starting up!${NC}" echo "" @@ -575,6 +653,9 @@ build_and_start() { echo "" echo -e " ${BOLD}Config file:${NC} ${DEPLOY_DIR}/.env" echo -e " ${BOLD}Compose file:${NC} ${DEPLOY_DIR}/${COMPOSE_FILE}" + if [[ -n "$data_dir" ]]; then + echo -e " ${BOLD}Data directory:${NC} ${data_dir}" + fi echo "" echo -e "${YELLOW}It may take a few minutes for SSL certificates to be provisioned.${NC}" echo "" @@ -616,6 +697,7 @@ main() { configure_interactive fi + prepare_data_directory build_and_start } diff --git a/docs/installation.mdx b/docs/installation.mdx index 6ed632e0..b5408b3c 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -31,6 +31,39 @@ By default, the installer deploys the latest GitHub release and verifies its Compose files against the release manifest. Set `TECHULUS_CLOUD_VERSION` before running the script only when you need a specific release. +## Separate control plane data volume + +On a fresh install, you can store persistent control plane data on an existing +mounted filesystem instead of Docker-managed volumes. Attach and persistently +mount the volume first. Create a directory on it, then choose **Existing mounted +host directory** when the installer asks about control plane storage. + +For an unattended install, add the directory to the file passed through +`--env-file`: + +```env +TECHULUS_CLOUD_DATA_DIR=/mnt/HC_Volume_123/control-plane +``` + +The directory must exist on a mounted filesystem separate from `/`. The mount +must have an `/etc/fstab` entry or an enabled systemd mount unit. The installer +creates these subdirectories: + +- `letsencrypt` +- `postgres` +- `registry` +- `victoria-logs` +- `victoria-metrics` +- `inngest` + +Docker images, container layers, and container logs remain in Docker's data +root. This setting does not move all Docker storage. + +This is an install-time choice. Do not add or change the setting on an existing +deployment. The installer does not migrate existing named-volume data. Back up +the attached volume independently because your server provider may exclude it +from server snapshots and backups. + ## Manual Setup Clone the repository and configure your environment: @@ -243,6 +276,7 @@ Back up both PostgreSQL and continued access to the KMS key. Deleting the KMS ke | Variable | Description | | --- | --- | | `COMPOSE_FILE` | Compose file used by self-updates. Use `compose.production.yml` for external PostgreSQL or `compose.postgres.yml` for bundled PostgreSQL. | +| `TECHULUS_CLOUD_DATA_DIR` | Optional existing mounted directory for persistent control plane data. Set only during a fresh install. | | `TECHULUS_CLOUD_VERSION` | Installed release tag. The installer defaults to the latest GitHub release. | | `CONTROL_PLANE_UPDATER_TOKEN` | Random token used by the web app to call the internal updater service. Generate with `openssl rand -hex 32`. | From 55f324ed95b3de13cdef4ec395ce04db9f1e2d59 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 24 Aug 2026 21:52:04 +1000 Subject: [PATCH 2/3] refactor(deployment): limit custom storage to registry --- deployment/.env.example | 6 +- deployment/README.md | 27 +++--- deployment/compose.postgres.yml | 12 +-- deployment/compose.production.yml | 10 +-- deployment/install.sh | 132 +++++++++++++++++------------- docs/installation.mdx | 41 +++++----- 6 files changed, 123 insertions(+), 105 deletions(-) diff --git a/deployment/.env.example b/deployment/.env.example index 87c9ea41..904b4514 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -42,9 +42,9 @@ INNGEST_EVENT_KEY=xxx # Control plane deployment # Use compose.production.yml for external PostgreSQL. COMPOSE_FILE=compose.production.yml -# Optional for fresh installs only. Point to an existing directory on a -# persistent, non-root mounted filesystem. The installer prepares subdirectories. -# TECHULUS_CLOUD_DATA_DIR=/mnt/HC_Volume_123/control-plane +# Optional for fresh installs only. Point to an existing registry directory on +# a persistent, non-root mounted filesystem. +# TECHULUS_CLOUD_REGISTRY_DATA_DIR=/mnt/HC_Volume_123/registry # Omit TECHULUS_CLOUD_VERSION to install GitHub's latest release. # Set it to a specific vX.Y.Z release to install that release. # TECHULUS_CLOUD_VERSION=vX.Y.Z diff --git a/deployment/README.md b/deployment/README.md index 2c13e801..f320a953 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -36,26 +36,31 @@ installer, which writes bounded `json-file` log settings on fresh Docker hosts. Prefer versioned or digest-pinned image references over mutable tags when you operate a long-lived deployment. -### Separate data volume +### Separate registry volume -On a fresh install, the installer can place persistent control plane data on an -existing mounted filesystem instead of Docker-managed volumes. Choose the custom +On a fresh install, the installer can place registry data on an existing mounted +filesystem instead of a Docker-managed volume. Choose the custom registry storage option interactively, or set this in the installer's `--env-file`: ```env -TECHULUS_CLOUD_DATA_DIR=/mnt/HC_Volume_123/control-plane +TECHULUS_CLOUD_REGISTRY_DATA_DIR=/mnt/HC_Volume_123/registry ``` The directory must already exist on a persistent mount separate from `/`. The -installer creates `letsencrypt`, `postgres`, `registry`, `victoria-logs`, -`victoria-metrics`, and `inngest` beneath it and configures Docker to start after -the backing mount. Docker images, container layers, and container logs remain in -Docker's data root. +installer sets it to mode `0700` and configures Docker to start after the backing +mount. PostgreSQL, logs, metrics, Inngest, ACME data, Docker images, container +layers, and container logs remain in their existing storage locations. This is an install-time choice. Do not add or change the setting on an existing -deployment: the installer does not migrate existing named-volume data. Back up -the attached volume independently because server snapshots or backups may not -include it. +deployment: the installer does not migrate existing `registry-data` content. +Back up the attached volume independently because server snapshots or backups +may not include it. If you intentionally stop using the mount, remove +`/etc/systemd/system/docker.service.d/techulus-cloud-registry-storage.conf` and +run `systemctl daemon-reload` before restarting Docker. + +Moving registry data does not limit its growth. Follow the +[registry garbage collection](../docs/infrastructure/registry.mdx#garbage-collection) +guidance to reclaim unreferenced blobs. Health checks in these Compose files are for visibility. Plain Compose reports unhealthy containers but does not restart them automatically. diff --git a/deployment/compose.postgres.yml b/deployment/compose.postgres.yml index cdc6dc04..32d7a97d 100644 --- a/deployment/compose.postgres.yml +++ b/deployment/compose.postgres.yml @@ -40,7 +40,7 @@ services: - "80:80" - "443:443" volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-letsencrypt}${TECHULUS_CLOUD_DATA_DIR:+/letsencrypt}:/letsencrypt${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - letsencrypt:/letsencrypt depends_on: docker-socket-proxy: condition: service_healthy @@ -60,7 +60,7 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-postgres-data}${TECHULUS_CLOUD_DATA_DIR:+/postgres}:/var/lib/postgresql${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - postgres-data:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] interval: 30s @@ -167,7 +167,7 @@ services: env_file: - ./.env volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-registry-data}${TECHULUS_CLOUD_DATA_DIR:+/registry}:/var/lib/registry${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - "${TECHULUS_CLOUD_REGISTRY_DATA_DIR:-registry-data}:/var/lib/registry${TECHULUS_CLOUD_REGISTRY_DATA_DIR:+:Z}" labels: - "traefik.enable=true" - "traefik.http.routers.registry.rule=Host(`registry.${ROOT_DOMAIN}`)" @@ -186,7 +186,7 @@ services: env_file: - ./.env volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-victoria-logs-data}${TECHULUS_CLOUD_DATA_DIR:+/victoria-logs}:/vlogs${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - victoria-logs-data:/vlogs command: - "-storageDataPath=/vlogs" - "-retentionPeriod=${VL_RETENTION:-7d}" @@ -210,7 +210,7 @@ services: env_file: - ./.env volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-victoria-metrics-data}${TECHULUS_CLOUD_DATA_DIR:+/victoria-metrics}:/vmdata${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - victoria-metrics-data:/vmdata command: - "-storageDataPath=/vmdata" - "-retentionPeriod=${VM_RETENTION:-30d}" @@ -237,7 +237,7 @@ services: INNGEST_LOG_LEVEL: info INNGEST_SQLITE_DIR: "/data" volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-inngest-data}${TECHULUS_CLOUD_DATA_DIR:+/inngest}:/data${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - inngest-data:/data command: - "inngest" - "start" diff --git a/deployment/compose.production.yml b/deployment/compose.production.yml index fd4973ab..977e34a8 100644 --- a/deployment/compose.production.yml +++ b/deployment/compose.production.yml @@ -40,7 +40,7 @@ services: - "80:80" - "443:443" volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-letsencrypt}${TECHULUS_CLOUD_DATA_DIR:+/letsencrypt}:/letsencrypt${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - letsencrypt:/letsencrypt depends_on: docker-socket-proxy: condition: service_healthy @@ -146,7 +146,7 @@ services: env_file: - ./.env volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-registry-data}${TECHULUS_CLOUD_DATA_DIR:+/registry}:/var/lib/registry${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - "${TECHULUS_CLOUD_REGISTRY_DATA_DIR:-registry-data}:/var/lib/registry${TECHULUS_CLOUD_REGISTRY_DATA_DIR:+:Z}" labels: - "traefik.enable=true" - "traefik.http.routers.registry.rule=Host(`registry.${ROOT_DOMAIN}`)" @@ -165,7 +165,7 @@ services: env_file: - ./.env volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-victoria-logs-data}${TECHULUS_CLOUD_DATA_DIR:+/victoria-logs}:/vlogs${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - victoria-logs-data:/vlogs command: - "-storageDataPath=/vlogs" - "-retentionPeriod=${VL_RETENTION:-7d}" @@ -189,7 +189,7 @@ services: env_file: - ./.env volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-victoria-metrics-data}${TECHULUS_CLOUD_DATA_DIR:+/victoria-metrics}:/vmdata${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - victoria-metrics-data:/vmdata command: - "-storageDataPath=/vmdata" - "-retentionPeriod=${VM_RETENTION:-30d}" @@ -216,7 +216,7 @@ services: INNGEST_LOG_LEVEL: info INNGEST_SQLITE_DIR: "/data" volumes: - - "${TECHULUS_CLOUD_DATA_DIR:-inngest-data}${TECHULUS_CLOUD_DATA_DIR:+/inngest}:/data${TECHULUS_CLOUD_DATA_DIR:+:Z}" + - inngest-data:/data command: - "inngest" - "start" diff --git a/deployment/install.sh b/deployment/install.sh index f6998a55..39b488d4 100755 --- a/deployment/install.sh +++ b/deployment/install.sh @@ -344,6 +344,36 @@ prompt_value() { configure_interactive() { log_header "Configuration" + echo "" + log_info "Registry storage" + echo -e " ${BOLD}1)${NC} Docker-managed volume" + echo -e " ${BOLD}2)${NC} Existing mounted host directory" + echo "" + + local storage_choice + while true; do + read -rp "$(echo -e "${CYAN}Choose registry storage option [1]: ${NC}")" storage_choice + storage_choice="${storage_choice:-1}" + case "$storage_choice" in + 1) + unset TECHULUS_CLOUD_REGISTRY_DATA_DIR + break + ;; + 2) + while true; do + prompt_value TECHULUS_CLOUD_REGISTRY_DATA_DIR "Enter the mounted registry directory (e.g. /mnt/HC_Volume_123/registry)" + if validate_registry_data_directory "$TECHULUS_CLOUD_REGISTRY_DATA_DIR"; then + break + fi + done + break + ;; + *) + log_warn "Please enter 1 or 2" + ;; + esac + done + prompt_value ROOT_DOMAIN "Enter your root domain (e.g. cloud.example.com)" local public_ip @@ -394,31 +424,6 @@ configure_interactive() { esac done - echo "" - log_info "Control plane storage" - echo -e " ${BOLD}1)${NC} Docker-managed volumes" - echo -e " ${BOLD}2)${NC} Existing mounted host directory" - echo "" - - local storage_choice - while true; do - read -rp "$(echo -e "${CYAN}Choose storage option [1]: ${NC}")" storage_choice - storage_choice="${storage_choice:-1}" - case "$storage_choice" in - 1) - unset TECHULUS_CLOUD_DATA_DIR - break - ;; - 2) - prompt_value TECHULUS_CLOUD_DATA_DIR "Enter the mounted data directory (e.g. /mnt/HC_Volume_123/control-plane)" - break - ;; - *) - log_warn "Please enter 1 or 2" - ;; - esac - done - BETTER_AUTH_SECRET="$(openssl rand -hex 32)" echo "" @@ -472,7 +477,7 @@ AWS_REGION=${AWS_REGION}" configure_from_file() { local src_file="$1" - local configured_compose_file + local configured_compose_file registry_data_dir log_header "Configuration (from file)" if [[ ! -f "$src_file" ]]; then @@ -487,6 +492,11 @@ configure_from_file() { exit 1 fi + registry_data_dir="$(grep -E '^TECHULUS_CLOUD_REGISTRY_DATA_DIR=' "$src_file" | tail -1 | cut -d= -f2- || true)" + if [[ -n "$registry_data_dir" ]] && ! validate_registry_data_directory "$registry_data_dir"; then + exit 1 + fi + local temp_path temp_path="$(mktemp "${DEPLOY_DIR}/.env.tmp.XXXXXX")" @@ -574,36 +584,31 @@ POSTGRES_DB=${POSTGRES_DB} EOF fi - if [[ -n "${TECHULUS_CLOUD_DATA_DIR:-}" ]]; then - printf '\nTECHULUS_CLOUD_DATA_DIR=%s\n' "$TECHULUS_CLOUD_DATA_DIR" >> "${DEPLOY_DIR}/.env" + if [[ -n "${TECHULUS_CLOUD_REGISTRY_DATA_DIR:-}" ]]; then + printf '\nTECHULUS_CLOUD_REGISTRY_DATA_DIR=%s\n' "$TECHULUS_CLOUD_REGISTRY_DATA_DIR" >> "${DEPLOY_DIR}/.env" fi chmod 600 "${DEPLOY_DIR}/.env" log_success "Configuration written" } -prepare_data_directory() { - local data_dir mount_target mount_unit - data_dir="$(grep -E '^TECHULUS_CLOUD_DATA_DIR=' "${DEPLOY_DIR}/.env" | tail -1 | cut -d= -f2- || true)" +validate_registry_data_directory() { + local registry_data_dir="$1" mount_target mount_unit - if [[ -z "$data_dir" ]]; then - unset TECHULUS_CLOUD_DATA_DIR - return - fi - if [[ ! "$data_dir" =~ ^/[A-Za-z0-9._/-]+$ || "$data_dir" == "/" ]]; then - log_error "TECHULUS_CLOUD_DATA_DIR must be an absolute path containing only letters, numbers, '.', '_', '-', and '/'." - exit 1 + if [[ ! "$registry_data_dir" =~ ^/[A-Za-z0-9._/-]+$ || "$registry_data_dir" == "/" ]]; then + log_error "TECHULUS_CLOUD_REGISTRY_DATA_DIR must be an unquoted absolute path containing only letters, numbers, '.', '_', '-', and '/'." + return 1 fi - if [[ ! -d "$data_dir" ]]; then - log_error "Data directory does not exist: ${data_dir}" + if [[ ! -d "$registry_data_dir" ]]; then + log_error "Registry data directory does not exist: ${registry_data_dir}" log_error "Attach and mount the volume, then create this directory before running the installer." - exit 1 + return 1 fi - mount_target="$(findmnt -n -o TARGET --target "$data_dir" 2>/dev/null || true)" + mount_target="$(findmnt --first-only -n -o TARGET --target "$registry_data_dir" 2>/dev/null || true)" if [[ -z "$mount_target" || "$mount_target" == "/" ]]; then - log_error "Data directory must be backed by a mounted filesystem separate from /: ${data_dir}" - exit 1 + log_error "Registry data directory must be backed by a mounted filesystem separate from /: ${registry_data_dir}" + return 1 fi mount_unit="$(systemd-escape --path --suffix=mount "$mount_target")" @@ -611,23 +616,32 @@ prepare_data_directory() { ! systemctl is-enabled --quiet "$mount_unit" >/dev/null 2>&1; then log_error "The filesystem mounted at ${mount_target} is not configured to persist after reboot." log_error "Add it to /etc/fstab or enable ${mount_unit}, then run the installer again." + return 1 + fi + + REGISTRY_DATA_MOUNT_TARGET="$mount_target" +} + +prepare_registry_data_directory() { + local registry_data_dir + registry_data_dir="$(grep -E '^TECHULUS_CLOUD_REGISTRY_DATA_DIR=' "${DEPLOY_DIR}/.env" | tail -1 | cut -d= -f2- || true)" + + if [[ -z "$registry_data_dir" ]]; then + unset TECHULUS_CLOUD_REGISTRY_DATA_DIR + return + fi + if ! validate_registry_data_directory "$registry_data_dir"; then exit 1 fi - export TECHULUS_CLOUD_DATA_DIR="$data_dir" - install -m 0755 -d \ - "${data_dir}/letsencrypt" \ - "${data_dir}/postgres" \ - "${data_dir}/registry" \ - "${data_dir}/victoria-logs" \ - "${data_dir}/victoria-metrics" \ - "${data_dir}/inngest" + export TECHULUS_CLOUD_REGISTRY_DATA_DIR="$registry_data_dir" + install -m 0700 -d "$registry_data_dir" install -m 0755 -d /etc/systemd/system/docker.service.d - printf '[Unit]\nRequiresMountsFor=%s\n' "$mount_target" > /etc/systemd/system/docker.service.d/techulus-cloud-storage.conf + printf '[Unit]\nRequiresMountsFor=%s\n' "$REGISTRY_DATA_MOUNT_TARGET" > /etc/systemd/system/docker.service.d/techulus-cloud-registry-storage.conf systemctl daemon-reload - log_success "Control plane data directory prepared at ${data_dir}" + log_success "Registry data directory prepared at ${registry_data_dir}" } build_and_start() { @@ -641,9 +655,9 @@ build_and_start() { echo "" log_header "Deployment Complete" - local root_domain data_dir + local root_domain registry_data_dir root_domain="$(grep "^ROOT_DOMAIN=" "${DEPLOY_DIR}/.env" | cut -d'=' -f2)" - data_dir="$(grep -E '^TECHULUS_CLOUD_DATA_DIR=' "${DEPLOY_DIR}/.env" | tail -1 | cut -d= -f2- || true)" + registry_data_dir="$(grep -E '^TECHULUS_CLOUD_REGISTRY_DATA_DIR=' "${DEPLOY_DIR}/.env" | tail -1 | cut -d= -f2- || true)" echo -e "${GREEN}${BOLD}Services are starting up!${NC}" echo "" @@ -653,8 +667,8 @@ build_and_start() { echo "" echo -e " ${BOLD}Config file:${NC} ${DEPLOY_DIR}/.env" echo -e " ${BOLD}Compose file:${NC} ${DEPLOY_DIR}/${COMPOSE_FILE}" - if [[ -n "$data_dir" ]]; then - echo -e " ${BOLD}Data directory:${NC} ${data_dir}" + if [[ -n "$registry_data_dir" ]]; then + echo -e " ${BOLD}Registry data:${NC} ${registry_data_dir}" fi echo "" echo -e "${YELLOW}It may take a few minutes for SSL certificates to be provisioned.${NC}" @@ -697,7 +711,7 @@ main() { configure_interactive fi - prepare_data_directory + prepare_registry_data_directory build_and_start } diff --git a/docs/installation.mdx b/docs/installation.mdx index b5408b3c..d0dafeb6 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -31,38 +31,37 @@ By default, the installer deploys the latest GitHub release and verifies its Compose files against the release manifest. Set `TECHULUS_CLOUD_VERSION` before running the script only when you need a specific release. -## Separate control plane data volume +## Separate registry data volume -On a fresh install, you can store persistent control plane data on an existing -mounted filesystem instead of Docker-managed volumes. Attach and persistently -mount the volume first. Create a directory on it, then choose **Existing mounted -host directory** when the installer asks about control plane storage. +On a fresh install, you can store registry data on an existing mounted filesystem +instead of a Docker-managed volume. Attach and persistently mount the volume +first. Create a directory on it, then choose **Existing mounted host directory** +when the installer asks about registry storage. For an unattended install, add the directory to the file passed through `--env-file`: ```env -TECHULUS_CLOUD_DATA_DIR=/mnt/HC_Volume_123/control-plane +TECHULUS_CLOUD_REGISTRY_DATA_DIR=/mnt/HC_Volume_123/registry ``` The directory must exist on a mounted filesystem separate from `/`. The mount must have an `/etc/fstab` entry or an enabled systemd mount unit. The installer -creates these subdirectories: - -- `letsencrypt` -- `postgres` -- `registry` -- `victoria-logs` -- `victoria-metrics` -- `inngest` - -Docker images, container layers, and container logs remain in Docker's data -root. This setting does not move all Docker storage. +sets the directory to mode `0700` and configures Docker to start after the backing +mount. PostgreSQL, logs, metrics, Inngest, ACME data, Docker images, container +layers, and container logs remain in their existing storage locations. This is an install-time choice. Do not add or change the setting on an existing -deployment. The installer does not migrate existing named-volume data. Back up -the attached volume independently because your server provider may exclude it -from server snapshots and backups. +deployment. The installer does not migrate existing `registry-data` content. +Back up the attached volume independently because your server provider may +exclude it from server snapshots and backups. If you intentionally stop using +the mount, remove +`/etc/systemd/system/docker.service.d/techulus-cloud-registry-storage.conf` and +run `systemctl daemon-reload` before restarting Docker. + +Moving registry data does not limit its growth. Follow the +[registry garbage collection guide](/infrastructure/registry#garbage-collection) +to reclaim unreferenced blobs. ## Manual Setup @@ -276,7 +275,7 @@ Back up both PostgreSQL and continued access to the KMS key. Deleting the KMS ke | Variable | Description | | --- | --- | | `COMPOSE_FILE` | Compose file used by self-updates. Use `compose.production.yml` for external PostgreSQL or `compose.postgres.yml` for bundled PostgreSQL. | -| `TECHULUS_CLOUD_DATA_DIR` | Optional existing mounted directory for persistent control plane data. Set only during a fresh install. | +| `TECHULUS_CLOUD_REGISTRY_DATA_DIR` | Optional existing mounted directory for registry data. Set only during a fresh install. | | `TECHULUS_CLOUD_VERSION` | Installed release tag. The installer defaults to the latest GitHub release. | | `CONTROL_PLANE_UPDATER_TOKEN` | Random token used by the web app to call the internal updater service. Generate with `openssl rand -hex 32`. | From 10c163592263c6b20cff2db57250f23e6dfd1e2d Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 24 Aug 2026 22:17:51 +1000 Subject: [PATCH 3/3] fix(registry): share SELinux label with maintenance jobs --- deployment/compose.postgres.yml | 2 +- deployment/compose.production.yml | 2 +- docs/infrastructure/registry.mdx | 5 ++++- registry/README.md | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/deployment/compose.postgres.yml b/deployment/compose.postgres.yml index 32d7a97d..9163d5a3 100644 --- a/deployment/compose.postgres.yml +++ b/deployment/compose.postgres.yml @@ -167,7 +167,7 @@ services: env_file: - ./.env volumes: - - "${TECHULUS_CLOUD_REGISTRY_DATA_DIR:-registry-data}:/var/lib/registry${TECHULUS_CLOUD_REGISTRY_DATA_DIR:+:Z}" + - "${TECHULUS_CLOUD_REGISTRY_DATA_DIR:-registry-data}:/var/lib/registry${TECHULUS_CLOUD_REGISTRY_DATA_DIR:+:z}" labels: - "traefik.enable=true" - "traefik.http.routers.registry.rule=Host(`registry.${ROOT_DOMAIN}`)" diff --git a/deployment/compose.production.yml b/deployment/compose.production.yml index 977e34a8..2892abb6 100644 --- a/deployment/compose.production.yml +++ b/deployment/compose.production.yml @@ -146,7 +146,7 @@ services: env_file: - ./.env volumes: - - "${TECHULUS_CLOUD_REGISTRY_DATA_DIR:-registry-data}:/var/lib/registry${TECHULUS_CLOUD_REGISTRY_DATA_DIR:+:Z}" + - "${TECHULUS_CLOUD_REGISTRY_DATA_DIR:-registry-data}:/var/lib/registry${TECHULUS_CLOUD_REGISTRY_DATA_DIR:+:z}" labels: - "traefik.enable=true" - "traefik.http.routers.registry.rule=Host(`registry.${ROOT_DOMAIN}`)" diff --git a/docs/infrastructure/registry.mdx b/docs/infrastructure/registry.mdx index 71861d4b..35e41f6d 100644 --- a/docs/infrastructure/registry.mdx +++ b/docs/infrastructure/registry.mdx @@ -43,7 +43,10 @@ The TLS setting applies to agent-managed Podman pulls, build exports, and manife ## Storage -Images are stored on the local filesystem in a persistent Docker volume (`registry-data`). Delete operations are enabled for garbage collection. +Images are stored on the local filesystem. The default deployment uses the +persistent Docker volume `registry-data`; fresh installs can instead configure +an [external registry data directory](/installation#separate-registry-data-volume). +Delete operations are enabled for garbage collection. ### Garbage collection diff --git a/registry/README.md b/registry/README.md index 6e012cf1..c5526364 100644 --- a/registry/README.md +++ b/registry/README.md @@ -13,7 +13,8 @@ docker compose up -d - **Port**: 5000 - **Storage**: Filesystem at `/var/lib/registry` - **Delete**: Enabled (for garbage collection) -- **Data**: Persisted in `registry-data` volume +- **Data**: Persisted in `registry-data` by default, or the optional + `TECHULUS_CLOUD_REGISTRY_DATA_DIR` bind mount on fresh installs ## Image Naming