Initial commit
This commit is contained in:
245
docs/DEPLOYMENT.md
Normal file
245
docs/DEPLOYMENT.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# Deploying DailyMeals on a Linux server
|
||||
|
||||
This guide assumes a **VPS or dedicated Linux host** (Ubuntu 22.04/24.04 or Debian 12) with:
|
||||
|
||||
- Docker Engine + Docker Compose plugin
|
||||
- An **external PostgreSQL** database (already populated — no DB container in this stack)
|
||||
- A domain name pointing at the server (recommended for HTTPS)
|
||||
|
||||
## Architecture on the server
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
▼
|
||||
[ports 80 / 443] nginx (frontend container) ── SPA static files
|
||||
│
|
||||
└── /api ──► ASP.NET Core API (api container, internal :5000)
|
||||
│
|
||||
▼
|
||||
External PostgreSQL (your DB host)
|
||||
```
|
||||
|
||||
Only **80** and **443** need to be public. The API stays on the Docker network.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prepare the server
|
||||
|
||||
```bash
|
||||
# Update system
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Install Docker (official convenience script) + Compose plugin
|
||||
curl -fsSL https://get.docker.com | sudo sh
|
||||
sudo usermod -aG docker "$USER"
|
||||
# Log out and back in so the docker group applies
|
||||
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
Optional firewall (UFW):
|
||||
|
||||
```bash
|
||||
sudo ufw allow OpenSSH
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Allow the API to reach PostgreSQL
|
||||
|
||||
On your **database server** (or cloud DB console):
|
||||
|
||||
1. Ensure PostgreSQL listens on an address reachable from the app server.
|
||||
2. Add the **app server’s public IP** to `pg_hba.conf` (or the provider’s “allowed IPs” list).
|
||||
3. Open port **5432** only to that IP (security group / firewall).
|
||||
|
||||
Test from the app server (requires `postgresql-client`):
|
||||
|
||||
```bash
|
||||
psql "host=YOUR_DB_HOST port=5432 dbname=YOUR_DB user=YOUR_USER password=YOUR_PASS" -c "SELECT 1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Deploy the application
|
||||
|
||||
```bash
|
||||
# Clone (or upload) the project
|
||||
git clone <your-repo-url> dailymeals
|
||||
cd dailymeals
|
||||
|
||||
# Configure secrets
|
||||
cp .env.example .env
|
||||
nano .env # or vim
|
||||
```
|
||||
|
||||
### Required `.env` values (production example)
|
||||
|
||||
```env
|
||||
DB_CONNECTION_STRING=Host=10.0.0.5;Port=5432;Database=mealplan;Username=mealapp;Password=STRONG_DB_PASSWORD
|
||||
|
||||
JWT_SECRET=PASTE_OUTPUT_OF_openssl_rand_-base64_48
|
||||
JWT_ISSUER=DailyMeals
|
||||
JWT_AUDIENCE=DailyMeals
|
||||
|
||||
# Must match the URL users type in the browser (scheme + host, no trailing slash)
|
||||
CORS_ALLOWED_ORIGIN=https://meals.example.com
|
||||
```
|
||||
|
||||
Generate JWT secret:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 48
|
||||
```
|
||||
|
||||
Build and start (production overlay hides public API port 5000):
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
Check status:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose logs -f api
|
||||
docker compose logs -f frontend
|
||||
curl -k https://localhost/health # via nginx proxy: use /api path — health is on API
|
||||
curl -k https://localhost/api/../health # wrong — health is NOT proxied by default
|
||||
```
|
||||
|
||||
API health is internal. Verify the site loads in a browser: `https://your-server-ip` (self-signed cert until step 4).
|
||||
|
||||
---
|
||||
|
||||
## 4. DNS and HTTPS (Let’s Encrypt)
|
||||
|
||||
1. Create an **A record**: `meals.example.com` → your server’s public IP.
|
||||
2. Wait for DNS to propagate.
|
||||
|
||||
### Option A — Certificates on the host, mounted into Docker (recommended)
|
||||
|
||||
Install Certbot:
|
||||
|
||||
```bash
|
||||
sudo apt install -y certbot
|
||||
```
|
||||
|
||||
Stop the frontend container so port 80 is free:
|
||||
|
||||
```bash
|
||||
cd ~/dailymeals
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml stop frontend
|
||||
```
|
||||
|
||||
Obtain certificate:
|
||||
|
||||
```bash
|
||||
sudo certbot certonly --standalone -d meals.example.com --agree-tos -m you@example.com
|
||||
```
|
||||
|
||||
Copy and edit the TLS compose file:
|
||||
|
||||
```bash
|
||||
cp docker-compose.tls.example.yml docker-compose.tls.yml
|
||||
nano docker-compose.tls.yml # set your domain paths under /etc/letsencrypt/live/...
|
||||
```
|
||||
|
||||
Ensure `.env` has:
|
||||
|
||||
```env
|
||||
CORS_ALLOWED_ORIGIN=https://meals.example.com
|
||||
```
|
||||
|
||||
Start again:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.tls.yml up -d
|
||||
```
|
||||
|
||||
Renewal (add to crontab, renew then reload):
|
||||
|
||||
```bash
|
||||
sudo certbot renew --deploy-hook "cd /home/YOUR_USER/dailymeals && docker compose -f docker-compose.yml -f docker-compose.prod.yml restart frontend"
|
||||
```
|
||||
|
||||
### Option B — Keep self-signed (testing only)
|
||||
|
||||
Skip `TLS_*` in `.env`. The image generates a self-signed cert. Browsers will show a security warning.
|
||||
|
||||
---
|
||||
|
||||
## 5. Post-deploy checklist
|
||||
|
||||
| Check | Command / action |
|
||||
|--------|------------------|
|
||||
| Containers running | `docker compose ps` |
|
||||
| API connects to DB | `docker compose logs api` — no connection errors on startup |
|
||||
| Login / register | Open `https://meals.example.com/register` |
|
||||
| HTTPS valid | Padlock in browser (with Let’s Encrypt) |
|
||||
| Firewall | Only 22, 80, 443 (and 5432 **not** open to the world on app server) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Updates (new version)
|
||||
|
||||
```bash
|
||||
cd ~/dailymeals
|
||||
git pull
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Important production notes
|
||||
|
||||
### Refresh tokens are in-memory
|
||||
|
||||
The API stores refresh tokens **in RAM**. After a container restart, users must log in again. For a **single** API container this is fine. Do **not** scale the API to multiple replicas without replacing `InMemoryRefreshTokenStore` with Redis or similar.
|
||||
|
||||
### Logs
|
||||
|
||||
API logs: `docker compose logs api` and files under the container at `logs/mealplan-*.log` (ephemeral unless you mount a volume).
|
||||
|
||||
### Backups
|
||||
|
||||
Back up your **PostgreSQL** database on the DB host. This app does not manage backups.
|
||||
|
||||
### Swagger
|
||||
|
||||
Swagger is **disabled** in `Production` (`ASPNETCORE_ENVIRONMENT=Production` in compose).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | What to check |
|
||||
|---------|----------------|
|
||||
| 502 / empty `/api` | `docker compose logs api`; DB connection string; is `api` healthy? |
|
||||
| CORS errors in browser | `CORS_ALLOWED_ORIGIN` must exactly match `https://your-domain` |
|
||||
| DB connection refused | Firewall, `pg_hba.conf`, wrong host/port in `DB_CONNECTION_STRING` |
|
||||
| Certificate errors | Paths in `docker-compose.tls.yml`; cert files readable inside container |
|
||||
| Register/login 404 on API | Rebuild images: `docker compose ... up -d --build` |
|
||||
|
||||
---
|
||||
|
||||
## Minimal command reference
|
||||
|
||||
```bash
|
||||
# Start
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||
|
||||
# Stop
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml down
|
||||
|
||||
# Logs
|
||||
docker compose logs -f api frontend
|
||||
|
||||
# Restart one service
|
||||
docker compose restart api
|
||||
```
|
||||
Reference in New Issue
Block a user