From dc4ee9a8db48b254f6c825e0a656a4fa992645be Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Mon, 20 Jul 2026 07:33:09 -0700 Subject: [PATCH] Scale DB tuning from DB_MEMORY; order Docker after the rclone mount (#226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two prod-readiness hardening changes: DB tuning scales with the container budget. Replace the fixed 8 GB 20-tuning.sql with 20-tuning.sh, which derives shared_buffers (25%), effective_cache_size (75%), work_mem, maintenance_work_mem and duckdb.max_memory (50%) from the DB_MEMORY the compose db service now passes in. The ratios reproduce the historical 8 GB tuning exactly and scale linearly, so the 48 GB prod box (db_memory 16g) gets shared_buffers 4 GB / duckdb 8 GB with no separate edit. Beta/local (8g default) are unchanged. Docs that told operators to raise the tuning by hand are updated. Boot ordering for the self-hosted archive. On an openmeteo host, install a docker.service drop-in (Wants/After rclone-om.service) so Docker starts after the object-storage mount is ready on every boot — the restart-policy containers never bind an empty mount point. rclone-om is Type=notify, so After waits for the mount to actually be ready. Cleaned up when openmeteo is toggled off. --- deploy/db/init/20-tuning.sh | 64 ++++++++++++++++++++++ deploy/db/init/20-tuning.sql | 31 ----------- deploy/openmeteo/README.md | 17 ++++++ docker-compose.yml | 15 +++-- terraform/README.md | 10 ++-- terraform/modules/thermograph-host/main.tf | 13 +++++ terraform/terraform.tfvars.example | 4 +- terraform/variables.tf | 5 +- 8 files changed, 115 insertions(+), 44 deletions(-) create mode 100755 deploy/db/init/20-tuning.sh delete mode 100644 deploy/db/init/20-tuning.sql diff --git a/deploy/db/init/20-tuning.sh b/deploy/db/init/20-tuning.sh new file mode 100755 index 0000000..0ed1748 --- /dev/null +++ b/deploy/db/init/20-tuning.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Postgres memory / performance tuning, scaled to the container's DB_MEMORY budget so +# the same init serves every host (beta 8g; prod 16g on the 48 GB box) with no +# hardcoding. Runs once on a fresh data volume from /docker-entrypoint-initdb.d, after +# 10-parquet.sql enables pg_duckdb. Settings are written via ALTER SYSTEM (persisted to +# postgresql.auto.conf); the container's post-init restart brings restart-only settings +# (shared_buffers, …) into effect. Init scripts do NOT re-run on an existing volume — to +# re-tune later, set DB_MEMORY and run this by hand, then restart: +# docker compose exec -e DB_MEMORY=16g db bash /docker-entrypoint-initdb.d/20-tuning.sh +# docker compose restart db +set -euo pipefail + +# Parse DB_MEMORY ("16g" / "8192m" / plain MB) into whole MB; default + floor at 8 GB. +budget="${DB_MEMORY:-8g}" +num="${budget//[!0-9]/}" +num="${num:-8}" +unit="$(printf '%s' "$budget" | tr -dc '[:alpha:]' | tr '[:upper:]' '[:lower:]')" +case "$unit" in + g | gb) mb=$((num * 1024)) ;; + m | mb | "") mb="$num" ;; + *) mb=8192 ;; +esac +if [ "$mb" -lt 1024 ]; then mb=8192; fi + +# Derive settings from the budget. The ratios reproduce the historical 8 GB tuning +# (shared_buffers 2 GB, effective_cache_size 6 GB, duckdb 4 GB, work_mem 64 MB, +# maintenance_work_mem 512 MB) and scale linearly on a bigger box. +shared_buffers=$((mb / 4)) # 25% — the shared page cache +effective_cache=$((mb * 3 / 4)) # 75% — planner's view of total cache (PG + OS) +duckdb_mem=$((mb / 2)) # 50% — pg_duckdb ceiling for parquet processing +work_mem=$((mb / 128)) # ~64 MB at 8 GB (per-operation; kept modest) +if [ "$work_mem" -lt 16 ]; then work_mem=16; fi +maint_mem=$((mb / 16)) # 512 MB at 8 GB — index builds / VACUUM + +echo "[tuning] DB_MEMORY=${budget} -> ${mb}MB: shared_buffers=${shared_buffers}MB" \ + "effective_cache_size=${effective_cache}MB work_mem=${work_mem}MB" \ + "maintenance_work_mem=${maint_mem}MB duckdb.max_memory=${duckdb_mem}MB" + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" </dev/null 2>&1 || true + sudo systemctl daemon-reload + fi echo "[${var.name}] openmeteo: disabled" exit 0 fi @@ -214,6 +220,13 @@ resource "null_resource" "host" { sudo systemctl status rclone-om --no-pager || true exit 1 fi + # Order Docker after the mount on every boot, so the restart-policy containers + # never bind an empty mount point. rclone-om is Type=notify, so `After` waits + # until the mount is actually ready — not merely that the unit was launched. + sudo install -d -m 0755 /etc/systemd/system/docker.service.d + printf '[Unit]\nWants=rclone-om.service\nAfter=rclone-om.service\n' \ + | sudo tee /etc/systemd/system/docker.service.d/10-wait-rclone.conf >/dev/null + sudo systemctl daemon-reload echo "[${var.name}] openmeteo: mounted at ${var.om_data_dir}" EOT ] diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example index 84a13ec..f430468 100644 --- a/terraform/terraform.tfvars.example +++ b/terraform/terraform.tfvars.example @@ -19,8 +19,8 @@ hosts = { domain = "thermograph.org" # Caddy TLS in front, app on loopback compose_files = ["docker-compose.yml"] app_dir = "/opt/thermograph" - # Sized up for the big box — tune freely. Also raise the Postgres internal budget - # in deploy/db/init/20-tuning.sql (shared_buffers etc.) to actually use the RAM. + # Sized up for the big box — tune freely. The Postgres internal budget scales from + # db_memory automatically (deploy/db/init/20-tuning.sh); no separate tuning edit. workers = 8 app_cpus = 8 db_cpus = 4 diff --git a/terraform/variables.tf b/terraform/variables.tf index 2258203..a468681 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -13,8 +13,9 @@ # # Resource sizing (workers / app_cpus / db_cpus / db_memory) defaults to the historical # 4 / 4 / 2 / 8g budget (right for beta). The prod box is 48 GB / 12 cores — raise these -# there (the example uses app_cpus 8, db_cpus 4, db_memory "16g"); also raise the -# Postgres *internal* budget in deploy/db/init/20-tuning.sql to actually exploit the RAM. +# there (the example uses app_cpus 8, db_cpus 4, db_memory "16g"). The Postgres internal +# budget scales from db_memory automatically (deploy/db/init/20-tuning.sh), so no +# separate tuning edit is needed to exploit the RAM. variable "hosts" { description = "Map of hosts to manage, keyed by a short name (e.g. \"prod\", \"dev\")." type = map(object({