Configuring Caddy Reverse Proxy on Ubuntu VPS

Configuring Caddy Reverse Proxy on Ubuntu VPS

What You’ll Need

Table of Contents


I used to rely heavily on Nginx for almost every production deployment. While Nginx is undeniably fast and robust, managing manual Let’s Encrypt certificates through Certbot, renewing hooks, and structuring verbose server blocks across dozens of microservices eventually became an operational tax I no longer wanted to pay.

That is where Caddy steps in. Caddy is a modern, open-source web server written in Go that provisions and auto-renews TLS certificates by default via Let’s Encrypt and ZeroSSL. It handles HTTP/2 and HTTP/3 natively, requires fractionally smaller configuration files, and functions seamlessly as a high-performance reverse proxy for backend applications, webhooks, and REST APIs.

In this guide, I will walk you through setting up Caddy on a fresh Ubuntu VPS, setting up security and firewall rules, and writing a comprehensive, production-grade Caddyfile to route traffic to multiple internal web services safely.


Installing Caddy on Ubuntu Server

To get the official build of Caddy on Ubuntu, you should install it directly from the Caddy Cloudsmith repository rather than relying on Ubuntu’s static universe repository, which often lags behind upstream releases.

First, install the required core dependencies to handle HTTPS keyring transport:

sudo apt-get update
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg

Next, import the official Caddy GPG key to your system’s keyring directory and add the official apt repository configuration to your sources list:

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

Update your package index and install Caddy:

sudo apt-get update
sudo apt-get install caddy -y

Once the installation finishes, systemd automatically enables and starts the Caddy service daemon. You can verify that Caddy is active and running with the following command:

sudo systemctl status caddy

You should see an active status output confirming Caddy is running in the background.

💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.


Configuring DNS and Firewall Rules

Before Caddy can request dynamic TLS certificates, public domain names must point directly to your server’s IP address, and your firewall must allow inbound web traffic.

If you are hosting your infrastructure on a Hetzner VPS or a DigitalOcean droplet, copy your server’s public IPv4 and IPv6 addresses. Navigate to Namecheap (or your primary DNS registrar) and create A and AAAA records pointing your domain and subdomains to your server.

For this tutorial, let’s assume we are setting up:

  • api.example.com pointing to your IPv4 address (203.0.113.10)
  • app.example.com pointing to your IPv4 address (203.0.113.10)

Now, configure Ubuntu’s Uncomplicated Firewall (UFW) to allow SSH access alongside standard HTTP (port 80) and HTTPS (port 443) traffic. Execute the following commands in sequence:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable

Note that opening port 443 UDP is critical if you want Caddy to serve HTTP/3 (QUIC) traffic efficiently to modern web browsers.

Verify your firewall status to ensure ports are open properly:

sudo ufw status verbose

Writing a Production-Ready Caddyfile

Caddy reads its central configuration from /etc/caddy/Caddyfile. By default, Caddy ships with a basic welcome file. We will replace this entire file with a clear, highly modular setup that handles multiple internal applications, auto-TLS, static asset caching, compression, and secure reverse proxying.

Suppose you have three internal applications running on different ports:

  1. A main frontend interface running on local port 3000
  2. An automated report generation microservice on port 5000
  3. An automation instance (such as n8n) on port 5678

When managing background services like Automating PDF Report Generation with n8n and Node.js , sending webhooks into a clean reverse proxy removes the complexity of managing application-level HTTPS certificates. Similarly, for external API integrations like learning How to Build a YouTube Upload Bot with Node.js and OAuth2 , Caddy automatically satisfies strict OAuth redirect URI protocols by guaranteeing end-to-end SSL encryption without manual intervention.

Open /etc/caddy/Caddyfile in your terminal editor:

sudo nano /etc/caddy/Caddyfile

Wipe out the existing contents and paste the following complete configuration file:

{
	email admin@example.com
	admin off
	obsolete_host_patterns ignore
}

(common_headers) {
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "DENY"
		X-XSS-Protection "1; mode=block"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}
}

(compression) {
	encode zstd gzip
}

app.example.com {
	import common_headers
	import compression

	reverse_proxy 127.0.0.1:3000 {
		header_up Host {upstream_hostport}
		header_up X-Real-IP {remote_host}
		header_up X-Forwarded-For {remote_host}
		header_up X-Forwarded-Proto {scheme}
	}

	log {
		output file /var/log/caddy/app_access.log {
			roll_size 10mb
			roll_keep 5
		}
		format json
	}
}

api.example.com {
	import common_headers
	import compression

	reverse_proxy 127.0.0.1:5000 {
		header_up Host {upstream_hostport}
		header_up X-Real-IP {remote_host}
		header_up X-Forwarded-For {remote_host}
		header_up X-Forwarded-Proto {scheme}
	}

	log {
		output file /var/log/caddy/api_access.log {
			roll_size 10mb
			roll_keep 5
		}
		format json
	}
}

automation.example.com {
	import common_headers
	import compression

	reverse_proxy 127.0.0.1:5678 {
		flush_interval -1
		header_up Host {upstream_hostport}
		header_up X-Real-IP {remote_host}
		header_up X-Forwarded-For {remote_host}
		header_up X-Forwarded-Proto {scheme}
	}

	log {
		output file /var/log/caddy/automation_access.log {
			roll_size 10mb
			roll_keep 5
		}
		format json
	}
}

Breaking Down the Caddyfile

  1. Global Block { ... }: Configures global variables. Defining your email address allows ACME providers (Let’s Encrypt and ZeroSSL) to notify you if there are issues renewing certificates. Disabling the internal admin API (admin off) secures the service if you don’t need dynamic API reconfiguration.
  2. Snippets (common_headers) and (compression): Snippets allow you to reuse blocks of logic across multiple domain definitions without repeating code.
  3. reverse_proxy Block: Passes incoming HTTP requests directly to internal microservices bound to 127.0.0.1. The explicit header adjustments ensure the backend app receives accurate IP address metadata from the end user.
  4. flush_interval -1: Included in the automation block to enable full HTTP streaming capabilities required for WebSockets and Server-Sent Events (SSE).

Advanced Caddy Security Headers and Performance Tuning

Once your initial proxy setup is working, you will want to verify your syntax and tune Caddy’s runtime parameters for heavy production loads.

1. Validate and Reload Caddy

Never restart Caddy blind on production servers. Validate the configuration syntax first using Caddy’s built-in formatting tool:

sudo caddy validate --config /etc/caddy/Caddyfile

If the terminal reports Valid configuration, apply your updates zero-downtime using the reload command:

sudo caddy reload --config /etc/caddy/Caddyfile

2. Configure Log Directory Permissions

Since our custom Caddyfile writes JSON access logs to /var/log/caddy/, ensure the caddy service user has proper ownership of the log path:

sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/log/caddy
sudo chmod -R 755 /var/log/caddy

3. Tuning Open File Limits via Systemd

High-concurrency servers require increased file descriptor limits so Caddy can handle thousands of simultaneous socket connections. If your backend APIs interact with high-concurrency database connections, ensuring high throughput at the reverse proxy level is vital. Consider Configuring PostgreSQL Connection Pooling on Linux Servers alongside proxy optimization to keep your backend from stalling under high load.

To increase Caddy’s system limits, create a systemd override configuration file:

sudo mkdir -p /etc/systemd/system/caddy.service.d
sudo nano /etc/systemd/system/caddy.service.d/override.conf

Paste the following explicit limit adjustments inside the file:

[Service]
LimitNOFILE=1048576
LimitNPROC=512000
TasksMax=infinity

Save and exit the file. Apply the systemd override settings by reloading the system daemon and restarting Caddy:

sudo systemctl daemon-reload
sudo systemctl restart caddy

You can verify that the system limits were correctly applied to the active process using the system status inspector:

sudo systemctl show caddy --property=LimitNOFILE

You should see LimitNOFILE=1048576, indicating Caddy is fully configured to handle massive concurrent throughput without dropped connection requests.


Getting Started

Configuring a reverse proxy does not need to involve hundreds of lines of complex Nginx boilerplate or fragile certificate renewal scripts. With Caddy running on your Ubuntu server, you get fast HTTP/3 performance, modern security headers, and automated TLS certificate management out of the box.

To get your setup deployed quickly:

  1. Provision a clean server instance on Hetzner VPS or DigitalOcean .
  2. Point your domain names via Namecheap directly to your server’s public IP address.
  3. Install Caddy, configure your /etc/caddy/Caddyfile using the templates provided above, and reload the service.

If you are running complex automation flows or webhook architectures, you can seamlessly tie your reverse proxy into n8n Cloud to handle backend processing with zero friction.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr — mention SYS3-HUGO for priority. Or DM at chasebot.online .

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online