256 lines
12 KiB
Markdown
256 lines
12 KiB
Markdown
---
|
||
title: Deployment
|
||
tags: [operations]
|
||
---
|
||
|
||
# Deployment
|
||
|
||
How the production stack runs and gets updated on the live host. The stack is fully containerised and self-updates via Watchtower from the Gitea container registry.
|
||
|
||
---
|
||
|
||
## 1. Topology
|
||
|
||
```
|
||
┌─────────────────────────┐
|
||
HTTPS 443 ──────────►│ External SSL term. │
|
||
│ (DNS amn.gg, dev.amn.gg)│
|
||
└────────────┬────────────┘
|
||
│ HTTP 80 (in-VPC)
|
||
▼
|
||
┌──────────────────────────────────┐
|
||
│ Nginx container │
|
||
│ (nickapp-nginx, port 80) │
|
||
└─┬───────────────────┬────────────┘
|
||
│ │
|
||
│ / │ /api /socket.io
|
||
▼ ▼
|
||
┌─────────────────────┐ ┌──────────────────────────┐
|
||
│ nickapp-frontend │ │ nickapp-backend │
|
||
│ Next.js, port 8083 │ │ Express 5, port 5001 │
|
||
└─────────────────────┘ └──────┬────────────┬──────┘
|
||
│ │
|
||
▼ ▼
|
||
┌──────────┐ ┌──────────┐
|
||
│ mongodb │ │ redis │
|
||
│ 8.2 │ │ 8 │
|
||
└──────────┘ └──────────┘
|
||
|
||
┌──────────────────────────────────┐
|
||
│ Watchtower │
|
||
│ Polls registry → restarts │
|
||
│ containers labelled enable=true │
|
||
└──────────────────────────────────┘
|
||
```
|
||
|
||
All containers run on the **`default`** Docker network defined by `docker-compose.production.yml`. Watchtower runs as a sidecar container on the same host.
|
||
|
||
DNS resolves both `amn.gg` and `dev.amn.gg` to the production host's public IP. SSL termination happens **outside** the compose stack (typically via the hosting provider's edge or a host-level reverse proxy), and traffic is forwarded as HTTP to the `nginx` container on port `80` (mapped to host `8083`).
|
||
|
||
---
|
||
|
||
## 2. Compose file
|
||
|
||
`backend/docker-compose.production.yml` is the single source of truth. Services:
|
||
|
||
| Service | Image | Ports | Volumes | Notes |
|
||
|---------|-------|-------|---------|-------|
|
||
| `nginx` | `nginx:alpine` | `8083:80` | `./nginx/nginx.conf`, `./nginx/logs`, `./uploads` (served as `/uploads`) | Reverse proxy |
|
||
| `nickapp-backend` | `nickapp-backend:latest` (build from `Dockerfile.prod`) | not exposed externally | `./uploads:/app/uploads` | Labelled for Watchtower |
|
||
| `nickapp-frontend` | `nickapp-frontend:latest` (build from `../frontend/Dockerfile`) | `expose: 8083` | — | Labelled for Watchtower |
|
||
| `mongodb` | `mongo:8.2` | not exposed | `mongodb_data:/data/db`, `./mongo-init:/docker-entrypoint-initdb.d` | Healthcheck via `mongosh ping` |
|
||
| `redis` | `redis:8-alpine` | not exposed | `redis_data:/data` | Started with `--requirepass "$REDIS_PASSWORD"` |
|
||
|
||
Healthchecks are configured for backend (`curl /health`), frontend (`curl /`), Mongo (`mongosh ping`), and Redis (`redis-cli -a $REDIS_PASSWORD ping`). See [[Monitoring]].
|
||
|
||
Watchtower polls images labelled `com.centurylinklabs.watchtower.enable=true` — currently `nickapp-backend` and `nickapp-frontend`. MongoDB and Redis are **not** auto-updated.
|
||
|
||
---
|
||
|
||
## 3. Registry & images
|
||
|
||
| Image | Registry path |
|
||
|-------|---------------|
|
||
| Backend prod | `git.manko.yoga/manawenuz/escrow-backend:latest` |
|
||
| Backend dev | `git.manko.yoga/manawenuz/escrow-backend:dev` |
|
||
| Backend tagged | `git.manko.yoga/manawenuz/escrow-backend:<package-version>` |
|
||
| Frontend | `git.manko.yoga/manawenuz/escrow-frontend:latest` and `:<version>` |
|
||
|
||
`docker-compose.production.yml` currently builds locally on first up (`build: context: .`). Once images are in the registry the file can be switched to `image: git.manko.yoga/manawenuz/escrow-backend:latest` to let Watchtower pull straight from there.
|
||
|
||
> [!tip] To pin a specific version while debugging, edit the compose file to `image: git.manko.yoga/manawenuz/escrow-backend:2.6.3` and re-run `docker compose up -d`. Remove the Watchtower label or the agent will undo it on next poll.
|
||
|
||
---
|
||
|
||
## 4. Watchtower
|
||
|
||
Watchtower runs as its own container (managed outside the compose file) with `WATCHTOWER_LABEL_ENABLE=true` so it only touches services that opt in. On each poll cycle (default 5 minutes, configurable via `WATCHTOWER_POLL_INTERVAL`) it:
|
||
|
||
1. Pulls the latest digest for each enabled service's image.
|
||
2. Compares to the running container's digest.
|
||
3. If different, stops the container, removes it, and starts a new one from the new image, preserving all named volumes.
|
||
|
||
Configuration knobs typically set on the host:
|
||
|
||
```bash
|
||
docker run -d --name watchtower \
|
||
--restart unless-stopped \
|
||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||
-v /root/.docker/config.json:/config.json \ # so it can pull from the private Gitea registry
|
||
-e WATCHTOWER_POLL_INTERVAL=300 \
|
||
-e WATCHTOWER_LABEL_ENABLE=true \
|
||
-e WATCHTOWER_CLEANUP=true \
|
||
-e WATCHTOWER_INCLUDE_RESTARTING=true \
|
||
containrrr/watchtower
|
||
```
|
||
|
||
The `~/.docker/config.json` must have a valid login for `git.manko.yoga` (created via `docker login git.manko.yoga -u manawenuz`).
|
||
|
||
---
|
||
|
||
## 5. First-time deploy (cold start)
|
||
|
||
> [!warning] Run these steps on a fresh production host. They are destructive on an existing one. See [[Backup & Recovery]] before touching live data.
|
||
|
||
### Prerequisites on the host
|
||
|
||
- Ubuntu 22.04+ (or any systemd Linux), Docker Engine 24+, `docker compose` plugin
|
||
- `git` installed
|
||
- DNS `amn.gg` + `dev.amn.gg` already pointing here
|
||
- An SSL terminator (Caddy / Nginx / Cloudflare) reverse-proxying to host port `8083`
|
||
- Registry login: `docker login git.manko.yoga -u manawenuz`
|
||
|
||
### Steps
|
||
|
||
```bash
|
||
# 1. Clone both repos as siblings (compose references ../frontend)
|
||
cd /opt
|
||
git clone ssh://git@git.manko.yoga:222/nick/backend.git
|
||
git clone ssh://git@git.manko.yoga:222/nick/frontend.git
|
||
cd backend
|
||
git checkout main
|
||
|
||
# 2. Create the production .env
|
||
sudo nano .env # fill from Environment Variables doc; production values, real secrets
|
||
|
||
# 3. Provision the nginx config + uploads dir
|
||
mkdir -p nginx/logs uploads mongo-init
|
||
sudo cp /path/to/nginx.conf nginx/nginx.conf
|
||
# (the nginx.conf forwards /api/* and /socket.io/* to nickapp-backend:5001,
|
||
# forwards /uploads/* to /uploads (volume), and everything else to nickapp-frontend:8083)
|
||
|
||
# 4. Build & start the stack
|
||
docker compose -f docker-compose.production.yml up --build -d
|
||
|
||
# 5. Verify
|
||
docker compose -f docker-compose.production.yml ps
|
||
docker compose -f docker-compose.production.yml logs -f --tail=200
|
||
curl -fsS http://localhost:8083/api/health | jq .
|
||
|
||
# 6. Seed initial data (optional — if AUTO_SEED_ON_START=true is set, it's already done)
|
||
docker compose -f docker-compose.production.yml exec nickapp-backend node dist/scripts/seedCategories.js
|
||
|
||
# 7. Start Watchtower (one-time)
|
||
docker run -d --name watchtower --restart unless-stopped \
|
||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||
-v /root/.docker/config.json:/config.json \
|
||
-e WATCHTOWER_POLL_INTERVAL=300 \
|
||
-e WATCHTOWER_LABEL_ENABLE=true \
|
||
-e WATCHTOWER_CLEANUP=true \
|
||
containrrr/watchtower
|
||
```
|
||
|
||
### SSL / TLS
|
||
|
||
Termination happens at the edge — outside the compose stack. The two common setups:
|
||
|
||
- **Caddy on the host** forwarding `amn.gg` and `dev.amn.gg` to `127.0.0.1:8083`. Caddy handles Let's Encrypt automatically.
|
||
- **Cloudflare Full (strict)** in front of the host. Use Cloudflare Origin certificates on the host's Caddy/Nginx.
|
||
|
||
Either way, the compose stack itself sees only HTTP on port 80 inside the nginx container. The `nginx.conf` should set `proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto` and the backend already trusts the proxy when `NODE_ENV=production` (see `trust proxy` block in `src/app.ts`).
|
||
|
||
---
|
||
|
||
## 6. Routine deploy (after first deploy)
|
||
|
||
The normal flow is **fully automatic**:
|
||
|
||
1. Developer merges PR to `main` (see [[Git Workflow]]).
|
||
2. Gitea Actions runs `.gitea/workflows/docker-build-no-cache.yml` (backend) or `deploy.yml` (frontend). The workflow builds the production image and pushes `:latest` + `:<version>` to the registry. See [[CI-CD Pipeline]].
|
||
3. Watchtower polls the registry, sees a new digest, restarts the container.
|
||
4. Healthcheck on the new container passes after `start_period=40s`, traffic resumes.
|
||
|
||
Total time from merge to live: **5–10 minutes** depending on Watchtower poll interval and image size.
|
||
|
||
### Force an immediate deploy
|
||
|
||
If you don't want to wait for the poll:
|
||
|
||
```bash
|
||
# On the production host:
|
||
cd /opt/backend
|
||
docker login git.manko.yoga -u manawenuz # if creds expired
|
||
docker compose -f docker-compose.production.yml pull nickapp-backend nickapp-frontend
|
||
docker compose -f docker-compose.production.yml up -d nickapp-backend nickapp-frontend
|
||
```
|
||
|
||
The `up -d` will detect changed images and restart only the affected containers.
|
||
|
||
### Roll back
|
||
|
||
```bash
|
||
# Find available versions
|
||
docker images git.manko.yoga/manawenuz/escrow-backend
|
||
|
||
# Pin to the previous tag in the compose file
|
||
sed -i 's|escrow-backend:latest|escrow-backend:2.6.2|' docker-compose.production.yml
|
||
|
||
# Re-up
|
||
docker compose -f docker-compose.production.yml up -d nickapp-backend
|
||
|
||
# Disable Watchtower for the affected service until you're ready to resume
|
||
docker compose ... restart # no-op if you removed the watchtower label
|
||
```
|
||
|
||
> [!warning] Watchtower will undo a pin to a non-`latest` tag on its next poll if the container still has the `watchtower.enable=true` label. Either remove the label temporarily or pause Watchtower (`docker stop watchtower`).
|
||
|
||
---
|
||
|
||
## 7. Logs
|
||
|
||
```bash
|
||
# All services
|
||
docker compose -f docker-compose.production.yml logs -f --tail=300
|
||
|
||
# Single service
|
||
docker compose -f docker-compose.production.yml logs -f nickapp-backend
|
||
|
||
# Nginx access log
|
||
tail -f /opt/backend/nginx/logs/access.log
|
||
```
|
||
|
||
Backend logs are also captured by Sentry breadcrumbs when an error occurs — see [[Monitoring]].
|
||
|
||
---
|
||
|
||
## 8. Maintenance window
|
||
|
||
Plan a 5-minute window when bumping major versions or running migrations:
|
||
|
||
```bash
|
||
# Announce + drain
|
||
# (set a maintenance banner in the frontend if possible)
|
||
|
||
# Take a backup first
|
||
./scripts/backup-mongo.sh # or per Backup & Recovery
|
||
|
||
# Pull new images, restart
|
||
docker compose -f docker-compose.production.yml pull
|
||
docker compose -f docker-compose.production.yml up -d
|
||
|
||
# Verify
|
||
curl -fsS https://amn.gg/api/health
|
||
```
|
||
|
||
If anything goes sideways, follow [[Incident Response]].
|