6.3 KiB
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
# 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):
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):
- Ensure PostgreSQL listens on an address reachable from the app server.
- Add the app server’s public IP to
pg_hba.conf(or the provider’s “allowed IPs” list). - Open port 5432 only to that IP (security group / firewall).
Test from the app server (requires postgresql-client):
psql "host=YOUR_DB_HOST port=5432 dbname=YOUR_DB user=YOUR_USER password=YOUR_PASS" -c "SELECT 1"
3. Deploy the application
# 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)
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:
openssl rand -base64 48
Build and start (production overlay hides public API port 5000):
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
Check status:
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)
- Create an A record:
meals.example.com→ your server’s public IP. - Wait for DNS to propagate.
Option A — Certificates on the host, mounted into Docker (recommended)
Install Certbot:
sudo apt install -y certbot
Stop the frontend container so port 80 is free:
cd ~/dailymeals
docker compose -f docker-compose.yml -f docker-compose.prod.yml stop frontend
Obtain certificate:
sudo certbot certonly --standalone -d meals.example.com --agree-tos -m you@example.com
Copy and edit the TLS compose file:
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:
CORS_ALLOWED_ORIGIN=https://meals.example.com
Start again:
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):
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)
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
# 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