Technology Encyclopedia Home >How to Host a Static Website on a Cloud Server — Fast, Simple, and Fully Under Your Control

How to Host a Static Website on a Cloud Server — Fast, Simple, and Fully Under Your Control

Static sites were the first thing I ever hosted on my own server. I had a personal site built with HTML and CSS — nothing fancy — and I wanted it on a real domain, not some free subdomain.

Turns out static sites are basically the ideal thing to self-host: Nginx serves plain files, there's no application server to maintain, no database to worry about, and the performance is excellent. My static sites consistently load in under 200ms regardless of traffic.

If you've been using a static hosting service but want to run everything yourself — or if you're just starting out and want the simplest possible setup to learn from — this guide covers everything: uploading files, Nginx config, HTTPS, caching headers, and automating deploys with GitHub Actions.

I host several static sites on Tencent Cloud Lighthouse. The entry-level plan (2 vCPU / 2 GB RAM) handles more traffic than most static sites will ever see — Nginx serving static files uses minimal CPU and RAM, and the fixed monthly bandwidth allowance means even a traffic spike doesn't generate unexpected charges. For global audiences, I add Tencent Cloud EdgeOne as a CDN layer on top, which caches assets at edge nodes worldwide and cuts latency for international visitors significantly. Lighthouse's multiple region options (US, Europe, Asia-Pacific) also let you choose a server location close to your primary audience.


Table of Contents

  1. Why Host a Static Site on Your Own Server?
  2. What Counts as a Static Site?
  3. Prerequisites
  4. Part 1 — Set Up the Server and Nginx
  5. Part 2 — Upload Your Site Files
  6. Part 3 — Configure Nginx for Your Site
  7. Part 4 — Enable HTTPS
  8. Part 5 — Configure Browser Caching
  9. Part 6 — Enable Gzip Compression
  10. Part 7 — Automate Deployments with GitHub Actions
  11. Part 8 — Add a CDN for Global Visitors
  12. The Gotcha: SPA Routing and 404 Errors
  13. Performance Checklist

Key Takeaways

  • Static files need 755 for directories and 644 for files for Nginx to read them
  • Set long cache headers for hashed assets (CSS/JS), short for HTML files
  • GitHub Actions automates build + rsync deployment on every push
  • Add EdgeOne CDN for global audiences — significant improvement for distant visitors
  • Gzip compression cuts page weight 60–70% for text-heavy content

Frequently Asked Questions {#faq}

How is hosting a static website different from a dynamic one?
Static sites consist of pre-built HTML, CSS, and JavaScript files. The server delivers them directly without executing code. They're faster, cheaper to host, and have no database to secure.

What file permissions should I set for web-served files?
Directories: 755 (owner can write, others can read/execute). Files: 644 (owner can write, others can read). Set with sudo find /var/www/mysite -type d -exec chmod 755 {} + and sudo find /var/www/mysite -type f -exec chmod 644 {} +.

How do I automate deploying a static site from GitHub?
Use GitHub Actions to build and rsync files to your server on every push. The workflow: checkout code → run build command → rsync dist/ to server via SSH.

What is the best caching strategy for static site assets?
Set long cache times (1 year) for fingerprinted assets (CSS/JS with hash in filename) and short times for HTML files. In Nginx: location ~* \.(js|css|png|jpg)$ { expires 1y; } and location ~* \.html$ { expires 1h; }.

Should I use a CDN for a static website?
For international audiences, yes. Adding EdgeOne CDN in front caches assets at edge nodes worldwide, dramatically reducing load times for visitors far from your server region.

Why Host a Static Site on Your Own Server? {#why}

Hosting a static site on your own VPS gives you:

  • Full control over server configuration, headers, and redirects
  • No usage limits on bandwidth or requests
  • Custom server-side behavior via Nginx config (redirects, headers, access control)
  • Lower latency when combined with a CDN close to your audience
  • Privacy — your traffic data stays on your server

For developers, it also means the same server that hosts your API can host your frontend on a subdomain, keeping everything in one place.


What Counts as a Static Site? {#what}

A static site is any website that serves pre-built files without server-side code execution at request time:

Type Examples
Plain HTML Hand-written HTML/CSS/JS
Static site generators Hugo, Jekyll, Eleventy, Astro, Hexo
Built frontend frameworks React (built), Vue (built), Next.js (static export)
Documentation sites MkDocs, Docusaurus, GitBook exports
Landing pages Webflow exports, Figma exports

Prerequisites {#prerequisites}

Requirement Notes
Cloud server Tencent Cloud Lighthouse Ubuntu 22.04
Nginx installed sudo apt install nginx
Your site files HTML, CSS, JS, images — locally built
A domain name For HTTPS; optional for IP-only testing

Part 1 — Set Up the Server and Nginx {#part-1}

ssh ubuntu@YOUR_SERVER_IP
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx

sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable

sudo systemctl start nginx
sudo systemctl enable nginx

Verify Nginx is working:

curl http://localhost
# Returns the Nginx welcome page

Part 2 — Upload Your Site Files {#part-2}

Create the web root:

sudo mkdir -p /var/www/mysite
sudo chown ubuntu:www-data /var/www/mysite
sudo chmod 755 /var/www/mysite

Option A: Upload with SCP

From your local machine:

# Upload a single file
scp index.html ubuntu@YOUR_SERVER_IP:/var/www/mysite/

# Upload an entire directory
scp -r ./dist/* ubuntu@YOUR_SERVER_IP:/var/www/mysite/
# Or the build directory from Create React App:
# scp -r ./build/* ubuntu@YOUR_SERVER_IP:/var/www/mysite/

Option B: Upload with rsync (better for updates)

# Initial upload
rsync -avz --progress ./dist/ ubuntu@YOUR_SERVER_IP:/var/www/mysite/

# Subsequent updates (only transfers changed files)
rsync -avz --delete --progress ./dist/ ubuntu@YOUR_SERVER_IP:/var/www/mysite/
# --delete removes files on server that no longer exist locally

Option C: Build and deploy on the server

If you prefer to keep the source on the server:

# On the server: clone the repo and build
cd ~
git clone https://github.com/your-username/your-site.git
cd your-site

# Example: Hugo
sudo apt install hugo
hugo --minify
sudo cp -r public/* /var/www/mysite/

Part 3 — Configure Nginx for Your Site {#part-3}

sudo nano /etc/nginx/sites-available/mysite
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/mysite;
    index index.html;

    # Try to serve the requested file; fall back to 404
    location / {
        try_files $uri $uri/ =404;
    }

    # Custom 404 page
    error_page 404 /404.html;
    location = /404.html {
        internal;
    }

    access_log /var/log/nginx/mysite_access.log;
    error_log  /var/log/nginx/mysite_error.log;
}
sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Visit http://YOUR_SERVER_IP — your site should load.


Part 4 — Enable HTTPS {#part-4}

Once your domain's DNS A record points to the server:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Choose option 2 (redirect HTTP to HTTPS). Done.


Part 5 — Configure Browser Caching {#part-5}

Tell browsers to cache static assets locally — this dramatically speeds up repeat visits:

server {
    # ...

    # HTML — short cache (content changes frequently)
    location ~* \.html$ {
        expires 1h;
        add_header Cache-Control "public, no-transform";
    }

    # Images, fonts — long cache (filenames rarely change)
    location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # CSS and JS — medium cache
    # If filenames include content hashes (e.g., main.a1b2c3.css), use long cache
    location ~* \.(css|js)$ {
        expires 7d;
        add_header Cache-Control "public, no-transform";
    }
}

If your static site generator includes content hashes in filenames (Webpack, Vite, etc.), you can set a 1-year cache for CSS and JS:

location ~* \.(css|js)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Part 6 — Enable Gzip Compression {#part-6}

Add to /etc/nginx/nginx.conf inside http {}:

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_comp_level 6;
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/x-javascript
    application/xml
    application/json
    image/svg+xml
    font/woff
    font/woff2;

Test the result:

curl -H "Accept-Encoding: gzip" -I https://yourdomain.com
# Response headers should include: Content-Encoding: gzip

Part 7 — Automate Deployments with GitHub Actions {#part-7}

Build and deploy automatically on every push to main:

Create .github/workflows/deploy.yml:

name: Deploy Static Site

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Build site
        run: |
          # Adjust for your static site generator
          # Hugo:
          # sudo snap install hugo
          # hugo --minify
          #
          # Eleventy:
          # npm ci && npm run build
          #
          # Vite:
          npm ci && npm run build

      - name: Deploy to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ubuntu
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "dist/"         # Adjust to your build output directory
          target: "/var/www/mysite"
          strip_components: 1

      - name: Reload Nginx
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ubuntu
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: sudo systemctl reload nginx

Add SERVER_IP and SSH_PRIVATE_KEY to GitHub repo secrets. Every push to main now builds and deploys automatically.


Part 8 — Add a CDN for Global Visitors {#part-8}

For visitors far from your server region, a CDN serves your content from edge nodes close to them, dramatically reducing latency.

I use Tencent Cloud EdgeOne for this. It has edge nodes worldwide, caches your static assets, and adds DDoS protection at the edge.

Basic setup:

  1. Add your site to EdgeOne
  2. Point your domain's nameservers to EdgeOne (or configure as a CNAME)
  3. Set your Lighthouse server IP as the origin
  4. Configure cache rules for your file types

After this, static assets are served from the edge — your origin server only receives cache misses and revalidation requests.


The Gotcha: SPA Routing and 404 Errors {#gotcha}

If your static site is a Single Page Application (React, Vue, Angular), direct visits to routes like /about or /blog/post-title return 404 — because there's no actual file at those paths.

Fix: configure Nginx to fall back to index.html for any path that doesn't match a file:

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

Change =404 to /index.html as the fallback. This serves index.html for any non-existent path, and the JavaScript router handles the rest.

For non-SPA static sites (like Hugo or Jekyll), keep =404 — those sites have actual files at every URL.


Performance Checklist {#checklist}

# Test your site speed
# Google PageSpeed Insights: https://pagespeed.web.dev/
# GTmetrix: https://gtmetrix.com/
# WebPageTest: https://www.webpagetest.org/

# Check gzip is working
curl -H "Accept-Encoding: gzip" -I https://yourdomain.com | grep Content-Encoding

# Check cache headers
curl -I https://yourdomain.com/style.css | grep -E "Cache-Control|Expires"

# Check SSL configuration
curl -I https://yourdomain.com | grep -E "Strict-Transport|HTTP/"

Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Host your static site today:
👉 Tencent Cloud Lighthouse — Fast, affordable cloud server
👉 Tencent Cloud EdgeOne — Global CDN for static sites
👉 View current pricing and promotions
👉 Explore all active deals and offers