This commit is contained in:
2026-05-19 23:36:58 +12:00
parent 5172588488
commit a7f8a619b1
68 changed files with 4486 additions and 1430 deletions
+151 -16
View File
@@ -35,6 +35,108 @@ fail() {
exit 1
}
get_mount_source_for_destination() {
local container_id="$1"
local destination="$2"
docker inspect -f '{{range .Mounts}}{{.Source}}|{{.Destination}}{{println}}{{end}}' "$container_id" \
| awk -F'|' -v wanted="$destination" '$2 == wanted { print $1; exit }'
}
write_acme_bootstrap_config() {
local source_config="$1"
local target_config="$2"
shift 2
awk -v skip_domains="$*" '
BEGIN {
split(skip_domains, items, " ")
for (i in items) {
if (items[i] != "") skip[items[i]] = 1
}
in_server = 0
depth = 0
block = ""
is_ssl = 0
has_skipped_domain = 0
}
function emit_block() {
if (!(is_ssl && has_skipped_domain)) {
printf "%s", block
}
in_server = 0
depth = 0
block = ""
is_ssl = 0
has_skipped_domain = 0
}
{
line = $0 ORS
if (!in_server) {
if ($0 ~ /^[[:space:]]*server[[:space:]]*\{[[:space:]]*$/) {
in_server = 1
depth = 1
block = line
next
}
printf "%s", line
next
}
block = block line
if ($0 ~ /^[[:space:]]*listen[[:space:]]+443[[:space:]]+ssl([[:space:]]|;)/) {
is_ssl = 1
}
if ($0 ~ /^[[:space:]]*server_name[[:space:]]+/) {
count = split($0, parts, /[[:space:];]+/)
for (i = 2; i <= count; i++) {
if (parts[i] in skip) {
has_skipped_domain = 1
}
}
}
opens = gsub(/\{/, "{", $0)
closes = gsub(/\}/, "}", $0)
depth += opens - closes
if (depth == 0) {
emit_block()
}
}
END {
if (in_server) {
emit_block()
}
}
' "$source_config" > "$target_config"
}
obtain_certificate() {
local domain="$1"
local cert_root="$2"
local acme_webroot="$3"
local certbot_email="info@goodwalk.co.nz"
echo "[deploy-remote] Obtaining TLS certificate for $domain"
docker run --rm \
-v "$cert_root:/etc/letsencrypt" \
-v "$acme_webroot:/var/www/certbot" \
certbot/certbot:latest \
certonly \
--webroot \
-w /var/www/certbot \
--cert-name "$domain" \
-d "$domain" \
--non-interactive \
--agree-tos \
-m "$certbot_email"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--archive)
@@ -292,22 +394,6 @@ if (( nginx_args_present )); then
[[ -f "$DEPLOY_PATH/$NGINX_SOURCE" ]] || fail "Nginx config missing from deployment payload: $DEPLOY_PATH/$NGINX_SOURCE"
[[ -f "$NGINX_COMPOSE_FILE" ]] || fail "Nginx compose file was not found on the server: $NGINX_COMPOSE_FILE"
# Pre-flight: SSL certificate for onboarding.goodwalk.co.nz must exist before
# nginx can reload — the config references this cert path directly.
ONBOARDING_CERT="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/fullchain.pem"
ONBOARDING_KEY="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/privkey.pem"
if [[ ! -f "$ONBOARDING_CERT" || ! -f "$ONBOARDING_KEY" ]]; then
fail "SSL certificate for onboarding.goodwalk.co.nz is not present on this server.
Expected: $ONBOARDING_CERT
One-time setup on the droplet:
1. Ensure the DNS A record for onboarding.goodwalk.co.nz points to this server's IP
2. Bring nginx up so the ACME webroot is reachable, then obtain the certificate:
certbot certonly --webroot -w /var/www/certbot \\
-d onboarding.goodwalk.co.nz \\
--non-interactive --agree-tos -m info@goodwalk.co.nz
3. Re-run this deploy script"
fi
MAINTENANCE_HTML_SRC="$DEPLOY_PATH/nginx/maintenance.html"
MAINTENANCE_LOGO_SRC="$DEPLOY_PATH/nginx/logo.png"
[[ -f "$MAINTENANCE_HTML_SRC" ]] || fail "Maintenance page missing from deployment payload: $MAINTENANCE_HTML_SRC"
@@ -320,6 +406,55 @@ if (( nginx_args_present )); then
NGINX_CID="$(docker ps -qf name=^nginx$ | head -n1 || true)"
[[ -n "$NGINX_CID" ]] || fail "Shared nginx container is not running (expected name 'nginx'). Bring it up before deploying."
CERT_ROOT_HOST_DIR="$(get_mount_source_for_destination "$NGINX_CID" "/etc/letsencrypt")"
[[ -n "$CERT_ROOT_HOST_DIR" ]] || fail "nginx container is missing the certificate bind mount for /etc/letsencrypt."
CERTBOT_WEBROOT_HOST_DIR="$(get_mount_source_for_destination "$NGINX_CID" "/var/www/certbot")"
[[ -n "$CERTBOT_WEBROOT_HOST_DIR" ]] || fail "nginx container is missing the ACME webroot bind mount for /var/www/certbot."
ONBOARDING_CERT="$CERT_ROOT_HOST_DIR/live/onboarding.goodwalk.co.nz/fullchain.pem"
ONBOARDING_KEY="$CERT_ROOT_HOST_DIR/live/onboarding.goodwalk.co.nz/privkey.pem"
CLIENTS_CERT="$CERT_ROOT_HOST_DIR/live/clients.goodwalk.co.nz/fullchain.pem"
CLIENTS_KEY="$CERT_ROOT_HOST_DIR/live/clients.goodwalk.co.nz/privkey.pem"
CP_CERT="$CERT_ROOT_HOST_DIR/live/cp.goodwalk.co.nz/fullchain.pem"
CP_KEY="$CERT_ROOT_HOST_DIR/live/cp.goodwalk.co.nz/privkey.pem"
MISSING_CERT_DOMAINS=()
if [[ ! -f "$ONBOARDING_CERT" || ! -f "$ONBOARDING_KEY" ]]; then
MISSING_CERT_DOMAINS+=("onboarding.goodwalk.co.nz")
fi
if [[ ! -f "$CLIENTS_CERT" || ! -f "$CLIENTS_KEY" ]]; then
MISSING_CERT_DOMAINS+=("clients.goodwalk.co.nz")
fi
if [[ ! -f "$CP_CERT" || ! -f "$CP_KEY" ]]; then
MISSING_CERT_DOMAINS+=("cp.goodwalk.co.nz")
fi
if (( ${#MISSING_CERT_DOMAINS[@]} > 0 )); then
echo "[deploy-remote] Missing TLS certificates detected: ${MISSING_CERT_DOMAINS[*]}"
echo "[deploy-remote] Bootstrapping nginx HTTP config so ACME challenges can be served"
mkdir -p "$(dirname "$NGINX_TARGET")"
BOOTSTRAP_CONFIG="$(mktemp "${TMPDIR:-/tmp}/goodwalk-nginx-acme.XXXXXX.conf")"
write_acme_bootstrap_config "$DEPLOY_PATH/$NGINX_SOURCE" "$BOOTSTRAP_CONFIG" "${MISSING_CERT_DOMAINS[@]}"
cp "$BOOTSTRAP_CONFIG" "$NGINX_TARGET"
rm -f "$BOOTSTRAP_CONFIG"
echo "[deploy-remote] Validating bootstrap nginx configuration"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -t
echo "[deploy-remote] Reloading shared nginx with bootstrap config"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -s reload
for domain in "${MISSING_CERT_DOMAINS[@]}"; do
echo "[deploy-remote] Ensure the DNS A record for $domain points to this server before certificate issuance"
obtain_certificate "$domain" "$CERT_ROOT_HOST_DIR" "$CERTBOT_WEBROOT_HOST_DIR" \
|| fail "Automatic certificate issuance failed for $domain. Confirm DNS resolves here and port 80 is reachable."
done
[[ -f "$ONBOARDING_CERT" && -f "$ONBOARDING_KEY" ]] || fail "Automatic certificate issuance did not create onboarding.goodwalk.co.nz at $ONBOARDING_CERT"
[[ -f "$CLIENTS_CERT" && -f "$CLIENTS_KEY" ]] || fail "Automatic certificate issuance did not create clients.goodwalk.co.nz at $CLIENTS_CERT"
[[ -f "$CP_CERT" && -f "$CP_KEY" ]] || fail "Automatic certificate issuance did not create cp.goodwalk.co.nz at $CP_CERT"
fi
if ! docker inspect -f '{{range .Mounts}}{{.Source}}|{{.Destination}}{{println}}{{end}}' "$NGINX_CID" \
| grep -Fxq "${MAINTENANCE_HOST_DIR}|/var/www/maintenance"; then
fail "nginx container is missing the maintenance bind mount.