Skip to content

Nginx — Coaching app and documentation

Use these examples on the same server as the app (Next.js + pm2) and the MkDocs site at docs.diasantos.com.

Files in the repository

The same content lives under documentation/examples/nginx/ so you can copy it to /etc/nginx/sites-available/ without using the browser.

Enable a site on nginx (Debian / Ubuntu)

Typical flow per domain (replace docs.diasantos.com with your chosen filename; for the app use e.g. coaching.app or your real domain).

1. Install nginx (if missing)

sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx

2. Create the virtual host file

Option A — copy the example from the repo (adjust the clone path):

sudo cp /var/www/coaching/documentation/examples/nginx/docs.diasantos.com.conf.example \
  /etc/nginx/sites-available/docs.diasantos.com

Option B — create/edit manually with your preferred editor:

sudo vim /etc/nginx/sites-available/docs.diasantos.com
# or: sudo nano /etc/nginx/sites-available/docs.diasantos.com

Paste the example content (sections below or the .example file), save and exit (:wq in vim, Ctrl+O / Ctrl+X in nano).

Before the first HTTPS

If you do not have certificates yet, you can start with only the listen 80 block and server_name, serve over HTTP, obtain a certificate with certbot, then add the listen 443 ssl block or let certbot edit the file for you. The examples assume Let's Encrypt paths already exist.

nginx only loads what is under sites-enabled/. Link to the file in sites-available/:

sudo ln -sf /etc/nginx/sites-available/docs.diasantos.com \
  /etc/nginx/sites-enabled/docs.diasantos.com
  • -f replaces the link if it already exists (handy when re-running).
  • Repeat the same pattern for the app, e.g. /etc/nginx/sites-available/coaching.appsites-enabled/coaching.app.

4. Conflict with the default site (optional)

On fresh installs, /etc/nginx/sites-enabled/default may bind port 80. If you need that port for your server_name, disable the default:

sudo rm /etc/nginx/sites-enabled/default
# or: sudo unlink /etc/nginx/sites-enabled/default

5. Validate and apply

sudo nginx -t

If you see syntax is ok and test is successful:

sudo systemctl reload nginx

Confirm in the browser (https://docs.diasantos.com) or with curl -I https://docs.diasantos.com.

6. Inspect errors if something fails

sudo journalctl -u nginx -n 50 --no-pager

Coaching (reverse proxy for Next.js)

Assumes next start or equivalent on 127.0.0.1:3000 (typical pm2 port). Adjust server_name, certificate paths, and proxy_pass if the port differs.

# Example: HTTPS reverse proxy for the Next.js app (pm2 on 127.0.0.1:3000).
#
# On Debian/Ubuntu:
#   sudo cp coaching.app.conf.example /etc/nginx/sites-available/coaching.app
#   sudo ln -sf /etc/nginx/sites-available/coaching.app /etc/nginx/sites-enabled/
#   sudo nginx -t && sudo systemctl reload nginx
#
# Replace app.diasantos.com with your application domain.
# Certificates: certbot certonly --nginx -d app.diasantos.com

server {
    listen 80;
    listen [::]:80;
    server_name app.diasantos.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name app.diasantos.com;

    ssl_certificate     /etc/letsencrypt/live/app.diasantos.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.diasantos.com/privkey.pem;

    # Optional: tighten TLS (match your policy)
    # ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 25m;

    # POST /api/admin/deploy — long-lived SSE (`next build` can exceed 5–10 min with sparse bytes).
    # Short `proxy_read_timeout` closes the upstream idle connection → browser **net::ERR_INCOMPLETE_CHUNKED_ENCODING**.
    location = /api/admin/deploy {
        proxy_pass http://127.0.0.1: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;
        proxy_buffering off;
        proxy_cache off;
        gzip off;
        proxy_read_timeout 7200s;
        proxy_send_timeout 7200s;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        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_cache_bypass $http_upgrade;
        proxy_read_timeout 300;
    }
}

The app also sends periodic SSE comment keepalives during deploy steps so proxies see steady upstream reads; nginx must still allow long proxy_read_timeout on this route.

Documentation (static MkDocs at docs.diasantos.com)

After mkdocs build, output is under documentation/site/. The root below must point to that directory on the server.

# Example: static MkDocs site (output of: cd documentation && mkdocs build).
#
# On Debian/Ubuntu:
#   sudo cp docs.diasantos.com.conf.example /etc/nginx/sites-available/docs.diasantos.com
#   sudo ln -sf /etc/nginx/sites-available/docs.diasantos.com /etc/nginx/sites-enabled/
#   sudo nginx -t && sudo systemctl reload nginx
#
# Adjust root to the real clone path (APP_DIR/documentation/site).
# Certificates: certbot certonly --nginx -d docs.diasantos.com

server {
    listen 80;
    listen [::]:80;
    server_name docs.diasantos.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name docs.diasantos.com;

    ssl_certificate     /etc/letsencrypt/live/docs.diasantos.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/docs.diasantos.com/privkey.pem;

    root /var/www/coaching/documentation/site;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Optional: short cache for static assets produced by MkDocs
    location ~* \.(?:css|js|woff2?|png|jpg|jpeg|gif|svg|ico)$ {
        try_files $uri =404;
        expires 7d;
        add_header Cache-Control "public, max-age=604800, immutable";
    }
}

Certificates with Certbot (Let's Encrypt)

Use Certbot's nginx plugin to issue and renew certificates without unnecessary downtime (challenge handled by nginx).

Prerequisites

  • DNS: A (and optionally AAAA) records for app.diasantos.com and docs.diasantos.com pointing to this server's public IP (replace with your real domains).
  • Port 80 reachable from the Internet (Let's Encrypt uses HTTP-01 by default with --nginx).
  • nginx running with at least the HTTP (port 80) block and correct server_name before you request HTTPS — if the file references ssl_certificate paths that do not exist yet, nginx -t fails; in that case start with listen 80 only (as in the activation section warning), obtain the certificate, then add or uncomment the 443 blocks from the examples.

Install Certbot

sudo apt update
sudo apt install -y certbot python3-certbot-nginx

With both virtual hosts enabled in sites-enabled and HTTP answering for each server_name:

# App (Next.js reverse proxy)
sudo certbot --nginx -d app.diasantos.com

# Documentation (static MkDocs)
sudo certbot --nginx -d docs.diasantos.com

Certbot asks for email, terms acceptance, and whether to redirect HTTP→HTTPS (equivalent to the return 301 lines in the examples). It usually adjusts your server { ... } blocks to use fullchain.pem and privkey.pem under /etc/letsencrypt/live/<domain>/.

Keep in sync with the examples on this page

If you already pasted the full example blocks (proxy, root, headers), back up first (sudo cp …/sites-available/… /tmp/) and after Certbot run sudo nginx -t. If Certbot oversimplifies a block (for example drops proxy_set_header on the app), restore those lines from the backup while keeping the ssl_certificate paths Certbot configured.

One certificate for both names (optional)

Useful if you want a single SAN certificate for both hosts:

sudo certbot --nginx -d app.diasantos.com -d docs.diasantos.com

Then both HTTPS server { … } blocks must reference the same fullchain.pem / privkey.pem pair (usually the first -d name defines the directory under /etc/letsencrypt/live/). The separate-domain examples assume one certificate per site (one certificate per server_name).

Verify and reload

sudo nginx -t && sudo systemctl reload nginx
sudo certbot certificates

Automatic renewal

The package usually installs a systemd timer that runs certbot renew:

sudo systemctl status certbot.timer
sudo certbot renew --dry-run

On Debian/Ubuntu, after a successful renewal the hook typically reloads nginx; if you customize configuration manually, confirm renew still works with dry-run.

Alternative: certificate only (plugin does not rewrite nginx)

If you prefer to obtain the certificate and edit nginx yourself:

sudo certbot certonly --nginx -d app.diasantos.com
sudo certbot certonly --nginx -d docs.diasantos.com

Then set ssl_certificate / ssl_certificate_key as in the examples (paths under /etc/letsencrypt/live/<domain>/).

Quick check when done

curl -fsSI https://app.diasantos.com | head -n 5
curl -fsSI https://docs.diasantos.com | head -n 5

If you do not use /var/www/certbot for /.well-known/acme-challenge/ and use manual webroot, align root with the webroot you pass to Certbot.

See also