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

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.
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.
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:
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
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!
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.