Files
dyolink/infrastructure/DEPLOY.md

422 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dyolink — Production Server Deploy Guide
Deploy the full stack (Postgres, NestJS API, Next.js, Nginx, Let's Encrypt) on a fresh Linux server using **Docker Hub** images.
**Example used in production:** `https://wixur.ir` on server `185.243.48.140`.
---
## Architecture
```
Internet → Nginx (:80 / :443)
├── / → frontend:3000 (Next.js)
└── /api → backend:3000 (NestJS)
└── postgres:5432
```
| Service | Image | Notes |
|-----------|------------------------------------|--------------------------------|
| postgres | `postgres:15-alpine` | Data in Docker volume |
| backend | `dyolink/dyolink-backend:latest` | Runs migrations + seed on start |
| frontend | `dyolink/dyolink-frontend:latest` | URLs baked in at **build time** |
| nginx | `nginx:alpine` | SSL termination + reverse proxy |
| certbot | `certbot/certbot` | Auto-renews certificates |
---
## Prerequisites
### On your Mac (build machine)
- Docker Desktop running
- Repo cloned
- Docker Hub account (`dyolink`) with images pushed
### On the server
- Ubuntu 24.04 (or similar)
- Root or sudo access
- **Domain** with DNS **A record** → server public IP
- Ports **22**, **80**, **443** open (UFW + cloud provider firewall)
---
## Part 1 — Build & push images (Mac)
Frontend URLs are **compiled into the image**. Always build with the real public domain:
```bash
cd /path/to/dyolink
docker login # only needed on Mac to push
./infrastructure/scripts/build-and-push-prod.sh YOUR_DOMAIN.com latest
```
Example:
```bash
./infrastructure/scripts/build-and-push-prod.sh wixur.ir latest
```
This pushes:
- `dyolink/dyolink-backend:latest`
- `dyolink/dyolink-frontend:latest`
**When to rebuild:** domain changes, frontend env (`NEXT_PUBLIC_*`) changes, or new app release.
---
## Part 2 — Server bootstrap (once per server)
SSH as root:
```bash
ssh root@YOUR_SERVER_IP
```
### 2.1 Update system & create deploy user
```bash
apt update && apt upgrade -y
apt install -y curl git ufw fail2ban
adduser dyolink
usermod -aG sudo dyolink
# Optional: copy SSH keys from root
mkdir -p /home/dyolink/.ssh
cp /root/.ssh/authorized_keys /home/dyolink/.ssh/ 2>/dev/null || true
chown -R dyolink:dyolink /home/dyolink/.ssh
chmod 700 /home/dyolink/.ssh
```
### 2.2 Install Docker
If `curl -fsSL https://get.docker.com | sh` returns **403**, use Ubuntu packages:
```bash
apt update
apt install -y docker.io docker-compose-v2
systemctl enable --now docker
usermod -aG docker dyolink
```
Verify:
```bash
docker --version
docker compose version
```
### 2.3 Docker Hub login (if images are private)
```bash
docker login
```
Public images skip this step.
### 2.4 Firewall
```bash
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
```
Also open **80** and **443** in your VPS provider's cloud firewall panel if one exists.
### 2.5 DNS
Before SSL, confirm DNS:
```bash
dig +short YOUR_DOMAIN.com
# Must return YOUR_SERVER_IP
```
---
## Part 3 — Copy infrastructure to server (Mac)
```bash
cd /path/to/dyolink
ssh dyolink@YOUR_SERVER_IP "sudo mkdir -p /opt/dyolink/secrets && sudo chown -R dyolink:dyolink /opt/dyolink"
scp -r infrastructure/docker-compose.prod.yml \
infrastructure/nginx \
infrastructure/scripts \
infrastructure/database \
infrastructure/deploy.prod.env.example \
infrastructure/backend.prod.env.example \
infrastructure/database.prod.env.example \
dyolink@YOUR_SERVER_IP:/opt/dyolink/infrastructure/
```
On the server:
```bash
ssh dyolink@YOUR_SERVER_IP
chmod +x /opt/dyolink/infrastructure/scripts/*.sh
```
---
## Part 4 — Configure secrets (server)
### 4.1 Main `.env`
```bash
cd /opt/dyolink/infrastructure
cp deploy.prod.env.example .env
nano .env
```
```env
DOMAIN=wixur.ir
DOCKER_USERNAME=dyolink
TAG=latest
LETSENCRYPT_EMAIL=your-email@example.com
DEPLOY_SECRETS_DIR=/opt/dyolink/secrets
```
### 4.2 Database secrets
```bash
cp database.prod.env.example /opt/dyolink/secrets/database.env
nano /opt/dyolink/secrets/database.env
```
```env
POSTGRES_USER=dyolink_user
POSTGRES_PASSWORD=STRONG_PASSWORD_HERE
POSTGRES_DB=dyolink_db
```
### 4.3 Backend secrets
```bash
cp backend.prod.env.example /opt/dyolink/secrets/backend.env
nano /opt/dyolink/secrets/backend.env
```
Generate JWT secrets:
```bash
openssl rand -hex 32 # use for JWT_SECRET
openssl rand -hex 32 # use for JWT_REFRESH_SECRET (must be different)
```
```env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://dyolink_user:STRONG_PASSWORD_HERE@postgres:5432/dyolink_db
JWT_SECRET=<first openssl output>
JWT_EXPIRES_IN=15m
JWT_REFRESH_SECRET=<second openssl output>
JWT_REFRESH_EXPIRES_IN=30d
FRONTEND_URL=https://wixur.ir
SMS_IR_API_KEY=your_key
SMS_IR_TEMPLATE_ID=your_template_id
```
**Critical checks:**
| Rule | Why |
|------|-----|
| `DATABASE_URL` password = `POSTGRES_PASSWORD` | Backend cannot connect otherwise |
| `FRONTEND_URL` = `https://YOUR_DOMAIN` | CORS, cookies, invite links |
| JWT secrets must **not** contain `CHANGE_ME` | App refuses to start (by design) |
| Postgres password set **before first** `up` | Password only applied on first volume create |
---
## Part 5 — Deploy (server)
```bash
cd /opt/dyolink/infrastructure
./scripts/deploy-prod.sh
```
This script:
1. Issues Let's Encrypt certificate (first run)
2. Renders HTTPS nginx config
3. Pulls images from Docker Hub
4. Starts all containers
First deploy takes **35 minutes**.
---
## Part 6 — Verify
```bash
docker compose -f docker-compose.prod.yml --env-file .env ps
```
Expected:
| Container | Status |
|-----------|--------|
| dyolink_db_prod | Up (healthy) |
| dyolink_backend_prod | Up (healthy) |
| dyolink_frontend_prod | Up |
| dyolink_nginx_prod | Up |
| dyolink_certbot_prod | Up |
```bash
curl -s https://YOUR_DOMAIN/api/health
# {"status":"ok","timestamp":"..."}
curl -I https://YOUR_DOMAIN/
# HTTP/2 200
```
Open `https://YOUR_DOMAIN` in a browser.
---
## Updating the app (new release)
**On Mac** — build & push:
```bash
./infrastructure/scripts/build-and-push-prod.sh wixur.ir latest
```
**On server:**
```bash
cd /opt/dyolink/infrastructure
docker compose -f docker-compose.prod.yml --env-file .env pull backend frontend
docker compose -f docker-compose.prod.yml --env-file .env up -d
```
Backend runs `prisma migrate deploy` automatically on container start.
---
## Troubleshooting
### Docker install: `get.docker.com` returns 403
Use `apt install docker.io docker-compose-v2` (see Part 2.2).
### Docker Hub pull: 403 Forbidden
```bash
docker login
```
If still blocked, transfer images from Mac:
```bash
# Mac
docker save dyolink/dyolink-backend:latest dyolink/dyolink-frontend:latest \
postgres:15-alpine nginx:alpine certbot/certbot:latest | gzip > images.tar.gz
scp images.tar.gz dyolink@SERVER:/tmp/
# Server
gunzip -c /tmp/images.tar.gz | docker load
```
### Backend crash: `JWT_SECRET must be changed from the placeholder value`
Edit `/opt/dyolink/secrets/backend.env` — replace JWT secrets with `openssl rand -hex 32` output. Restart:
```bash
docker compose -f docker-compose.prod.yml --env-file .env up -d backend
```
### HTTP shows "obtaining SSL certificate" / HTTPS fails
Nginx is still on the bootstrap config. Fix:
```bash
cd /opt/dyolink/infrastructure
./scripts/render-nginx-ssl.sh
docker compose -f docker-compose.prod.yml --env-file .env up -d nginx --force-recreate
curl -s https://YOUR_DOMAIN/api/health
```
### Backend `Restarting` — database password mismatch
If you changed `POSTGRES_PASSWORD` after the first deploy, reset the DB volume (destroys data):
```bash
docker compose -f docker-compose.prod.yml --env-file .env down
docker volume rm dyolink_postgres_data_prod
# Fix database.env + backend.env passwords to match
./scripts/deploy-prod.sh
```
### View logs
```bash
docker compose -f docker-compose.prod.yml --env-file .env logs backend --tail 50
docker compose -f docker-compose.prod.yml --env-file .env logs nginx --tail 50
docker compose -f docker-compose.prod.yml --env-file .env logs frontend --tail 50
```
### Internal health checks (bypass public network)
```bash
docker compose -f docker-compose.prod.yml --env-file .env exec nginx \
wget -qO- http://backend:3000/api/health
docker compose -f docker-compose.prod.yml --env-file .env exec frontend \
wget -qO- http://127.0.0.1:3000/ | head -3
```
---
## File reference
| Path on server | Purpose |
|----------------|---------|
| `/opt/dyolink/infrastructure/.env` | Domain, Docker Hub user, Let's Encrypt email |
| `/opt/dyolink/secrets/database.env` | Postgres credentials |
| `/opt/dyolink/secrets/backend.env` | API secrets, DATABASE_URL, JWT, SMS |
| `/opt/dyolink/infrastructure/nginx/generated/default.conf` | Auto-generated nginx SSL config |
| `/opt/dyolink/infrastructure/scripts/deploy-prod.sh` | Main deploy entry point |
| `/opt/dyolink/infrastructure/scripts/build-and-push-prod.sh` | Build & push (run on Mac) |
---
## Quick checklist (new server)
- [ ] DNS A record → server IP
- [ ] Docker installed on server
- [ ] UFW + cloud firewall: 22, 80, 443 open
- [ ] Images built with correct domain and pushed to Docker Hub
- [ ] `infrastructure/` copied to `/opt/dyolink/`
- [ ] `.env`, `database.env`, `backend.env` configured (real passwords + JWT)
- [ ] `./scripts/deploy-prod.sh` completed
- [ ] `curl https://DOMAIN/api/health` returns `{"status":"ok",...}`
- [ ] App loads in browser
---
## Issues encountered on first deploy (wixur.ir) — summary
| Problem | Cause | Type |
|---------|-------|------|
| `get.docker.com` 403 | Regional/network block | **Server setup** — use `apt install docker.io` |
| Docker Hub pull 403 | Hub blocked without login | **Server setup**`docker login` |
| Backend crash loop | `JWT_SECRET` still had `CHANGE_ME` | **Config** — edit `backend.env` |
| HTTP "obtaining SSL" / HTTPS broken | Nginx not recreated after SSL config | **Deploy script** — fixed in `init-letsencrypt.sh` / `deploy-prod.sh` |
| Frontend "unhealthy" in `docker ps` | Healthcheck timing; app still served pages | **Cosmetic** — no action needed |
**No application code changes were required.** The app, migrations, and seed all worked on first deploy once config was correct.