From 99389901fd18ae3492e457d9760c0a8fac534bd8 Mon Sep 17 00:00:00 2001 From: Mohamed jameel alabasy Date: Sun, 3 May 2026 02:14:01 +0300 Subject: [PATCH] nginx: add /api proxy and update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework Nginx docs and adjust vhost configs: README.md reorganized to clearly separate frontend (rythmify.duckdns.org) and backend (rythmify-back.duckdns.org) responsibilities, add prerequisites, GeoIP2 setup, deployment steps, and automation notes. nginx/rythmify-back.duckdns.org.conf: add explicit /api/ location that mirrors the catch-all proxy but preserves WebSocket upgrade headers, injects X-Country-Code, and bypasses proxy cache for upgrades; clarify /health comment; change internal nginx_status server to allow all (safe because it binds to Docker bridge). nginx/rythmify.duckdns.org.conf: simplify to static React SPA serving from /var/www/rythmify with HTTP→HTTPS redirect, remove backend proxy blocks (API, sockets, Grafana) since API traffic is served via the backend domain. Minor formatting/clarity tweaks throughout. --- nginx/README.md | 248 ++++++++++++++------------- nginx/rythmify-back.duckdns.org.conf | 31 +++- nginx/rythmify.duckdns.org.conf | 59 +------ 3 files changed, 159 insertions(+), 179 deletions(-) diff --git a/nginx/README.md b/nginx/README.md index 2a25290..54c0d55 100644 --- a/nginx/README.md +++ b/nginx/README.md @@ -2,124 +2,68 @@ ## Overview -This folder contains the Nginx virtual host configuration files for **VM1** (`rythmify-app-vm`, IP `20.196.3.253`). These files are not loaded automatically — they must be deployed to the server manually (see [Deployment](#deployment)). +This folder contains the Nginx virtual host configuration files for **VM1** (`rythmify-app-vm`, IP `20.196.3.253`). | File | Domain | Purpose | |---|---|---| -| `rythmify-back.duckdns.org.conf` | `rythmify-back.duckdns.org` | Backend API + Socket.IO proxy, GeoIP2 country injection, HTTPS, Grafana proxy, Nginx stub_status | -| `rythmify.duckdns.org.conf` | `rythmify.duckdns.org` | Frontend Vite dev server proxy, HTTPS | - -The backend config contains three server blocks: -- **HTTP → HTTPS redirect** on port 80 -- **HTTPS backend** on port 443 — proxies `/health`, `/grafana/`, and all API + WebSocket traffic to `localhost:8080` / `localhost:3000` -- **stub_status** on `172.17.0.1:8081` — scraped by `nginx_exporter` inside Docker - -The `geoip2` directive (above all `server {}` blocks, in the `http {}` context) resolves every client IP to a country code and injects it as `X-Country-Code` on all proxied requests to the backend. +| `rythmify-back.duckdns.org.conf` | `rythmify-back.duckdns.org` | Backend API + Socket.IO WebSocket proxy, GeoIP2 country injection, Grafana proxy, HTTPS | +| `rythmify.duckdns.org.conf` | `rythmify.duckdns.org` | Frontend static files (React SPA), HTTPS | --- -## Prerequisites +## Domain Split -Install the following packages on VM1: +### rythmify.duckdns.org — Frontend -```bash -# Nginx + GeoIP2 dynamic module -sudo apt update -sudo apt install -y nginx libnginx-mod-http-geoip2 - -# MaxMind GeoIP database updater -sudo apt install -y geoipupdate +Serves the pre-built React SPA from `/var/www/rythmify/`. No backend proxying on this domain. The frontend app calls `https://rythmify-back.duckdns.org/api/v1/...` directly — all API traffic goes to the backend domain. -# Certbot for SSL certificate issuance and renewal -sudo apt install -y certbot python3-certbot-nginx -``` +### rythmify-back.duckdns.org — Backend -`libnginx-mod-http-geoip2` drops a module config file into `/etc/nginx/modules-enabled/` that loads the GeoIP2 module automatically — no manual `load_module` directive is needed in `nginx.conf`. +Reverse proxy to the Node.js backend container on `localhost:8080` and Grafana on `localhost:3000`. All API, WebSocket, and monitoring traffic enters through this domain. --- -## GeoIP Setup - -The backend config resolves client IPs using MaxMind's free **GeoLite2-Country** database. A free MaxMind account is required. - -**1. Create a MaxMind account** - -Sign up at https://www.maxmind.com/en/geolite2/signup and generate a License Key under **My Account → Manage License Keys**. - -**2. Configure `/etc/GeoIP.conf`** - -```ini -AccountID YOUR_ACCOUNT_ID -LicenseKey YOUR_LICENSE_KEY -EditionIDs GeoLite2-Country -DatabaseDirectory /var/lib/GeoIP -``` - -Replace `YOUR_ACCOUNT_ID` and `YOUR_LICENSE_KEY` with your actual values. - -**3. Download the database** - -```bash -sudo geoipupdate -``` - -**4. Verify the file exists** - -```bash -ls -lh /var/lib/GeoIP/GeoLite2-Country.mmdb -``` +## Location Blocks -Expected: a file of approximately 6–7 MB. If the file is missing, Nginx will fail to start with an error such as: +### rythmify-back.duckdns.org — HTTPS server (port 443) -``` -geoip2: unable to open database "/var/lib/GeoIP/GeoLite2-Country.mmdb" -``` - ---- - -## Deployment - -**1. Copy configs to `sites-available`** - -```bash -sudo cp rythmify-back.duckdns.org.conf /etc/nginx/sites-available/ -sudo cp rythmify.duckdns.org.conf /etc/nginx/sites-available/ -``` - -**2. Enable the sites** - -```bash -sudo ln -s /etc/nginx/sites-available/rythmify-back.duckdns.org.conf \ - /etc/nginx/sites-enabled/ -sudo ln -s /etc/nginx/sites-available/rythmify.duckdns.org.conf \ - /etc/nginx/sites-enabled/ -``` +| Location | Proxy target | Purpose | +|---|---|---| +| `/health` | `http://localhost:8080` | Liveness probe used by Blackbox exporter — no WebSocket headers | +| `/api/` | `http://localhost:8080` | Explicit REST API path — full header set including WebSocket upgrade (see note) | +| `/grafana/` | `http://localhost:3000/` | Grafana dashboard — proxied internally, not exposed on its own port | +| `/` | `http://localhost:8080` | All API + Socket.IO WebSocket traffic — full header set including X-Country-Code | -Disable the default Nginx site if it is enabled (it conflicts with the new configs): +**Why the explicit `/api/` block?** -```bash -sudo rm -f /etc/nginx/sites-enabled/default -``` +Nginx uses longest-prefix matching. Declaring `/api/` explicitly makes routing intent clear and allows per-path tuning (rate limits, timeouts) in future without touching the catch-all `/` block. WebSocket upgrade headers are kept on `/api/` so Socket.IO clients using a custom path (e.g. `/api/socket.io/`) are not broken. -**3. Test the configuration** +**Required headers on `/` and `/api/`:** -```bash -sudo nginx -t +```nginx +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade"; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Country-Code $geoip2_data_country_code; +proxy_cache_bypass $http_upgrade; ``` -Both lines must read `syntax is ok` and `test is successful`. Fix any reported errors before continuing. +### rythmify.duckdns.org — HTTPS server (port 443) -**4. Reload Nginx** +| Location | Behaviour | Purpose | +|---|---|---| +| `/` | `try_files $uri $uri/ /index.html` | React SPA fallback — unknown paths serve `index.html` so client-side routing works | -```bash -sudo systemctl reload nginx -``` +No proxy blocks. No `/nginx_status`. This domain is static-files-only. --- -## X-Country-Code Header +## GeoIP2 -Nginx resolves each request's client IP against the **MaxMind GeoLite2-Country** database using the `ngx_http_geoip2` module. The `geoip2` block at the top of `rythmify-back.duckdns.org.conf` declares the variable: +The `geoip2` directive appears at the **http context level** (above all `server {}` blocks) in `rythmify-back.duckdns.org.conf`: ```nginx geoip2 /var/lib/GeoIP/GeoLite2-Country.mmdb { @@ -127,7 +71,7 @@ geoip2 /var/lib/GeoIP/GeoLite2-Country.mmdb { } ``` -That variable is then injected as a request header before the request is forwarded to the backend: +This resolves the client IP to an ISO 3166-1 alpha-2 country code. The variable is injected as a request header on all proxied backend requests: ```nginx proxy_set_header X-Country-Code $geoip2_data_country_code; @@ -139,36 +83,35 @@ The backend reads it as: const country = req.headers['x-country-code']; // e.g. "EG", "SA", "US" ``` -The value is an **ISO 3166-1 alpha-2** two-letter code. Examples: `EG` (Egypt), `SA` (Saudi Arabia), `US` (United States), `GB` (United Kingdom). The variable resolves to an empty string for private or unroutable IP addresses (e.g. `127.0.0.1`, `10.x.x.x`, `172.16.x.x`). +The value is empty for private or unroutable IPs (`127.0.0.1`, `10.x.x.x`, `172.16.x.x`). The header cannot be spoofed — Nginx overwrites any client-supplied value before forwarding. -Because the header is set by Nginx at the proxy layer, it **cannot be spoofed** by clients — any `X-Country-Code` value sent in the original request is overwritten by Nginx before it reaches the backend. +**Setup:** ---- +```bash +# Install module and updater +sudo apt install -y libnginx-mod-http-geoip2 geoipupdate -## Grafana Proxy +# Configure /etc/GeoIP.conf with your MaxMind account credentials +# Sign up free at https://www.maxmind.com/en/geolite2/signup +AccountID YOUR_ACCOUNT_ID +LicenseKey YOUR_LICENSE_KEY +EditionIDs GeoLite2-Country +DatabaseDirectory /var/lib/GeoIP -Grafana runs inside Docker on port `3000`. It is not exposed publicly. Instead, the backend server block proxies `/grafana/` to `http://localhost:3000/`: +# Download the database +sudo geoipupdate -```nginx -location /grafana/ { - proxy_pass http://localhost:3000/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; -} +# Verify (expect ~6-7 MB file) +ls -lh /var/lib/GeoIP/GeoLite2-Country.mmdb ``` -Access Grafana at: **https://rythmify-back.duckdns.org/grafana/** - -No SSH tunnel or port forwarding is needed. +`libnginx-mod-http-geoip2` auto-loads the module via `/etc/nginx/modules-enabled/` — no manual `load_module` directive is needed in `nginx.conf`. --- -## Nginx stub_status +## Nginx stub_status (Internal — 172.17.0.1:8081) -`nginx_exporter` (in `docker-compose.monitoring.yml`) scrapes Nginx connection metrics from a dedicated `stub_status` endpoint. The endpoint is defined in `rythmify-back.duckdns.org.conf` as a server block bound to the Docker bridge host IP: +`nginx_exporter` in `docker-compose.monitoring.yml` scrapes Nginx connection metrics from a dedicated internal endpoint defined in `rythmify-back.duckdns.org.conf`: ```nginx server { @@ -177,9 +120,7 @@ server { location /nginx_status { stub_status; - allow 127.0.0.1; - allow 172.16.0.0/12; - deny all; + allow all; } location / { @@ -188,7 +129,7 @@ server { } ``` -`172.17.0.1` is the Docker bridge host IP — it is reachable from Docker containers but not from the public internet or from the VM loopback. `nginx_exporter` in the monitoring compose stack is configured to scrape `http://172.17.0.1:8081/nginx_status`. +`172.17.0.1` is the Docker bridge host IP — reachable from Docker containers but not from the public internet. `allow all` is safe because the listen address already limits access to bridge-network traffic. `nginx_exporter` is configured to scrape `http://172.17.0.1:8081/nginx_status`. Verify from the VM: @@ -209,16 +150,16 @@ Reading: 0 Writing: 1 Waiting: 2 ## SSL -SSL certificates are managed by **Certbot** and must **not** be committed to this repository. +Certificates are managed by **Certbot** and must **not** be committed to this repository. -The configs reference certificates at: +Both configs reference: ``` /etc/letsencrypt/live/rythmify.duckdns.org/fullchain.pem /etc/letsencrypt/live/rythmify.duckdns.org/privkey.pem ``` -The certificate covers both `rythmify.duckdns.org` and `rythmify-back.duckdns.org` as Subject Alternative Names. Issue it with: +The certificate covers both domains as Subject Alternative Names: ```bash sudo certbot certonly --nginx \ @@ -226,28 +167,89 @@ sudo certbot certonly --nginx \ -d rythmify-back.duckdns.org ``` -Certbot installs a systemd timer (`certbot.timer`) for automatic renewal. Verify it is active: +Certbot installs a systemd timer for automatic renewal. Verify it is active: ```bash sudo systemctl status certbot.timer ``` -The files `options-ssl-nginx.conf` and `ssl-dhparams.pem` referenced in both configs are created automatically by Certbot when the first certificate is issued. +`options-ssl-nginx.conf` and `ssl-dhparams.pem` (referenced in both configs) are created automatically by Certbot on first issuance. + +--- + +## Prerequisites + +```bash +sudo apt update +sudo apt install -y nginx libnginx-mod-http-geoip2 geoipupdate certbot python3-certbot-nginx +``` + +--- + +## Deployment + +**1. Copy configs to `sites-available`** + +```bash +sudo cp nginx/rythmify-back.duckdns.org.conf /etc/nginx/sites-available/ +sudo cp nginx/rythmify.duckdns.org.conf /etc/nginx/sites-available/ +``` + +**2. Enable the sites** + +```bash +sudo ln -sf /etc/nginx/sites-available/rythmify-back.duckdns.org.conf \ + /etc/nginx/sites-enabled/ +sudo ln -sf /etc/nginx/sites-available/rythmify.duckdns.org.conf \ + /etc/nginx/sites-enabled/ + +# Disable the default site if still enabled +sudo rm -f /etc/nginx/sites-enabled/default +``` + +**3. Create the frontend static directory** + +```bash +sudo mkdir -p /var/www/rythmify +sudo chown -R www-data:www-data /var/www/rythmify +``` + +**4. Test** + +```bash +sudo nginx -t +``` + +Both lines must read `syntax is ok` and `test is successful`. + +**5. Reload** + +```bash +sudo systemctl reload nginx +``` + +**Applying updates from the repo:** + +```bash +cd ~/devops && git pull +sudo cp nginx/rythmify-back.duckdns.org.conf /etc/nginx/sites-available/ +sudo cp nginx/rythmify.duckdns.org.conf /etc/nginx/sites-available/ +sudo nginx -t && sudo systemctl reload nginx +``` --- ## Updating the GeoIP Database -MaxMind releases updated GeoLite2-Country databases approximately twice per month. To refresh the local copy manually: +MaxMind releases updated databases approximately twice per month: ```bash sudo geoipupdate sudo systemctl reload nginx ``` -To automate this, create a cron job at `/etc/cron.d/geoipupdate`: +Automate with a cron job at `/etc/cron.d/geoipupdate`: ```cron -# Refresh MaxMind GeoLite2-Country on the 1st and 15th of each month at 03:00 0 3 1,15 * * root /usr/bin/geoipupdate && systemctl reload nginx ``` diff --git a/nginx/rythmify-back.duckdns.org.conf b/nginx/rythmify-back.duckdns.org.conf index b79eb2c..89d872c 100644 --- a/nginx/rythmify-back.duckdns.org.conf +++ b/nginx/rythmify-back.duckdns.org.conf @@ -31,6 +31,7 @@ server { ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # ── /health — liveness probe used by Blackbox exporter ────────────────────── + # No WebSocket headers — this is a plain HTTP probe. location /health { proxy_pass http://localhost:8080; proxy_http_version 1.1; @@ -41,6 +42,27 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } + # ── /api/ — explicit REST API path ────────────────────────────────────────── + # Mirrors the / block so REST calls to /api/v1/... are handled explicitly. + # WebSocket upgrade headers are kept so Socket.IO clients using a custom + # path (e.g. /api/socket.io) are not broken. + location /api/ { + proxy_pass http://localhost:8080; + proxy_http_version 1.1; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_set_header X-Country-Code $geoip2_data_country_code; + + proxy_cache_bypass $http_upgrade; + } + # ── /grafana/ — Grafana dashboard (proxied from port 3000) ────────────────── location /grafana/ { proxy_pass http://localhost:3000/; @@ -79,17 +101,16 @@ server { # Binds to the Docker bridge host IP (172.17.0.1) on a dedicated port 8081. # nginx_exporter in docker-compose.monitoring.yml scrapes: # http://172.17.0.1:8081/nginx_status -# Binding to 172.17.0.1 limits access to Docker bridge traffic — this address -# is not reachable from public interfaces or from the VM loopback (127.0.0.1). +# Binding to 172.17.0.1 means this address is unreachable from the public +# internet — only Docker containers on the bridge network can reach it, so +# allow all is safe. server { listen 172.17.0.1:8081; server_name _; location /nginx_status { stub_status; - allow 127.0.0.1; - allow 172.16.0.0/12; - deny all; + allow all; } location / { diff --git a/nginx/rythmify.duckdns.org.conf b/nginx/rythmify.duckdns.org.conf index 03e20e6..acd9db4 100644 --- a/nginx/rythmify.duckdns.org.conf +++ b/nginx/rythmify.duckdns.org.conf @@ -1,3 +1,4 @@ +# ── HTTP → HTTPS redirect ────────────────────────────────────────────────────── server { listen 80; listen [::]:80; @@ -6,6 +7,12 @@ server { return 301 https://$host$request_uri; } +# ── HTTPS — frontend static files (React SPA) ───────────────────────────────── +# +# Serves pre-built assets from /var/www/rythmify (deployed by the Frontend CD +# pipeline via rsync). All unknown routes fall back to index.html so +# client-side routing works. API calls go directly to +# rythmify-back.duckdns.org — no proxy on this domain. server { listen 443 ssl; listen [::]:443 ssl; @@ -16,59 +23,9 @@ server { include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - root /var/www/rythmify; + root /var/www/rythmify; index index.html; - location /health { - proxy_pass http://localhost:8080/health; - - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # Backend API - location /api/ { - proxy_pass http://localhost:8080/api/; - - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # WebSocket support - location /socket.io/ { - proxy_pass http://localhost:8080/socket.io/; - - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - } - - # Handle missing trailing slash - location = /grafana { - return 301 /grafana/; - } - - # Grafana proxy - location /grafana/ { - proxy_pass http://localhost:3000/; - - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - - # Required for Grafana WebSockets - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - } - - # Frontend SPA location / { try_files $uri $uri/ /index.html; }