- Published on
How to deploy a Node.js website with Nginx and Systemd on a VPS
- Authors
- Name
- Michael Scheiwiller
For some time i wanted to read the The Daily Stoic, but i never got to start it. So i decided to build a simple website that would serve one stoic meditation per day. With this i could get some more practice with systemd and nginx.
If you are just looking for the code, you can find it here.
Disclaimer: The rest of this post is mostly copied from the README of the project and AI generated. I just added some more details and explanations.
Project Overview
The Daily Stoic website is has a simple design:
- Purpose: Deliver one stoic meditation per day based on the current date
- Design Philosophy: Clean, readable interface
- Tech Stack: Minimal but robust - Node.js backend, SQLite database, vanilla HTML/CSS/JavaScript frontend
- Deployment: Production-ready setup on a VPS with proper SSL and caching
Tech Stack and Architecture
Backend
- Node.js + Express: Lightweight server framework
- SQLite: File-based database with 366 pre-populated meditations
- RESTful API: Clean endpoints for fetching meditations
Frontend
- Vanilla HTML/CSS/JavaScript: No frameworks, maximum performance
- Google Fonts (Crimson Text): Elegant typography for readability
- Responsive Design: Works seamlessly on mobile and desktop
Infrastructure
- Ubuntu VPS: Cost-effective hosting solution
- Nginx: Reverse proxy with SSL termination and static file caching
- Systemd: Process management and automatic restart capabilities
- Let's Encrypt: Free SSL certificates with automatic renewal
Local Development Setup
1. Project Structure
366dailystoic/
├── server.js # Express server with API routes
├── package.json # Dependencies and scripts
├── daily-stoic.db # SQLite database (586KB)
├── public/ # Static frontend files
│ ├── index.html # Main page
│ ├── style.css # Styling (4KB)
│ └── script.js # Frontend logic (3KB)
└── README.md # Documentation
2. Dependencies
The project uses minimal dependencies for maximum reliability:
{
"dependencies": {
"express": "^4.18.2",
"sqlite3": "^5.1.6"
}
}
3. Server Configuration
The Express server (server.js
) handles:
- Static file serving from the
public/
directory - API endpoints for meditation data
- Database connections and queries
- Graceful shutdown handling
Key features:
// Serve static files
app.use(express.static('public'))
// API endpoints
app.get('/api/today', (req, res) => {
/* Today's meditation */
})
app.get('/api/meditation/:month/:day', (req, res) => {
/* Specific date */
})
app.get('/api/all', (req, res) => {
/* All meditations for navigation */
})
app.get('/api/health', (req, res) => {
/* Health check */
})
4. Running Locally
# Install dependencies
npm install
# Start development server
npm start
# Visit the application
open http://localhost:3000
Production Deployment on VPS
1. Server Preparation and repo clone
Check out my other posts on how to setup a vps and how to configure ssh for your vps so you can easily handle your vps.
2. Systemd Service Configuration
Create a systemd service for process management and automatic restarts:
File: /etc/systemd/system/daily-stoic.service
[Unit]
Description=Daily Stoic Website
Documentation=https://github.com/lappemic/366dailystoic
After=network.target
[Service]
Type=simple
User=devuser
WorkingDirectory=/home/devuser/projects/366dailystoic
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=daily-stoic
Environment=NODE_ENV=production
Environment=PORT=3000
# Security settings
NoNewPrivileges=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
Key Configuration Points:
- User: Runs as non-root user (
devuser
) for security - WorkingDirectory: Absolute path to your project
- Restart Policy: Automatically restarts on failure with 10-second delay
- Logging: Outputs to syslog with identifier for easy monitoring
- Security: Additional hardening with
NoNewPrivileges
andPrivateTmp
Service Management:
# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable daily-stoic
sudo systemctl start daily-stoic
# Check status
sudo systemctl status daily-stoic
# View logs
sudo journalctl -u daily-stoic -f
For other systemd examples, check out the blog post Fast Private Streamlit Sharing with Systemd
3. Nginx Reverse Proxy Configuration
Nginx serves as a reverse proxy, handling SSL termination, static file caching, and security headers.
File: /etc/nginx/sites-available/daily-stoic
server {
server_name 366dailystoic.com www.366dailystoic.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self'" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# Cache static assets and proxy to Node.js
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
proxy_pass http://localhost: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;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Proxy to Node.js app
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
listen 443 ssl; # managed by Certbot -> this part will automatically be set after you run the certbot command in the next step
ssl_certificate /etc/letsencrypt/live/366dailystoic.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/366dailystoic.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
# HTTP to HTTPS redirect
server {
if ($host = www.366dailystoic.com) {
return 301 https://$host$request_uri;
}
if ($host = 366dailystoic.com) {
return 301 https://$host$request_uri;
}
listen 80;
server_name 366dailystoic.com www.366dailystoic.com;
return 404;
}
Key Configuration Features:
- Security Headers: Comprehensive security headers for XSS, clickjacking, and content type protection
- Gzip Compression: Reduces bandwidth usage for text-based files
- Static File Caching: 1-year cache for CSS/JS files with immutable cache-control
- Proxy Configuration: Proper headers for WebSocket support and real IP forwarding
- SSL Configuration: Managed by Certbot for automatic certificate management
Enable the Configuration:
# Create symlink to enable site
sudo ln -s /etc/nginx/sites-available/daily-stoic /etc/nginx/sites-enabled/
# Test configuration
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
4. SSL Certificate Setup with Let's Encrypt
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y
# Obtain SSL certificate
sudo certbot --nginx -d 366dailystoic.com -d www.366dailystoic.com
# Test automatic renewal
sudo certbot renew --dry-run
Common Issues and Troubleshooting
Issue 1: Static Files Return 404 Errors
Problem: CSS and JavaScript files return 404 Not Found
errors when accessed via the domain.
Symptoms:
GET https://366dailystoic.com/style.css net::ERR_ABORTED 404 (Not Found)
GET https://366dailystoic.com/script.js net::ERR_ABORTED 404 (Not Found)
Root Cause: The Nginx location block for static files only sets cache headers but doesn't include proxy_pass
directive.
Solution: Add the proxy_pass
directive to the static files location block:
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
proxy_pass http://localhost:3000; # This was missing!
proxy_http_version 1.1;
proxy_set_header Host $host;
# ... other headers
expires 1y;
add_header Cache-Control "public, immutable";
}
Verification:
# Test locally
curl -I http://localhost:3000/style.css
# Test through Nginx
curl -I https://366dailystoic.com/style.css
Issue 2: Service Won't Start
Common causes and solutions:
- Port conflicts: Ensure port 3000 isn't used by another service
- Permission issues: Verify the service user has read access to project files
- Path errors: Double-check
WorkingDirectory
andExecStart
paths in service file - Missing dependencies: Run
npm install
in the project directory
Issue 3: Database Connection Errors
Solution: Ensure the SQLite database file has proper permissions:
sudo chown devuser:devuser daily-stoic.db
chmod 644 daily-stoic.db
Performance Optimizations
1. Database Query Optimization
- Indexed date_key column for fast daily lookups
- Connection pooling handled by sqlite3 driver
- Graceful database connection management
2. Caching Strategy
- Static files: 1-year browser cache with immutable flag
- API responses: Consider adding ETag headers for future optimization
- Nginx: Gzip compression for text-based content
3. Security Measures
- Content Security Policy: Restricts resource loading to trusted sources
- Security headers: Protection against common web vulnerabilities
- HTTPS enforcement: All traffic redirected to secure connections
- Process isolation: Service runs with minimal privileges
Monitoring and Maintenance
Service Monitoring
# Check service status
sudo systemctl status daily-stoic
# View real-time logs
sudo journalctl -u daily-stoic -f
# Check Nginx status
sudo systemctl status nginx
# Monitor resource usage
htop
Conclusion
This setup provides a robust, production-ready deployment for a Node.js application with:
- High availability through systemd process management
- Performance via Nginx reverse proxy and caching
- Security through SSL, security headers, and proper permissions
- Maintainability through clear configuration and logging
The Daily Stoic website demonstrates that simple, well-architected solutions can be both powerful and maintainable. By leveraging proven technologies like Nginx, systemd, and Let's Encrypt, we've created a deployment that's secure, fast, and reliable.
The total setup takes about 30 minutes once you understand the components, and the result is a professional-grade web application that can handle significant traffic while remaining easy to maintain and monitor.
Want to build something similar for your team or business? Contact me