Why Self-Host Instead of Netlify?
Netlify offers excellent developer experience for static sites and JAMstack applications, but costs can increase with bandwidth, build minutes, and team members. Self-hosting gives you predictable costs, unlimited bandwidth, and full control over your deployment pipeline.
Netlify Feature Replacements
| Netlify Feature | Self-Hosted Alternative |
|---|---|
| CDN | Cloudflare (free tier) |
| Build & Deploy | GitHub Actions + rsync/Docker |
| Forms | Formspree, or custom API endpoint |
| Functions | Express/Fastify API or Cloudflare Workers |
| Identity | Logto, Authentik, or Auth0 |
| Analytics | Plausible or Umami (self-hosted) |
| Split Testing | Nginx split_clients module |
| Redirects | Nginx rewrite rules |
Step 1: Set Up Nginx for Static Hosting
sudo apt install -y nginx
# Configure virtual host
sudo tee /etc/nginx/sites-available/mysite << CONF
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
# Handle SPA routing
location / {
try_files \$uri \$uri/ \$uri.html /index.html;
}
# Custom 404 page
error_page 404 /404.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
CONF
sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# SSL
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Step 2: Convert _redirects to Nginx
# Netlify _redirects file:
# /old-page /new-page 301
# /api/* https://api.example.com/:splat 200
# Nginx equivalent:
location = /old-page {
return 301 /new-page;
}
location /api/ {
proxy_pass https://api.example.com/;
}
Step 3: CI/CD with GitHub Actions
name: Deploy Static Site
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Deploy to server
uses: burnett01/rsync-deployments@7
with:
switches: -avz --delete
path: dist/
remote_path: /var/www/example.com/
remote_host: ${{ secrets.SERVER_IP }}
remote_user: deploy
remote_key: ${{ secrets.SSH_KEY }}
Step 4: CDN with Cloudflare
# 1. Add your domain to Cloudflare (free plan)
# 2. Update nameservers at your registrar
# 3. Enable proxy (orange cloud) for A records
# 4. Configure caching rules:
# - Cache Level: Standard
# - Browser Cache TTL: 1 year for static assets
# - Always Online: Enabled
# 5. Enable Auto Minify for HTML, CSS, JS
Step 5: Replace Netlify Functions
# Convert serverless functions to Express API
# Netlify function (functions/hello.js):
exports.handler = async (event) => ({
statusCode: 200,
body: JSON.stringify({ message: "Hello" })
});
# Express equivalent:
app.get("/api/hello", (req, res) => {
res.json({ message: "Hello" });
});
# Deploy as a separate service or on the same server
Best Practices
- Use Cloudflare for free CDN and DDoS protection
- Set up GitHub Actions for automated deployments on push
- Configure proper caching headers for optimal performance
- Use a dedicated deploy user with limited permissions
- Set up monitoring with Uptime Kuma for availability alerts