Complete Real-World Project
ជំពូកនេះមិនទាន់ត្រូវបានបកប្រែជាភាសាខ្មែរនៅឡើយទេ — សូមអានជាភាសាអង់គ្លេសខាងក្រោម។
Table of Contents
- Architecture
- Nginx Edge: Single Entry Point
- SSL/TLS
- Docker Compose: Layering the Files
- Automatic Restart
- Production-Ready Checklist
- Run It
This final part assembles everything from the entire series into one production-ready deployment: React (Vite), NestJS, PostgreSQL, Redis, Nginx, and SSL—all launched with Docker Compose. Rather than starting over, it adds one more layer on top of multi-container-project/ (Parts 11, 15, and 18): a single Nginx entry point in front of the whole stack, terminating HTTPS.
1. Architecture
┌─────────────────────────────┐
HTTPS (443) ───────▶ │ nginx (reverse proxy) │
HTTP (80) ───────▶ │ terminates TLS, routes: │
│ / -> frontend │
│ /health, ... -> backend │
└───────────┬─────────┬────────┘
│ │
┌──────────▼──┐ ┌──▼───────────┐
│ frontend │ │ backend │
│ React/Vite │ │ NestJS │
│ (internal) │ │ (internal) │
└─────────────┘ └───┬──────┬───┘
│ │
┌──────▼──┐ ┌─▼─────┐
│ postgres│ │ redis │
└─────────┘ └───────┘
Only nginx is reachable from outside the Docker network—frontend, backend, db, and redis all communicate over the internal app-net bridge network (recap: Part 9), the same pattern already used throughout Part 11.
2. Nginx Edge: Single Entry Point
nginx/nginx.conf is the new piece: it redirects plain HTTP to HTTPS, then routes by path—backend API routes go to backend, everything else goes to frontend.
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location ~ ^/(health|db-check|cache-check)$ {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
}
location / {
proxy_pass http://frontend:80;
proxy_set_header Host $host;
}
}
Because the frontend is now always served same-origin through this proxy, its API calls no longer need a cross-origin URL—VITE_API_URL is baked in as an empty string at build time, so fetch(${API_URL}/health) in App.jsx becomes a same-origin fetch("/health") automatically, no source change required (recap the build-arg pattern from Part 18, section 1).
A note on route matching: this demo proxies a fixed, small list of known backend paths. A larger real application would typically give the backend a shared prefix (e.g. NestJS's
app.setGlobalPrefix('api')) and proxy everything under/api/in onelocationblock instead of enumerating routes—simpler to maintain as the API grows.
3. SSL/TLS
For local testing, nginx/generate-self-signed-cert.sh generates a throwaway self-signed certificate so HTTPS can be exercised end-to-end without owning a real domain:
./nginx/generate-self-signed-cert.sh
# writes nginx/certs/fullchain.pem and nginx/certs/privkey.pem
Browsers will (correctly) warn that a self-signed certificate isn't trusted—that's expected for local development, verifiable with curl -k instead of a browser.
For production, replace the self-signed files with a real certificate the same way Part 17, section 6 already covered: run Certbot on the host (or in its own container) against your actual domain, and mount the resulting fullchain.pem/privkey.pem into nginx/certs/ at the exact same paths—nginx.conf doesn't need to change at all.
# On the VPS, after Certbot has issued a real certificate for example.com:
cp /etc/letsencrypt/live/example.com/fullchain.pem nginx/certs/fullchain.pem
cp /etc/letsencrypt/live/example.com/privkey.pem nginx/certs/privkey.pem
docker compose -f docker-compose.prod.yml -f docker-compose.ssl.yml restart nginx
4. Docker Compose: Layering the Files
Rather than duplicating the whole stack, docker-compose.ssl.yml is an override file (recap: Part 12, section 5) that layers on top of the existing docker-compose.prod.yml from Part 18:
services:
nginx:
build: ./nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/certs:/etc/nginx/certs:ro
depends_on:
- frontend
- backend
networks:
- app-net
frontend:
build:
args:
VITE_API_URL: ""
ports: !reset [] # no longer published directly—only nginx is
backend:
ports: !reset [] # same: only reachable through nginx now
The !reset [] marker is needed because Compose normally merges array fields like ports across layered files rather than replacing them—without it, frontend and backend would stay published on their original host ports (80 and 3000) in addition to Nginx trying to claim 80, causing a port conflict.
# Run both files together—later files override/extend earlier ones
docker compose -f docker-compose.prod.yml -f docker-compose.ssl.yml up --build -d
5. Automatic Restart
Every service in the stack—including the new nginx service—uses restart: unless-stopped (recap: Part 18, section 2), so the entire stack self-heals after a crash or host reboot without manual intervention. Health checks on backend, db, and redis (recap: Part 18, section 3) still gate startup order underneath the new Nginx layer.
6. Production-Ready Checklist
Everything from Part 19 applies to this stack directly:
- ✅ Multi-stage, optimized production Dockerfiles (
Dockerfile.prodfor bothfrontendandbackend, Part 15) - ✅ Non-root user in the backend container (
USER node, Part 19, section 4) - ✅
restart: unless-stoppedon every service (Part 18, section 2; recap above in section 5) - ✅ Health checks gating startup order (Part 18, section 3)
- ✅ Bounded JSON-file logging on every service (Part 18, section 4)
- ✅ Persistent named volumes for
postgres_dataandredis_data(Part 8) - ✅ Secrets/config via
.env, never committed (only.env.exampleis, Part 12) - ✅ Single public entry point behind Nginx, HTTPS-only (this part)
- ✅ Immutable image tags in a real deployment, not
:latest(Part 19, section 5)
7. Run It
cd multi-container-project
cp .env.example .env
./nginx/generate-self-signed-cert.sh # local HTTPS testing only
docker compose -f docker-compose.prod.yml -f docker-compose.ssl.yml up --build -d
# Frontend (through Nginx): https://localhost/
curl -k https://localhost/
# Backend API (through Nginx):
curl -k https://localhost/health
curl -k https://localhost/db-check
curl -k https://localhost/cache-check
# Tear down (add -v to also delete the Postgres/Redis volumes)
docker compose -f docker-compose.prod.yml -f docker-compose.ssl.yml down
That's the complete series: from docker run hello-world in Part 1 to a health-checked, auto-restarting, HTTPS-terminated multi-container production stack here in Part 20.
Summary Cheat Sheet
| Task | Command |
|---|---|
| Generate a local self-signed cert | ./nginx/generate-self-signed-cert.sh |
| Run the complete stack | docker compose -f docker-compose.prod.yml -f docker-compose.ssl.yml up --build -d |
| Reset an array field in an override | <key>: !reset [] |
| Test HTTPS locally (self-signed cert) | curl -k https://localhost/ |
| Swap in a real certificate | copy Certbot's fullchain.pem/privkey.pem into nginx/certs/, restart nginx |
| Tear down the full stack | docker compose -f docker-compose.prod.yml -f docker-compose.ssl.yml down -v |
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!