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.
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.
Hosting a static site on your own VPS gives you:
For developers, it also means the same server that hosts your API can host your frontend on a subdomain, keeping everything in one place.
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 |
| 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 |
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
Create the web root:
sudo mkdir -p /var/www/mysite
sudo chown ubuntu:www-data /var/www/mysite
sudo chmod 755 /var/www/mysite
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/
# 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
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/
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.
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.
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";
}
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
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.
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:
After this, static assets are served from the edge — your origin server only receives cache misses and revalidation requests.
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.
# 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/"
| 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