Some checks failed
Architecture Governance / architecture (push) Failing after 6m16s
Reconcile TLS, exact CORS, and backend proxying before release activation so every storefront uses its derived API host.
203 lines
7.3 KiB
Bash
Executable File
203 lines
7.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# One-time server provisioning for the marketplaces frontend.
|
|
# Idempotent: safe to re-run. Run as root on the target server.
|
|
#
|
|
# bash server-setup.sh --pubkey "ssh-ed25519 AAAA... ci@marketplaces"
|
|
#
|
|
# What it does NOT do: issue TLS certificates (no domain points here yet).
|
|
# Run add-domain.sh per domain once DNS resolves. See docs/DEPLOYMENT.md.
|
|
|
|
set -euo pipefail
|
|
|
|
DEPLOY_USER="deploy"
|
|
BASE="/srv/marketplaces"
|
|
PUBKEY=""
|
|
KEEP_RELEASES=5
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--pubkey) PUBKEY="$2"; shift 2 ;;
|
|
--user) DEPLOY_USER="$2"; shift 2 ;;
|
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
[[ $EUID -eq 0 ]] || { echo "must run as root" >&2; exit 1; }
|
|
[[ -n "$PUBKEY" ]] || { echo "--pubkey is required (the CI deploy key's PUBLIC half)" >&2; exit 1; }
|
|
|
|
echo "==> packages"
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
apt-get install -y -qq nginx certbot python3-certbot-nginx rsync ufw jq curl openssl
|
|
|
|
echo "==> deploy user: $DEPLOY_USER"
|
|
if ! id -u "$DEPLOY_USER" >/dev/null 2>&1; then
|
|
# No password is ever set: this account is key-only by construction.
|
|
adduser --system --group --shell /bin/bash --home "/home/$DEPLOY_USER" "$DEPLOY_USER"
|
|
fi
|
|
install -d -m 700 -o "$DEPLOY_USER" -g "$DEPLOY_USER" "/home/$DEPLOY_USER/.ssh"
|
|
AUTH="/home/$DEPLOY_USER/.ssh/authorized_keys"
|
|
touch "$AUTH"
|
|
grep -qxF "$PUBKEY" "$AUTH" || echo "$PUBKEY" >> "$AUTH"
|
|
chown "$DEPLOY_USER:$DEPLOY_USER" "$AUTH"
|
|
chmod 600 "$AUTH"
|
|
|
|
echo "==> directories"
|
|
install -d -m 755 -o "$DEPLOY_USER" -g "$DEPLOY_USER" "$BASE" "$BASE/releases"
|
|
# First deploy creates $BASE/current as a symlink into releases/.
|
|
# Seed a placeholder so nginx starts cleanly before anything is deployed.
|
|
if [[ ! -e "$BASE/current" ]]; then
|
|
install -d -m 755 -o "$DEPLOY_USER" -g "$DEPLOY_USER" "$BASE/releases/bootstrap/frontend"
|
|
echo "<!doctype html><title>marketplaces</title><p>Not deployed yet." \
|
|
> "$BASE/releases/bootstrap/frontend/index.html"
|
|
chown -R "$DEPLOY_USER:$DEPLOY_USER" "$BASE/releases/bootstrap"
|
|
ln -sfn "$BASE/releases/bootstrap" "$BASE/current"
|
|
chown -h "$DEPLOY_USER:$DEPLOY_USER" "$BASE/current"
|
|
fi
|
|
|
|
echo "==> nginx catch-all (multi-tenant: one bundle serves every domain)"
|
|
cat > /etc/nginx/sites-available/marketplaces.conf <<'NGINX'
|
|
# Multi-tenant by design: the SPA derives its tenant from the Host header,
|
|
# so ONE server block serves every customer domain. Do not add a per-tenant
|
|
# root here. Per-domain server blocks exist only to hold TLS certificates
|
|
# (see add-domain.sh) and proxy to this same root.
|
|
|
|
server {
|
|
listen 80 default_server;
|
|
listen [::]:80 default_server;
|
|
server_name _;
|
|
|
|
root /srv/marketplaces/current/frontend;
|
|
index index.html;
|
|
|
|
access_log /var/log/nginx/marketplaces.access.log;
|
|
error_log /var/log/nginx/marketplaces.error.log;
|
|
|
|
# Do not let the browser cache the app shell: a deploy must take effect
|
|
# on the next reload, not whenever a stale index.html expires.
|
|
location = /index.html {
|
|
add_header Cache-Control "no-store, must-revalidate" always;
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# Hashed build artifacts are immutable by construction.
|
|
location ~* \.(js|css|woff2?|png|jpe?g|svg|gif|webp|avif|ico)$ {
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable" always;
|
|
try_files $uri =404;
|
|
}
|
|
|
|
location /health {
|
|
access_log off;
|
|
return 200 "ok\n";
|
|
add_header Content-Type text/plain;
|
|
}
|
|
|
|
location /api/ {
|
|
proxy_pass http://127.0.0.1:8080;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 60s;
|
|
}
|
|
|
|
# SPA fallback. Must stay last: every unmatched path is a client route.
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
|
|
gzip on;
|
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
|
gzip_min_length 1024;
|
|
}
|
|
NGINX
|
|
|
|
ln -sfn /etc/nginx/sites-available/marketplaces.conf /etc/nginx/sites-enabled/marketplaces.conf
|
|
rm -f /etc/nginx/sites-enabled/default
|
|
|
|
echo "==> firewall"
|
|
ufw allow OpenSSH >/dev/null
|
|
ufw allow 80/tcp >/dev/null
|
|
ufw allow 443/tcp >/dev/null
|
|
ufw --force enable >/dev/null
|
|
|
|
echo "==> nginx config test"
|
|
nginx -t
|
|
systemctl enable --now nginx
|
|
systemctl reload nginx
|
|
|
|
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
echo "==> tenant API-domain configurator"
|
|
if [[ -f "$SRC_DIR/configure-api-domain.sh" ]]; then
|
|
install -m 755 -o root -g root "$SRC_DIR/configure-api-domain.sh" \
|
|
/usr/local/sbin/marketplaces-configure-api-domain
|
|
else
|
|
echo "configure-api-domain.sh not found next to server-setup.sh" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> dynamic domain reconciler"
|
|
install -d -m 755 "$BASE/bin" /etc/marketplaces "/var/lib/marketplaces"
|
|
if [[ -f "$SRC_DIR/sync-domains.sh" ]]; then
|
|
install -m 755 "$SRC_DIR/sync-domains.sh" "$BASE/bin/sync-domains.sh"
|
|
|
|
if [[ ! -f /etc/marketplaces/domains.env ]]; then
|
|
cat > /etc/marketplaces/domains.env <<'ENVFILE'
|
|
# Where the desired domain list comes from.
|
|
# file:/etc/marketplaces/domains.txt one hostname per line
|
|
# https://api.example.com/api/admin/v2/domains JSON, once the backend exists
|
|
DOMAINS_SOURCE=file:/etc/marketplaces/domains.txt
|
|
|
|
# Required: certbot expiry notices.
|
|
CERTBOT_EMAIL=
|
|
|
|
# Cap per run so a bad source cannot burn the weekly ACME budget in one pass.
|
|
MAX_ISSUE_PER_RUN=10
|
|
|
|
# Set by setup-wildcard-tls.sh. Subdomains of this apex skip per-domain issuance.
|
|
#WILDCARD_APEX=
|
|
ENVFILE
|
|
chmod 600 /etc/marketplaces/domains.env
|
|
fi
|
|
touch /etc/marketplaces/domains.txt
|
|
|
|
if [[ -d "$SRC_DIR/systemd" ]]; then
|
|
install -m 644 "$SRC_DIR/systemd/marketplaces-domains.service" /etc/systemd/system/
|
|
install -m 644 "$SRC_DIR/systemd/marketplaces-domains.timer" /etc/systemd/system/
|
|
systemctl daemon-reload
|
|
# Not started yet: CERTBOT_EMAIL is still blank. Enable it after filling in
|
|
# /etc/marketplaces/domains.env, or the first run just fails on every tick.
|
|
echo " timer installed but NOT started - set CERTBOT_EMAIL first, then:"
|
|
echo " systemctl enable --now marketplaces-domains.timer"
|
|
fi
|
|
else
|
|
echo " sync-domains.sh not found next to this script - skipping"
|
|
fi
|
|
|
|
echo "==> sudoers: deployment reload plus validated tenant API provisioning"
|
|
cat > /etc/sudoers.d/marketplaces-deploy <<SUDO
|
|
Cmnd_Alias MARKETPLACES_DEPLOY = /bin/systemctl reload nginx, /usr/local/sbin/marketplaces-configure-api-domain *
|
|
$DEPLOY_USER ALL=(root) NOPASSWD: MARKETPLACES_DEPLOY
|
|
SUDO
|
|
chmod 440 /etc/sudoers.d/marketplaces-deploy
|
|
visudo -c -f /etc/sudoers.d/marketplaces-deploy
|
|
|
|
echo
|
|
echo "done."
|
|
echo " deploy user : $DEPLOY_USER (key-only, no password)"
|
|
echo " web root : $BASE/current/frontend"
|
|
echo " keep : last $KEEP_RELEASES releases"
|
|
echo
|
|
echo "next:"
|
|
echo " 1. curl -I http://<this-server>/health -> expect 200"
|
|
echo " 2. point a domain's A record here"
|
|
echo " 3. bash add-domain.sh <domain> -> issues TLS"
|
|
echo " 4. add CI secrets, push to main -> first real deploy"
|