diff --git a/deploy.sh b/deploy.sh index c2eac79..be95042 100755 --- a/deploy.sh +++ b/deploy.sh @@ -8,11 +8,17 @@ set -euo pipefail cd /opt/performancewest -SERVICES="${@:-site api workers proxy-relay}" +SERVICES="${@:-site api workers proxy-relay listmonk-hc}" -# proxy-relay is an upstream image (no build context). Build everything else, -# but always include it in the `up` set so the healthcare proxy sidecar runs. -BUILD_SERVICES="$(echo "$SERVICES" | tr ' ' '\n' | grep -v '^proxy-relay$' | tr '\n' ' ')" +# proxy-relay and listmonk-hc are upstream images (no build context). Build +# everything else, but always include them in the `up` set so the healthcare +# proxy sidecar and the healthcare-stream Listmonk run. +# NB: listmonk-hc needs a one-time DB setup the first time it is deployed: +# docker compose exec api-postgres psql -U pw -d postgres -c 'CREATE DATABASE listmonk_hc OWNER pw;' +# docker compose run --rm --entrypoint /bin/sh listmonk-hc -c './listmonk --install --idempotent --yes --config /listmonk/config.toml' +# then configure its 3 SMTP servers (hc ports 2526/2527/2528). See +# docs/healthcare-email-stream-plan.md. +BUILD_SERVICES="$(echo "$SERVICES" | tr ' ' '\n' | grep -vE '^(proxy-relay|listmonk-hc)$' | tr '\n' ' ')" echo "=== Pulling latest from git ===" git pull origin main diff --git a/docker-compose.dev.override.yml b/docker-compose.dev.override.yml new file mode 100644 index 0000000..4daf9f9 --- /dev/null +++ b/docker-compose.dev.override.yml @@ -0,0 +1,17 @@ +# Dev-only overrides. Dev shares the host with prod and historically used its +# own postgres data volume (dev-pgdata) and no host-port bindings. The canonical +# (prod) compose uses api-pgdata + host ports; pin dev back to its own data and +# drop the clashing host ports. !override replaces base lists. +services: + api-postgres: + ports: !override [] + volumes: !override + - dev-pgdata:/var/lib/postgresql/data + listmonk: + ports: !override [] + listmonk-hc: + ports: !override [] +volumes: + dev-pgdata: + external: true + name: performancewest-dev_dev-pgdata diff --git a/infra/postfix/pw-hc-rampcap.sh b/infra/postfix/pw-hc-rampcap.sh new file mode 100644 index 0000000..cff737b --- /dev/null +++ b/infra/postfix/pw-hc-rampcap.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Ramp the listmonk-hc hourly send cap in lockstep with the healthcare IP warmup. +# +# The HEALTHCARE HOT stream sends from fresh dedicated IPs (.107/.108/.109), so +# even though institutional B2B mail tolerates far more volume than consumer +# cold mail, we still warm these IPs before hitting the 10k/day ceiling. This is +# the hc analogue of /usr/local/bin/pw-listmonk-rampcap, driven off a SEPARATE +# warmup stamp (/etc/postfix/hc-warmup-start) and writing a SEPARATE Listmonk +# DB (listmonk_hc), so the trucking ramp/cap and the healthcare ramp/cap are +# fully independent. +# +# Steady-state target (institutional, 3 IPs): +# day 0-1 : ~1,000/day -> 100/h +# day 2-4 : ~3,000/day -> 300/h +# day 5-9 : ~6,000/day -> 600/h +# day 10+ : ~10,000/day -> 1000/h (the chosen ceiling) +set -euo pipefail + +STATE=/etc/postfix/hc-warmup-start +COMPOSE_DIR=/opt/performancewest +DB=listmonk_hc +PGPASSWORD=${DB_PASSWORD:-pw_dev_2026} + +[ -f "$STATE" ] || { echo "no hc warmup stamp ($STATE); run hc_stream_setup.sh first"; exit 1; } +START=$(cat "$STATE"); NOW=$(date +%s); DAYS=$(( (NOW - START) / 86400 )) + +if [ "$DAYS" -le 1 ]; then RATE=100 +elif [ "$DAYS" -le 4 ]; then RATE=300 +elif [ "$DAYS" -le 9 ]; then RATE=600 +else RATE=1000; fi + +cd "$COMPOSE_DIR" +psql() { PGPASSWORD=$PGPASSWORD docker compose exec -T -e PGPASSWORD=$PGPASSWORD api-postgres \ + psql -U pw -d "$DB" -tAc "$1"; } + +CUR=$(psql "SELECT value FROM settings WHERE key='app.message_sliding_window_rate';" 2>/dev/null || echo "") +if [ "$CUR" != "$RATE" ]; then + psql "UPDATE settings SET value='$RATE' WHERE key='app.message_sliding_window_rate'; + UPDATE settings SET value='\"1h\"' WHERE key='app.message_sliding_window_duration'; + UPDATE settings SET value='true' WHERE key='app.message_sliding_window';" >/dev/null + docker compose restart listmonk-hc >/dev/null 2>&1 || true + logger -t pw-hc-rampcap "day $DAYS -> listmonk-hc cap ${RATE}/h (was ${CUR}/h)" + echo "$(date '+%F %T') hc-rampcap: day=$DAYS cap=${RATE}/h (changed from ${CUR}/h, listmonk-hc restarted)" +else + echo "$(date '+%F %T') hc-rampcap: day=$DAYS cap=${RATE}/h (no change)" +fi diff --git a/scripts/deploy-dev.sh b/scripts/deploy-dev.sh index 77bea38..11f074a 100755 --- a/scripts/deploy-dev.sh +++ b/scripts/deploy-dev.sh @@ -60,12 +60,20 @@ done # and not overwritten. rsync -avz -e "$SSH" docker-compose.yml "$REMOTE:$DEV_DIR/docker-compose.yml" +# Dev-only compose override: dev shares the host with prod, so it must NOT bind +# the prod host ports (5432/9100/9101/...) and must use its own postgres data +# volume (dev-pgdata). Shipped as docker-compose.override.yml (compose auto-loads +# it). Without this, syncing the canonical compose makes dev's api-postgres try +# to rebind prod's :5432 and switch volumes -> stack breakage. See +# docs/healthcare-email-stream-plan.md. +rsync -avz -e "$SSH" docker-compose.dev.override.yml "$REMOTE:$DEV_DIR/docker-compose.override.yml" + echo "" echo "=== Rebuilding containers ===" # proxy-relay is an upstream image (no build context); `up --build` skips the # build for it but still (re)creates it from the compose definition. -$SSH "$REMOTE" "cd $DEV_DIR && sudo docker compose up -d --build api site workers proxy-relay" +$SSH "$REMOTE" "cd $DEV_DIR && sudo docker compose up -d --build api site workers proxy-relay listmonk-hc" echo "" echo "=== Deploy complete ==="