141 lines
5.0 KiB
Markdown
141 lines
5.0 KiB
Markdown
|
|
# 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://<server-ip>/health
|
||
|
|
```
|
||
|
|
|
||
|
|
Expect `200`. A placeholder page is served until the first real deploy.
|
||
|
|
|
||
|
|
### 3.3 Capture the host key
|
||
|
|
|
||
|
|
```bash
|
||
|
|
ssh-keyscan -H <server-ip>
|
||
|
|
```
|
||
|
|
|
||
|
|
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 <server-ip>` |
|
||
|
|
|
||
|
|
### 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@<server-ip>
|
||
|
|
ls -1dt /srv/marketplaces/releases/*/ # newest first
|
||
|
|
ln -sfnT /srv/marketplaces/releases/<sha> /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://<host>/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.
|