Deploying Next.js Apps on Ubuntu 24.04 with Systemd and Caddy: A Minimalist Approach

Hermann KaoHermann Kao
4 min read

When my friend approached me about deploying his Next.js applications on a tiny VPS (2vCPU, 2GB RAM) without Docker, I initially raised an eyebrow. But sometimes constraints breed creativity, and this deployment strategy turned out to be quite elegant because it uses systemd which is included in most Debian distros and not some random process manager like PM2 or something like that

Below is a guide to setting up Next.js deployment using systemd and Caddy on Ubuntu 24.04 - perfect for resource-constrained environments or situations where Docker isn't an option.

Setting Up Node.js 20 LTS

First, we need to install Node.js 20 LTS.

💡
Since this tutorial will be outdated, please refer to the official documentation link

Ubuntu's default repositories often contain older Node versions, so we'll use the NodeSource repository:

# Add NodeSource repository
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -

# Install Node.js and npm
sudo apt-get install -y nodejs

# Verify installation
node -v  # Should output v20.x.x
npm -v   # Should output 10.x.x

Building the Next.js Application

Assuming your Next.js application is ready for production:

# Navigate to your application directory
cd /path/to/nextjs-app

# Install dependencies
npm install

# Create .env file if needed

# Build the application
npm run build

Creating a Systemd Service with Memory Limits

Now let's create a systemd service to run our Next.js application with appropriate resource constraints:

# nano because noone could escape vi 💀
sudo nano /etc/systemd/system/nextjs-app.service

Add the following configuration:

[Unit]
Description=Next.js Application
After=network.target

[Service]
Type=simple
User=ubuntu  # Replace with your server user
WorkingDirectory=/path/to/nextjs-app
ExecStart=/usr/bin/npm start
Restart=on-failure
RestartSec=15
# Memory limits (1GB as requested by my boi)
MemoryMax=1G
MemoryHigh=768M
MemoryAccounting=true
# Environment variables if needed
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=HOST=127.0.0.1

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl enable nextjs-app.service
sudo systemctl start nextjs-app.service
sudo systemctl status nextjs-app.service  # Check if it's running correctly

Installing Caddy Web Server

Caddy is a modern, security-first web server that automatically handles HTTPS certificates.

💡
Since this article may become outdated, here is the link to the official installation documentation from Caddy’s website

Let's install it:

# Install Caddy
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Configuring Caddy as a Reverse Proxy

Now we'll configure Caddy to serve our Next.js application:

sudo nano /etc/caddy/Caddyfile

Replace the contents with:

yourwebsite.com {
    # Enable HTTPS automatically
    tls your@email.com

    # Reverse proxy to your Next.js app
    reverse_proxy localhost:3000

    # For better logging
    log {
        output file /var/log/caddy/yourwebsite.com.log
    }

    # Optional: Compress responses for better performance
    encode gzip zstd
}

Apply the configuration:

sudo systemctl reload caddy

Monitoring Your Deployment

To keep an eye on your application:

# Check systemd service logs
sudo journalctl -u nextjs-app.service -f

# Check Caddy logs 
sudo tail -f /var/log/caddy/yourwebsite.com.log

Performance Optimizations for Limited Resources

Since you're working with only 2GB RAM and a 1GB limit for the Next.js app, consider these additional optimizations:

  1. Enable Node's --max-old-space-size flag in your systemd service:

     ExecStart=/usr/bin/node --max-old-space-size=800 node_modules/.bin/next start
    
  2. Monitor swap usage and add swap if necessary:

     sudo fallocate -l 1G /swapfile
     sudo chmod 600 /swapfile
     sudo mkswap /swapfile
     sudo swapon /swapfile
     echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
    
     # check if the swap is active
     sudo swapon --show
     free -h
    

Conclusion

This setup provides a lightweight yet robust deployment solution for Next.js applications (or nodejs apps) without Docker. The combination of systemd (for process management and resource constraints) with Caddy (for HTTPS and reverse proxying) creates a surprisingly powerful stack that works well on limited hardware.

While containers offer more isolation, this approach has its own advantages: simplicity, lower resource overhead, and straightforward troubleshooting - perfect for that tiny VPS with just enough resources to get the job done.


Need any specific adjustments or have questions about any part of this deployment strategy? Let me know in the comments!

0
Subscribe to my newsletter

Read articles from Hermann Kao directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Hermann Kao
Hermann Kao

Software developer documenting my journey through web and mobile development. I share what I learn, build, and struggle with—from React to SwiftUI, architecture decisions to deployment challenges. Not an expert, just passionate about coding and learning in public. Join me as I navigate the tech landscape one commit at a time.