Files
ladill-link/deploy/server-bootstrap.sh
T
isaaccladandClaude 07c2769301
Deploy Ladill Link / deploy (push) Successful in 59s
link: version the ladl.link cache and log rotation
Both were applied by hand during an incident and existed only in /etc on one
server. server-bootstrap.sh generates the ladl.link vhost, and its version had no
caching at all — so rebuilding the box would have silently restored a ~44 req/s
ceiling on a link being handed to a very large audience, with nothing in git
recording that the cache should exist. Laravel log rotation had the same problem:
none was configured anywhere, and logs had reached 808M across apps.

Now tracked under deploy/ and installed by bootstrap:

  nginx/conf.d/ladill-link-cache.conf        FastCGI cache zone
  nginx/snippets/ladill-link-cached.conf     cached path, 10m
  nginx/snippets/ladill-link-cached-short.conf   same, 60s
  nginx/snippets/ladill-link-public-cache.conf   which routes are cached
  nginx/snippets/ladill-link-hot-pages.conf  per-campaign slugs
  logrotate/ladill-laravel

Campaign slugs live in their own snippet so a hot link can be added or retired
with an nginx reload instead of a deploy, and bootstrap seeds that file only when
absent so it is never clobbered.

Caching stays deliberately narrow. ladl.link also serves the admin app, and the
cached path strips Set-Cookie, so anything reaching it would lose its session.
Only /q/, */bookshop-* and listed campaign pages are cached; SSO and dashboard
fall through untouched — verified after this change: both still set cookies and
carry no cache header.

Generated vhosts now include the snippet from the start, and an existing vhost is
patched in place (with a timestamped backup) rather than rewritten, since it may
carry local edits. Both paths are idempotent.

Applied to production as part of this change, so the committed config is the
config that is running rather than an untested transcription: page 12ms,
cover 14ms, vanity URL 11ms, all cache HIT.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-25 12:14:14 +00:00

191 lines
8.5 KiB
Bash

#!/usr/bin/env bash
# One-time server bootstrap for Ladill Link on 161.97.138.149
set -Eeuo pipefail
APP_ROOT="/var/www/ladill-link"
MONOLITH="/var/www/ladill.com/current"
QR_PLUS_ENV="/var/www/ladill-qr-plus/shared/.env"
NGINX_SETUP="/var/www/ladill.com/current/deployment/setup-service-subdomain-nginx.sh"
RELEASE_ARCHIVE="${1:-/tmp/ladill-link-release.tgz}"
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*"; }
[ -f "$RELEASE_ARCHIVE" ] || { echo "Missing release archive: $RELEASE_ARCHIVE" >&2; exit 1; }
[ -f "$QR_PLUS_ENV" ] || { echo "Missing qr-plus env template" >&2; exit 1; }
log "Creating app directories"
mkdir -p "$APP_ROOT"/{releases,shared}
chown -R deploy:www-data "$APP_ROOT" 2>/dev/null || true
if [ ! -f "$APP_ROOT/shared/.env" ]; then
log "Bootstrapping .env from qr-plus template"
cp "$QR_PLUS_ENV" "$APP_ROOT/shared/.env"
sed -i \
-e 's/^APP_NAME=.*/APP_NAME="Ladill Link"/' \
-e 's#^APP_URL=.*#APP_URL=https://link.ladill.com#' \
-e 's/^DB_DATABASE=.*/DB_DATABASE=ladill_link/' \
-e 's/^DB_USERNAME=.*/DB_USERNAME=ladill_link/' \
-e 's/^BILLING_API_KEY_QR=.*/BILLING_API_KEY_LINK=/' \
"$APP_ROOT/shared/.env"
# Generate DB password + billing key if missing
DB_PASS="$(openssl rand -hex 16)"
BILL_KEY="$(openssl rand -hex 24)"
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=${DB_PASS}/" "$APP_ROOT/shared/.env"
if ! grep -q '^BILLING_API_KEY_LINK=' "$APP_ROOT/shared/.env"; then
echo "BILLING_API_KEY_LINK=${BILL_KEY}" >> "$APP_ROOT/shared/.env"
else
sed -i "s/^BILLING_API_KEY_LINK=.*/BILLING_API_KEY_LINK=${BILL_KEY}/" "$APP_ROOT/shared/.env"
fi
grep -q '^LINK_PUBLIC_DOMAIN=' "$APP_ROOT/shared/.env" || echo 'LINK_PUBLIC_DOMAIN=ladl.link' >> "$APP_ROOT/shared/.env"
grep -q '^LINK_PRICE_PER_LINK_GHS=' "$APP_ROOT/shared/.env" || echo 'LINK_PRICE_PER_LINK_GHS=0.05' >> "$APP_ROOT/shared/.env"
grep -q '^IDENTITY_API_KEY_LINK=' "$APP_ROOT/shared/.env" || echo "IDENTITY_API_KEY_LINK=$(openssl rand -hex 24)" >> "$APP_ROOT/shared/.env"
chmod 640 "$APP_ROOT/shared/.env"
chown deploy:www-data "$APP_ROOT/shared/.env" 2>/dev/null || true
log "Creating MySQL database ladill_link"
mysql -e "CREATE DATABASE IF NOT EXISTS ladill_link CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -e "CREATE USER IF NOT EXISTS 'ladill_link'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';"
mysql -e "GRANT ALL PRIVILEGES ON ladill_link.* TO 'ladill_link'@'127.0.0.1';"
mysql -e "FLUSH PRIVILEGES;"
# Save keys for monolith wiring (picked up below)
echo "$BILL_KEY" > /root/.ladill-link-bootstrap-billing-key
echo "$DB_PASS" > /root/.ladill-link-bootstrap-db-pass
fi
log "Deploying release archive"
export LADILL_APP_ROOT="$APP_ROOT"
export LADILL_RELEASE_ARCHIVE="$RELEASE_ARCHIVE"
bash "$APP_ROOT/releases/bootstrap-deploy.sh" 2>/dev/null || true
# deploy.sh lives inside the archive — extract deploy helper first if needed
TMP_EXTRACT="/tmp/ladill-link-deploy-$$"
mkdir -p "$TMP_EXTRACT"
tar -xzf "$RELEASE_ARCHIVE" -C "$TMP_EXTRACT" deploy/deploy.sh
cp "$TMP_EXTRACT/deploy/deploy.sh" /tmp/ladill-link-deploy.sh
LADILL_APP_ROOT="$APP_ROOT" LADILL_RELEASE_ARCHIVE="$RELEASE_ARCHIVE" bash /tmp/ladill-link-deploy.sh
log "Configuring nginx for link.ladill.com"
if [ -x "$NGINX_SETUP" ]; then
bash "$NGINX_SETUP" link --app "$APP_ROOT/current"
else
bash /var/www/ladill.com/current/deployment/setup-service-subdomain-nginx.sh link --app "$APP_ROOT/current" 2>/dev/null || \
echo "WARN: run setup-service-subdomain-nginx.sh link manually"
fi
log "Installing nginx cache + logrotate config"
REPO_DEPLOY="$APP_ROOT/current/deploy"
if [ -d "$REPO_DEPLOY/nginx" ]; then
install -m 0644 "$REPO_DEPLOY/nginx/conf.d/"*.conf /etc/nginx/conf.d/
# Do not clobber hot-pages: it is edited in place between deploys to add or
# retire campaign links, so only seed it when absent.
for f in "$REPO_DEPLOY/nginx/snippets/"*.conf; do
base="$(basename "$f")"
if [ "$base" = "ladill-link-hot-pages.conf" ] && [ -f "/etc/nginx/snippets/$base" ]; then
log " keeping existing snippets/$base"
continue
fi
install -m 0644 "$f" "/etc/nginx/snippets/$base"
done
mkdir -p /var/cache/nginx/ladill-link
chown www-data:www-data /var/cache/nginx/ladill-link
fi
if [ -f "$REPO_DEPLOY/logrotate/ladill-laravel" ]; then
install -m 0644 "$REPO_DEPLOY/logrotate/ladill-laravel" /etc/logrotate.d/ladill-laravel
fi
log "Configuring nginx for ladl.link"
LADL_CONF="/etc/nginx/sites-available/ladl.link.conf"
if [ ! -f "$LADL_CONF" ]; then
cat > "$LADL_CONF" <<'NGINX'
server {
listen 80;
listen [::]:80;
server_name ladl.link www.ladl.link;
root /var/www/ladill-link/current/public;
location ^~ /.well-known/acme-challenge/ { allow all; }
include snippets/ladill-link-public-cache.conf;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm-ladill.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}
}
NGINX
ln -sf "$LADL_CONF" /etc/nginx/sites-enabled/ladl.link.conf
nginx -t && systemctl reload nginx
certbot certonly --webroot -w /var/www/ladill-link/current/public -d ladl.link -d www.ladl.link --non-interactive --agree-tos -m admin@ladill.com 2>/dev/null || \
certbot certonly --nginx -d ladl.link -d www.ladl.link --non-interactive --agree-tos -m admin@ladill.com 2>/dev/null || \
log "WARN: certbot for ladl.link failed — DNS may not point here yet"
if [ -d /etc/letsencrypt/live/ladl.link ]; then
cat > "$LADL_CONF" <<'NGINX'
server {
listen 80;
listen [::]:80;
server_name ladl.link www.ladl.link;
location ^~ /.well-known/acme-challenge/ { root /var/www/ladill-link/current/public; allow all; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ladl.link www.ladl.link;
client_max_body_size 64M;
ssl_certificate /etc/letsencrypt/live/ladl.link/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ladl.link/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/ladill-link/current/public;
index index.php;
include snippets/ladill-link-public-cache.conf;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm-ladill.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}
location ~ /\.(?!well-known).* { deny all; }
}
NGINX
nginx -t && systemctl reload nginx
fi
fi
log "Wiring monolith billing + link DB (if monolith has link registry)"
if [ -f "$MONOLITH/artisan" ]; then
BILL_KEY="$(cat /root/.ladill-link-bootstrap-billing-key 2>/dev/null || grep '^BILLING_API_KEY_LINK=' "$APP_ROOT/shared/.env" | cut -d= -f2-)"
MONO_ENV="/var/www/ladill.com/shared/.env"
if [ -n "$BILL_KEY" ] && [ -f "$MONO_ENV" ]; then
grep -q '^BILLING_API_KEY_LINK=' "$MONO_ENV" || echo "BILLING_API_KEY_LINK=${BILL_KEY}" >> "$MONO_ENV"
grep -q '^LINK_DB_DATABASE=' "$MONO_ENV" || cat >> "$MONO_ENV" <<EOF
LINK_DB_HOST=127.0.0.1
LINK_DB_DATABASE=ladill_link
LINK_DB_USERNAME=ladill_link
LINK_DB_PASSWORD=$(grep '^DB_PASSWORD=' "$APP_ROOT/shared/.env" | cut -d= -f2-)
LINK_PUBLIC_DOMAIN=ladl.link
EOF
(cd "$MONOLITH" && php artisan config:clear) || true
fi
if grep -q "'link'" "$MONOLITH/config/ladill_apps.php" 2>/dev/null; then
(cd "$MONOLITH" && php artisan ladill:onboard-app link --run-dns) || true
(cd "$MONOLITH" && php artisan dns:sync-subdomains) || true
else
log "Monolith missing link registry — deploy monolith code then re-run onboard"
fi
fi
log "Bootstrap complete"
curl -sI "https://link.ladill.com/up" 2>/dev/null | head -3 || curl -sI "http://link.ladill.com/up" 2>/dev/null | head -3 || true
# Existing servers were provisioned before the cache existed, and the vhost above
# is only written when absent. Patch the include in rather than rewriting a file
# that may carry local edits.
if [ -f "$LADL_CONF" ] && ! grep -q "ladill-link-public-cache.conf" "$LADL_CONF"; then
log "Adding cache include to existing ladl.link vhost"
cp "$LADL_CONF" "$LADL_CONF.bak-$(date +%Y%m%d-%H%M%S)"
sed -i 's|^\( *\)location / { try_files \$uri \$uri/ /index.php?\$query_string; }|\1include snippets/ladill-link-public-cache.conf;\n\1location / { try_files $uri $uri/ /index.php?$query_string; }|' "$LADL_CONF"
nginx -t && systemctl reload nginx || log "WARN: nginx test failed; restore from $LADL_CONF.bak-*"
fi