# 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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Since this tutorial will be outdated, please refer to the <a target="_self" rel="noopener noreferrer nofollow" href="https://github.com/nodesource/distributions?tab=readme-ov-file#using-ubuntu-nodejs-20" style="pointer-events: none">official documentation link</a></div>
</div>

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

```bash
# 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:

```bash
# 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:

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

Add the following configuration:

```ini
[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:

```bash
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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Since this article may become outdated, here is the link to the <a target="_self" rel="noopener noreferrer nofollow" href="https://caddyserver.com/docs/install" style="pointer-events: none">official installation documentation</a> from Caddy’s website</div>
</div>

Let's install it:

```bash
# 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:

```bash
sudo nano /etc/caddy/Caddyfile
```

Replace the contents with:

```json
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:

```bash
sudo systemctl reload caddy
```

## Monitoring Your Deployment

To keep an eye on your application:

```bash
# 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:
    
    ```ini
    ExecStart=/usr/bin/node --max-old-space-size=800 node_modules/.bin/next start
    ```
    
2. **Monitor swap usage** and add swap if necessary:
    
    ```bash
    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!
