# 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 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 ```