# Deployment — seo.northwestcar.group

Production runbook. Nothing here is "deploy blindly" — test each step.

## 1. Server requirements

- Ubuntu 22.04+ (or any x64 Linux), 2 vCPU / 4 GB RAM minimum.
- Node.js 22 LTS (only if running without Docker), Docker + Compose otherwise.
- Managed PostgreSQL 16 and managed Redis 7 are recommended over self-hosted.

## 2. DNS

Pick ONE (never both for the same host):

```
Type: A      Host: seo   Value: SERVER_PUBLIC_IP_ADDRESS      TTL: 3600
Type: CNAME  Host: seo   Value: HOSTNAME_PROVIDED_BY_HOST     TTL: 3600
```

## 3. Environment variables

Copy `.env.example`, then set production values (full list in
ENVIRONMENT.md). Critical:

```
NODE_ENV=production
APP_URL=https://seo.northwestcar.group
NEXT_PUBLIC_APP_URL=https://seo.northwestcar.group
COOKIE_DOMAIN=          # leave unset unless cross-subdomain SSO is required
DATABASE_URL=postgresql://…      # managed Postgres, sslmode=require
REDIS_URL=rediss://…             # TLS Redis
CREDENTIAL_ENCRYPTION_KEY=<64 hex chars, generated once, stored in a secret manager>
MAIL_FROM=seo@northwestcar.group
SMTP_URL=smtp://…
```

Never expose localhost or staging URLs in production email links — they are
generated from `APP_URL`, so verifying `APP_URL` verifies the links.

## 4–6. PostgreSQL, Redis, object storage

- Postgres: create db `northwest_seo`; least-privilege app user; enable
  automated backups + PITR on the managed service.
- Redis: password/TLS required; used only for BullMQ.
- Object storage (S3-compatible) is introduced with reports/exports in later
  phases; not required for Phase 1–2.

## 7–10. Build & processes

Without Docker:

```bash
npm ci
npx prisma generate
npm run build
npx prisma migrate deploy
npm run start          # web  (port 3000)
npm run worker         # worker (separate process)
```

Run both under a process manager (systemd or pm2), e.g. systemd units
`northwest-seo-web.service` and `northwest-seo-worker.service` with
`Restart=on-failure` and the `.env` loaded via `EnvironmentFile=`.

With Docker: `docker compose up -d` builds the image and starts web + worker
(compose file included; point DATABASE_URL/REDIS_URL at managed services in
production instead of the bundled dev containers).

## 11. Nginx reverse proxy

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name seo.northwestcar.group;
    return 301 https://seo.northwestcar.group$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name seo.northwestcar.group;

    ssl_certificate     /etc/letsencrypt/live/seo.northwestcar.group/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/seo.northwestcar.group/privkey.pem;

    client_max_body_size 20M;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
    }
}
```

Also redirect any alternative hostnames to the canonical host. Add HSTS
(`add_header Strict-Transport-Security "max-age=31536000" always;`) **only
after** HTTPS is verified working.

## 12. SSL

```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d seo.northwestcar.group
sudo certbot renew --dry-run   # auto-renewal check
```

Cloudflare alternative: DNS-only while testing origin → then proxy mode with
**Full (Strict)** SSL. Never Flexible. Don't cache `/api/*` or authenticated
pages; preserve visitor IPs (the app reads `x-forwarded-for`).

## 13–14. Migrations & seed

```bash
npx prisma migrate deploy      # every deploy, before starting new code
npm run db:seed                # OPTIONAL demo data — do not run for real tenants
```

## 15–16. Backups & restore

- Nightly `pg_dump -Fc northwest_seo > backup-$(date +%F).dump` (or managed
  snapshots), retained 30 days, encrypted at rest, stored off-server.
- Restore: `pg_restore --clean --if-exists -d northwest_seo backup.dump`,
  then `npx prisma migrate deploy`, then smoke-test login + health.
- Rehearse a restore before go-live and quarterly after.

## 17. Deployment rollback

- Keep the previous build directory/image tagged (`:previous`).
- Rollback = redeploy previous image. Migrations are additive-only wherever
  possible; a migration that must be reverted requires a restored backup —
  which is why destructive migrations need a backup taken immediately before.

## 18. Health checks

`GET /api/health` → 200 `{status:"ok"}` — wire this into the load balancer /
uptime monitor. It reports database and redis status without secrets.

## 19. Logs

- Web + worker log JSON (pino) to stdout → journald/docker logs.
- Never log secrets; the logger redacts token/password-shaped fields.

## 20–21. Callbacks & webhooks (later phases)

Document as they are enabled:
- `https://seo.northwestcar.group/api/auth/callback/google`
- `https://seo.northwestcar.group/api/integrations/google/callback`
- `https://seo.northwestcar.group/api/integrations/wordpress/callback`
- `https://seo.northwestcar.group/api/webhooks/stripe`

## 22–24. Production verification & troubleshooting

Run the checklist in PRODUCT_SPEC.md §Verification after every production
deploy (loads over HTTPS, register/login/logout, verification email URLs,
crawl start/progress/cancel, health endpoint, private pages noindex, no
localhost URLs anywhere).

Common issues:
| Symptom | Check |
| --- | --- |
| 500 on auth routes | env vars missing/invalid — startup logs name the field |
| Crawls stuck FAILED "queue unavailable" | Redis reachable? `REDIS_URL` correct? worker running? |
| `prepared statement already exists` | connection pooler in transaction mode → add `pgbouncer=true` to DATABASE_URL |
| Emails contain wrong host | `APP_URL` misconfigured |
| Cookies not set in production | HTTPS termination missing `X-Forwarded-Proto https` |
