CPU: 2 cores
RAM: 4GB
Disk: 60GB SSD
Bandwidth: 1TB/month
Cost: ~$5/month
CPU: 2 cores
RAM: 4GB
Disk: 60GB SSD
Bandwidth: 1TB/month
Cost: ~$5/month
CPU: 2 cores
RAM: 4GB
Disk: 60GB SSD
Bandwidth: 1TB/month
Cost: ~$5/month
┌─────────────────┐ │ Cloudflare │ │ (CDN + HTTPS) │ └────────┬────────┘ │ ┌────────▼────────┐ │ Nginx │ │ (Reverse Proxy)│ └───────┬┬───────┘ │ │ ┌──────────────────┘ └──────────────────┐ │ │ ┌──────▼──────┐ ┌────────▼────────┐ │ App 1 │ │ App 2 │ │ (:3000) │ │ (:3001) │ │ Main site │ │ Signal service │ └─────────────┘ └─────────────────┘ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ App 3 │ │ App 4 │ │ App 5 │ │ (:3099) │ │ Static │ │ Room UI │ │ Formatter │ │ Blog/Hugo │ │ Dashboard │ └─────────────┘ └─────────────┘ └─────────────┘
┌─────────────────┐ │ Cloudflare │ │ (CDN + HTTPS) │ └────────┬────────┘ │ ┌────────▼────────┐ │ Nginx │ │ (Reverse Proxy)│ └───────┬┬───────┘ │ │ ┌──────────────────┘ └──────────────────┐ │ │ ┌──────▼──────┐ ┌────────▼────────┐ │ App 1 │ │ App 2 │ │ (:3000) │ │ (:3001) │ │ Main site │ │ Signal service │ └─────────────┘ └─────────────────┘ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ App 3 │ │ App 4 │ │ App 5 │ │ (:3099) │ │ Static │ │ Room UI │ │ Formatter │ │ Blog/Hugo │ │ Dashboard │ └─────────────┘ └─────────────┘ └─────────────┘
┌─────────────────┐ │ Cloudflare │ │ (CDN + HTTPS) │ └────────┬────────┘ │ ┌────────▼────────┐ │ Nginx │ │ (Reverse Proxy)│ └───────┬┬───────┘ │ │ ┌──────────────────┘ └──────────────────┐ │ │ ┌──────▼──────┐ ┌────────▼────────┐ │ App 1 │ │ App 2 │ │ (:3000) │ │ (:3001) │ │ Main site │ │ Signal service │ └─────────────┘ └─────────────────┘ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ App 3 │ │ App 4 │ │ App 5 │ │ (:3099) │ │ Static │ │ Room UI │ │ Formatter │ │ Blog/Hugo │ │ Dashboard │ └─────────────┘ └─────────────┘ └─────────────┘
# Update system
sudo apt update && sudo apt upgrade -y # Install Nginx
sudo apt install nginx -y # Install Node.js (via nvm)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install --lts # Install Git
sudo apt install git -y
# Update system
sudo apt update && sudo apt upgrade -y # Install Nginx
sudo apt install nginx -y # Install Node.js (via nvm)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install --lts # Install Git
sudo apt install git -y
# Update system
sudo apt update && sudo apt upgrade -y # Install Nginx
sudo apt install nginx -y # Install Node.js (via nvm)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install --lts # Install Git
sudo apt install git -y
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y # Get certificate (replace with your domain)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y # Get certificate (replace with your domain)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y # Get certificate (replace with your domain)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
// server.js
const express = require('express');
const app = express();
const PORT = 3000; app.get('/api/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() });
}); app.listen(PORT, '127.0.0.1', () => { console.log(`App running on port ${PORT}`);
});
// server.js
const express = require('express');
const app = express();
const PORT = 3000; app.get('/api/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() });
}); app.listen(PORT, '127.0.0.1', () => { console.log(`App running on port ${PORT}`);
});
// server.js
const express = require('express');
const app = express();
const PORT = 3000; app.get('/api/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() });
}); app.listen(PORT, '127.0.0.1', () => { console.log(`App running on port ${PORT}`);
});
[Unit]
Description=Your Application
After=network.target [Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/your-app
ExecStart=/home/ubuntu/.nvm/versions/node/v22.22.1/bin/node server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production [Install]
WantedBy=multi-user.target
[Unit]
Description=Your Application
After=network.target [Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/your-app
ExecStart=/home/ubuntu/.nvm/versions/node/v22.22.1/bin/node server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production [Install]
WantedBy=multi-user.target
[Unit]
Description=Your Application
After=network.target [Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/your-app
ExecStart=/home/ubuntu/.nvm/versions/node/v22.22.1/bin/node server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production [Install]
WantedBy=multi-user.target
sudo systemctl enable yourapp
sudo systemctl start yourapp
sudo systemctl status yourapp # Should show "active (running)"
sudo systemctl enable yourapp
sudo systemctl start yourapp
sudo systemctl status yourapp # Should show "active (running)"
sudo systemctl enable yourapp
sudo systemctl start yourapp
sudo systemctl status yourapp # Should show "active (running)"
server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # Main app → port 3000 location / { 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; } # Second app → port 3001 location /signal/ { proxy_pass http://127.0.0.1:3001/; 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; }
}
server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # Main app → port 3000 location / { 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; } # Second app → port 3001 location /signal/ { proxy_pass http://127.0.0.1:3001/; 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; }
}
server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # Main app → port 3000 location / { 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; } # Second app → port 3001 location /signal/ { proxy_pass http://127.0.0.1:3001/; 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; }
}
sudo nginx -t # Check syntax
sudo systemctl reload nginx
sudo nginx -t # Check syntax
sudo systemctl reload nginx
sudo nginx -t # Check syntax
sudo systemctl reload nginx
# Install Hugo
hugo_version="0.146.0"
curl -L -o hugo.tar.gz \ "https://github.com/gohugoio/hugo/releases/download/v${hugo_version}/hugo_${hugo_version}_linux-amd64.tar.gz"
tar -xzf hugo.tar.gz && sudo mv hugo /usr/local/bin/ # Create site
hugo new site my-blog && cd my-blog
git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod # Configure (hugo.toml):
# baseURL = "https://yourdomain.com/blog/"
# theme = 'PaperMod' # Write a post
hugo new content posts/my-first-post.md # Build
hugo --minify
# Output is in public/
# Install Hugo
hugo_version="0.146.0"
curl -L -o hugo.tar.gz \ "https://github.com/gohugoio/hugo/releases/download/v${hugo_version}/hugo_${hugo_version}_linux-amd64.tar.gz"
tar -xzf hugo.tar.gz && sudo mv hugo /usr/local/bin/ # Create site
hugo new site my-blog && cd my-blog
git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod # Configure (hugo.toml):
# baseURL = "https://yourdomain.com/blog/"
# theme = 'PaperMod' # Write a post
hugo new content posts/my-first-post.md # Build
hugo --minify
# Output is in public/
# Install Hugo
hugo_version="0.146.0"
curl -L -o hugo.tar.gz \ "https://github.com/gohugoio/hugo/releases/download/v${hugo_version}/hugo_${hugo_version}_linux-amd64.tar.gz"
tar -xzf hugo.tar.gz && sudo mv hugo /usr/local/bin/ # Create site
hugo new site my-blog && cd my-blog
git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod # Configure (hugo.toml):
# baseURL = "https://yourdomain.com/blog/"
# theme = 'PaperMod' # Write a post
hugo new content posts/my-first-post.md # Build
hugo --minify
# Output is in public/
location /blog { alias /path/to/my-blog/public; index index.html; try_files $uri $uri/ /blog/index.html; }
location /blog { alias /path/to/my-blog/public; index index.html; try_files $uri $uri/ /blog/index.html; }
location /blog { alias /path/to/my-blog/public; index index.html; try_files $uri $uri/ /blog/index.html; }
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
# In http block:
limit_req_zone $binary_remote_addr zone=general rate=10r/s;
limit_req_zone $binary_remote_addr zone=login rate=5r/m; # In location block:
location /api/auth/ { limit_req zone=login burst=5 nodelay; proxy_pass http://127.0.0.1:3000;
}
# In http block:
limit_req_zone $binary_remote_addr zone=general rate=10r/s;
limit_req_zone $binary_remote_addr zone=login rate=5r/m; # In location block:
location /api/auth/ { limit_req zone=login burst=5 nodelay; proxy_pass http://127.0.0.1:3000;
}
# In http block:
limit_req_zone $binary_remote_addr zone=general rate=10r/s;
limit_req_zone $binary_remote_addr zone=login rate=5r/m; # In location block:
location /api/auth/ { limit_req zone=login burst=5 nodelay; proxy_pass http://127.0.0.1:3000;
}
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Total RAM: 4GB
Used: ~2.2GB (55%)
Free: ~1.8GB Total Disk: 60GB Used: ~37GB (62%)
Free: ~23GB CPU Load: 0.05-0.15 (basically idle)
Total RAM: 4GB
Used: ~2.2GB (55%)
Free: ~1.8GB Total Disk: 60GB Used: ~37GB (62%)
Free: ~23GB CPU Load: 0.05-0.15 (basically idle)
Total RAM: 4GB
Used: ~2.2GB (55%)
Free: ~1.8GB Total Disk: 60GB Used: ~37GB (62%)
Free: ~23GB CPU Load: 0.05-0.15 (basically idle) - $5/month total (not per app)
- Full control over everything
- Skills you'll use forever (Linux, Nginx, systemd)
- Unlimited apps (within resource limits) - Set up monitoring from day one. I didn't learn about UptimeRobot until an app was down for 3 days without me noticing.
- Use Docker from the start. Makes deploying new apps much easier (though raw Node.js works fine for simple cases).
- Automate deployments. Right now I SSH in and pull manually. A simple CI/CD pipeline would save time.
- Back up automatically. I learned this the hard way after losing a database. Now I have daily automated backups to object storage. - Basic Linux knowledge
- A willingness to Google error messages