# Deployment — server provisioning, CD, TLS Frontend only. The backend service (`:8080`) is a separate developer's responsibility; nginx already proxies `/api/` to it and will `502` until it exists. **Multi-tenant, one bundle.** Every customer domain is served by the same build. The SPA resolves its tenant from the `Host` header ([BACKEND-HANDOFF §1a](backend/BACKEND-HANDOFF.md)). One deploy updates every domain simultaneously — there is no per-tenant build and no per-tenant deploy. --- ## 1. Files | Path | Purpose | |---|---| | `scripts/deploy/server-setup.sh` | One-time server provisioning. Idempotent. Run as root. | | `scripts/deploy/add-domain.sh` | Attach one domain + issue TLS. Run per domain, as root, after DNS resolves. | | `.github/workflows/deploy.yml` | CD: build → upload → atomic swap → verify. Triggers on push to `main`. | --- ## 2. Layout on the server ``` /srv/marketplaces/ ├── releases/ │ ├── a1b2c3d4e5f6/frontend/ <- one directory per deployed commit │ └── ... (last 5 kept) └── current -> releases/a1b2c3d4e5f6 ``` nginx root is `/srv/marketplaces/current/frontend`. Activation is a symlink swap, so no request is ever served from a half-written directory, and a rollback is a symlink change rather than a rebuild. --- ## 3. First-time setup ### 3.1 Generate a CI deploy key On your machine, **not** on the server: ```bash ssh-keygen -t ed25519 -C "ci@marketplaces" -f ./marketplaces_deploy -N "" ``` Two files result. `marketplaces_deploy.pub` goes to the server; `marketplaces_deploy` (private) goes into CI secrets and nowhere else. ### 3.2 Provision the server Copy `scripts/deploy/` to the server and run: ```bash sudo bash server-setup.sh --pubkey "$(cat marketplaces_deploy.pub)" ``` This installs nginx + certbot, creates a **key-only** `deploy` user with no password, writes the catch-all nginx config, opens 80/443/OpenSSH in ufw, and grants `deploy` exactly one sudo right: `systemctl reload nginx`. Verify before continuing: ```bash curl -I http:///health ``` Expect `200`. A placeholder page is served until the first real deploy. ### 3.3 Capture the host key ```bash ssh-keyscan -H ``` The output is the `DEPLOY_KNOWN_HOSTS` secret. Pinning it means a rebuilt or impersonated server fails the deploy instead of being trusted silently. ### 3.4 Add CI secrets | Secret | Value | |---|---| | `DEPLOY_HOST` | server IP or hostname | | `DEPLOY_USER` | `deploy` | | `DEPLOY_SSH_KEY` | contents of the **private** key file | | `DEPLOY_KNOWN_HOSTS` | output of `ssh-keyscan -H ` | ### 3.5 Deploy Push to `main`, or run the workflow manually with a ref. The workflow refuses to swap the symlink unless the uploaded release contains an `index.html`, so a failed upload leaves the previous release serving. --- ## 4. TLS No certificate is issued during provisioning, because certbot cannot validate a domain that does not yet point at the server. Per domain, once its A record resolves here: ```bash sudo bash add-domain.sh shop.example.com --email ops@example.com --with-www ``` This writes a server block for that domain (same root — it exists only to give certbot a concrete `server_name`), issues the certificate, enables the HTTP→HTTPS redirect, and enables `certbot.timer` for renewal. Verify: ```bash curl -I https://shop.example.com/health sudo certbot certificates ``` **On wildcards:** `add-domain.sh` uses HTTP-01, which cannot issue wildcards. If tenants all live under one apex (`*.marketplaces.example.com`), a DNS-01 wildcard is fewer moving parts — but it requires API credentials for the DNS provider and a different certbot plugin. Not set up here; raise it when the tenant count makes per-domain issuance annoying. --- ## 5. Rollback ```bash ssh deploy@ ls -1dt /srv/marketplaces/releases/*/ # newest first ln -sfnT /srv/marketplaces/releases/ /srv/marketplaces/current.new mv -Tf /srv/marketplaces/current.new /srv/marketplaces/current sudo systemctl reload nginx ``` Only the last 5 releases are retained. Older ones need a rebuild from the tag. --- ## 6. Operational checks ```bash curl -I http:///health # 200 from nginx readlink -f /srv/marketplaces/current # which commit is live sudo nginx -t # config valid systemctl status nginx certbot.timer # both active sudo tail -f /var/log/nginx/marketplaces.error.log ``` --- ## 7. Known limits - **`/api/` 502s until the backend runs.** Expected. nginx proxies to `127.0.0.1:8080`; nothing listens there yet. - **No staging environment.** `main` goes straight to production. Adding one means a second server plus a `staging` branch trigger. - **No smoke test beyond HTTP 200.** The verify step confirms nginx serves the shell, not that the app boots. A real check needs the E2E harness from Track Q. - **Caching.** `index.html` is `no-store`; hashed assets are `immutable` for a year. A deploy therefore takes effect on the next page load, with no cache purge.