Logo
Michael Scheiwiller
Published on

How to deploy a Node.js website with Nginx and Systemd on a VPS

Authors
  • avatar
    Name
    Michael Scheiwiller
    Twitter

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 and PrivateTmp

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 and ExecStart 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