Files
2026-07-12 10:17:17 -04:00

3.3 KiB

Static Site Deployment via VPS Reverse Proxy

Deploying a static HTML/CSS/JS website through the VPS reverse proxy infrastructure.

Architecture

Internet → airrepairteam.graj-media.com (VPS nginx, SSL) 
           ↳ Tailscale → 100.93.253.36:3451 (home nginx, SSL duckdns)
               ↳ root /mnt/seagate8tb/Websites/AirRepairTeam

Step 1: Home server nginx config

Static sites use root + try_files, not proxy_pass. Create in /etc/nginx/sites-enabled/<name>:

server {
    listen <PORT> ssl;
    server_name grajmedia.duckdns.org;

    ssl_certificate /etc/letsencrypt/live/grajmedia.duckdns.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grajmedia.duckdns.org/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    root /mnt/seagate8tb/Websites/<ProjectName>;
    index index.html;

    # Clean URLs — /hvac serves /hvac/index.html
    location / {
        try_files $uri $uri/ $uri/index.html =404;
    }

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
    }
}

Port selection: use the next available in the 344x/345x range. Check existing ports:

grep -r "listen" /etc/nginx/sites-enabled/ | grep -oP '\d{4}' | sort -n

Step 2: File permissions

nginx workers run as uid 911. Static files must be world-readable:

chmod -R a+rX /mnt/seagate8tb/Websites/<ProjectName>

Step 3: Test and reload

sudo nginx -t && sudo nginx -s reload

Verify locally:

curl -sk -o /dev/null -w "%{http_code}" https://localhost:<PORT>/
curl -sk -o /dev/null -w "%{http_code}" https://localhost:<PORT>/hvac/

Step 4: VPS nginx config

Create /etc/nginx/sites-enabled/<name> on the VPS. Start with HTTP-only (port 80) for certbot:

server {
    server_name <subdomain>.graj-media.com;
    client_max_body_size 50M;

    location / {
        proxy_pass https://100.93.253.36:<PORT>;
        proxy_http_version 1.1;
        proxy_set_header Host grajmedia.duckdns.org;
        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_ssl_verify off;
        proxy_read_timeout 86400;
        proxy_buffering off;
    }

    listen 80;
}

Step 5: SSL via certbot

sudo nginx -t && sudo nginx -s reload
sudo certbot --nginx -d <subdomain>.graj-media.com --non-interactive --agree-tos -m admin@graj-media.com

Certbot issues a separate cert per subdomain and adds SSL to the config. No need for --expand.

Step 6: Verify

curl -sk -o /dev/null -w '%{http_code}' https://<subdomain>.graj-media.com/
curl -sk https://<subdomain>.graj-media.com/ | grep -o '<title>[^<]*</title>'

Step 7: Landing page

Add a link on graj-media.com by editing /etc/nginx/sites-enabled/landing and inserting before </body>:

<a href=https://<subdomain>.graj-media.com>Service Name</a>

Pitfalls

  • 403 from home nginx: File permissions too restrictive. Fix: chmod -R a+rX /path/to/site.
  • Host header mismatch: VPS must send Host: grajmedia.duckdns.org (home nginx server_name), not the VPS domain.
  • Port already in use: Check with grep -r "listen" /etc/nginx/sites-enabled/.