Installing on Ubuntu / Debian #
Ubuntu and Debian are the most popular Linux-based operating systems for servers in cloud infrastructure and Virtual Private Servers (VPS). Caddy provides an official APT repository actively maintained by the core developer team and its partners. By using this official repository, you can be sure that installation, upgrades, and security management run automatically and are fully integrated with the operating system’s built-in package manager.
This article walks through installing Caddy on Ubuntu and Debian comprehensively, step by step — from repository setup, understanding the post-installation directory architecture, systemd configuration for zero-downtime operations, handling low-port access permissions, to troubleshooting production issues.
System Prerequisites #
Before starting the Caddy installation process, make sure your operating system is ready. Caddy supports modern architectures like amd64, arm64, and armhf. Ensure you have administrative access (sudo) and a few basic tools installed on your server.
You can verify your server environment with the following commands:
# Check your Ubuntu or Debian distribution version
lsb_release -a
# Make sure your OS package repository list is up to date
sudo apt update
# Ensure curl and gnupg are installed for downloading repository security keys
sudo apt install -y curl gnupg debian-keyring debian-archive-keyring apt-transport-https
Caddy officially supports active Ubuntu LTS releases (20.04, 22.04, 24.04 LTS) and active Debian Stable releases (Debian 11 Bullseye and Debian 12 Bookworm).
Installing via the Official APT Repository #
Downloading random Caddy binaries from the internet for your production server is strongly discouraged. The best and safest method is using the official APT repository hosted by Cloudsmith. This repository provides Debian packages (.deb) cryptographically signed with the official GPG key to ensure package authenticity.
Here’s the command sequence to add the repository and install Caddy on your system:
# Step 1: Download the official Caddy GPG key and save it to your system keyring.
# This key ensures the APT system validates that packages haven't been modified by third parties.
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
# Step 2: Add the Caddy repository to the APT sources list (/etc/apt/sources.list.d/)
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
# Step 3: Update your local package database so it reads the newly added repository
sudo apt update
# Step 4: Install the Caddy Web Server
sudo apt install -y caddy
After installation finishes, you can verify Caddy’s presence and installation status:
# Check the installed Caddy version
caddy version
# Expected output includes version info, commit hash, and Go runtime, e.g.:
# v2.8.4 h1:XYZ123...
# Check the Caddy executable location
which caddy
# Output: /usr/bin/caddy
Post-Installation File Structure #
Installing via the APT package manager automatically arranges directories and system configuration according to the Linux Filesystem Hierarchy Standard (FHS). Knowing where these files live is crucial so you don’t make mistakes when managing configuration or backing up SSL certificates.
Here’s the directory structure created by the Caddy APT installer:
/
├── usr/
│ └── bin/
│ └── caddy ← Main Caddy binary (executable)
│
├── etc/
│ └── caddy/ ← Main configuration directory
│ └── Caddyfile ← Your main configuration file
│
├── var/
│ ├── lib/
│ │ └── caddy/ ← Caddy data directory (Very Important!)
│ │ └── .local/share/caddy/ ← TLS certificate & ACME private key storage
│ │
│ └── log/
│ └── caddy/ ← Application log directory (if configured)
│
└── lib/
└── systemd/
└── system/
└── caddy.service ← Default systemd service unit file
The caddy System User and Group
#
The APT installer automatically creates a system user named caddy and a group caddy with restricted privileges (no interactive shell and no root access).
# Check the automatically created caddy user info
id caddy
# Example output: uid=998(caddy) gid=998(caddy) groups=998(caddy)
For security reasons, the Caddy process controlled by systemd runs as this caddy user. You must ensure your website files (for example, HTML or PHP files in /var/www/) are readable by the caddy user, or the server will produce 403 Forbidden errors.
# Ensure the Caddy data directory is owned by the caddy user
ls -la /var/lib/caddy
Managing Caddy with Systemd #
On Ubuntu and Debian, systemd acts as the system and service manager. Caddy installed via APT is automatically registered as a service unit named caddy.service.
Basic Service Commands #
You can control the Caddy service lifecycle with the following commands:
# Check the current detailed status of the Caddy service
sudo systemctl status caddy
# Enable the Caddy service to start automatically at server boot
sudo systemctl enable caddy
# Start the Caddy service
sudo systemctl start caddy
# Stop the Caddy service
sudo systemctl stop caddy
Reload vs Restart: The Key to Avoiding Downtime in Production #
One of Caddy’s main advantages is its ability to update configuration without dropping active client connections. You need to understand the fundamental difference between these two commands:
# ANTI-PATTERN: Completely stop the server then run it again.
# Active client connections are dropped instantly, causing brief downtime.
sudo systemctl restart caddy
# CORRECT: Apply configuration changes gracefully.
# Caddy validates the new config in memory; if valid, it swaps pointers
# to the new config without shutting down network sockets. Old connections keep
# being served until they finish, new connections are served by the new config. Zero downtime!
sudo systemctl reload caddy
Monitoring Service Logs via Journalctl #
By default, the standard output (stdout/stderr) of the Caddy process is captured by the systemd journal. You can monitor server activity, TLS certificate issuance, and application errors using journalctl:
# Monitor Caddy logs in real time (like tail -f)
sudo journalctl -u caddy -f
# Monitor Caddy logs with high-precision timestamps
sudo journalctl -u caddy -f --output=short-precise
# Show the last 100 log lines without paging (cat)
sudo journalctl -u caddy -n 100 --no-pager
# Show Caddy logs only in the Warning or Error category (critical notices)
sudo journalctl -u caddy -p err..warning --since "1 day ago"
Initial Configuration and Caddyfile Validation #
Once installed, Caddy is ready to serve requests. Your default configuration file is at /etc/caddy/Caddyfile.
The Default Configuration #
By default, the installer ships a minimal config that serves Caddy’s welcome page on port 80:
:80 {
# Set your website's document directory
root * /usr/share/caddy
# Enable the static file server
file_server
}
Configuring a Real Domain #
If you want to point a real domain (for example, yoursite.com) and enable automatic HTTPS encryption, just replace the contents of that config file:
# Edit the main configuration file
sudo nano /etc/caddy/Caddyfile
Fill it with configuration matching your architecture needs:
# Example 1: Serve a static site on a real domain with automatic SSL
yoursite.com {
root * /var/www/yoursite
file_server
encode gzip zstd
}
# Example 2: Act as a reverse proxy for a Node.js app running on port 3000
app.yoursite.com {
reverse_proxy localhost:3000
}
# Example 3: Permanent redirect from www to non-www
www.yoursite.com {
redir https://yoursite.com{uri} permanent
}
Validating Configuration Before Applying #
[!IMPORTANT] Always Validate Before Reload! Never reload or restart a production server without validating the Caddyfile syntax first. If there’s a typo, the reload command will fail — but if you restart, the server will die completely and won’t come back until the error is fixed.
You can validate the integrity of your Caddyfile structure using the validate subcommand:
# Validate the configuration file locally
caddy validate --config /etc/caddy/Caddyfile
If the configuration is valid, you’ll see the output:
Valid configuration
Once validation passes, you can safely proceed to reload the configuration:
# Safe workflow: validate, and if successful immediately reload
caddy validate --config /etc/caddy/Caddyfile && sudo systemctl reload caddy
Low-Port Access & Linux Capabilities #
On Linux-based operating systems, there’s a strict security rule where non-root processes (like our caddy user) aren’t allowed to bind to privileged ports below 1024. Port 80 (HTTP) and port 443 (HTTPS) fall within that privileged range.
How Systemd Handles This Automatically #
The Caddy APT installer solves this limitation elegantly using Linux Capabilities — without running the server process as root (which would be very dangerous from a security standpoint).
If you inspect the Caddy systemd unit file:
cat /lib/systemd/system/caddy.service
You’ll find the following declaration under the [Service] section:
[Service]
...
User=caddy
Group=caddy
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
...
The CAP_NET_BIND_SERVICE instruction grants the /usr/bin/caddy binary the special right to bind to ports 80 and 443 even though it runs under the non-root caddy user identity.
Granting Access Manually (If Running Caddy Without Systemd) #
If you’re debugging and want to run Caddy manually in the terminal as a regular user (not root) using the caddy run command, you’ll hit a Permission Denied error when trying to bind ports 80/443.
To fix this, you can grant that capability to the Caddy binary manually:
# Persistently grant CAP_NET_BIND_SERVICE to the Caddy binary
sudo setcap cap_net_bind_service=+ep /usr/bin/caddy
# Verify whether the capability was installed successfully
getcap /usr/bin/caddy
# Output: /usr/bin/caddy cap_net_bind_service=ep
Once granted, you can run Caddy as a regular user in the terminal and it will still be able to listen on HTTP/HTTPS ports.
[!WARNING] Keep in mind that every time the Caddy package is upgraded via APT, the
/usr/bin/caddybinary is replaced with a clean new binary. This wipes out your manualsetcapsetting. Therefore, running Caddy under systemd control is the best solution because systemd applies this capability dynamically every time the process starts.
DNS and Firewall (UFW) #
Caddy can only issue automatic SSL certificates if it can complete the ACME challenge from the certificate authority (like Let’s Encrypt or ZeroSSL). This requires two absolute prerequisites: the domain’s DNS must point to your server’s IP, and the firewall must not block external connections on the HTTP/HTTPS ports.
1. Verify DNS #
Make sure your domain is configured with an A record pointing to your server’s public IP address. You can verify this from inside the server using DNS tools:
# Show your server's public IP
curl ifconfig.me
echo ""
# Check your domain's DNS resolution
dig +short yoursite.com
# Output must return the same public IP as the first step
2. Configuring UFW (Uncomplicated Firewall) #
Ubuntu uses UFW as its firewall management interface by default. You must open port 80 (TCP) and port 443 (TCP & UDP) so internet traffic can come in. Caddy uses port 443 UDP to enable the HTTP/3 protocol automatically.
# Check the current UFW status
sudo ufw status
# If UFW is active, allow standard web traffic
# UFW provides a built-in profile named 'WWW Full' that opens ports 80 and 443
sudo ufw allow 'WWW Full'
# Explicitly allow port 443 UDP if the WWW Full profile doesn't cover UDP (for HTTP/3)
sudo ufw allow 443/udp
# Reload the firewall rules
sudo ufw reload
# Verify that the ports are open
sudo ufw status verbose
Modifying the Systemd Service Unit with Override #
If you need to customize how systemd runs Caddy — for example, adding environment variables, changing the open-files limit (LimitNOFILE), or setting a custom working directory — you must not edit /lib/systemd/system/caddy.service directly. That file gets overwritten and restored to default whenever you update the Caddy package via APT.
The correct, safe way is to use the systemd override mechanism.
# Open the systemd override editor for caddy
sudo systemctl edit caddy
This command opens an empty text editor. You can add your custom configuration between the provided comment lines. For example, let’s add environment variables for domain or email credentials:
[Service]
Environment="PRIMARY_DOMAIN=yoursite.com"
Environment="[email protected]"
LimitNOFILE=1048576
Save the file and exit the editor. Systemd automatically creates the override file at /etc/systemd/system/caddy.service.d/override.conf and reloads the daemon configuration.
You can verify that your custom configuration was merged correctly:
# Show the currently active service unit configuration (including overrides)
sudo systemctl cat caddy
Now you can use those environment variables inside your /etc/caddy/Caddyfile:
{
email {$ADMIN_EMAIL}
}
{$PRIMARY_DOMAIN} {
reverse_proxy localhost:3000
}
Caddy Maintenance and Upgrades #
Because you installed Caddy through the official APT repository, software update management becomes very simple. Caddy joins your operating system’s regular update cycle.
# Update the system package list
sudo apt update
# Check whether a newer Caddy version is available in the repository
apt-cache policy caddy
# Upgrade only Caddy without touching other system packages
sudo apt install --only-upgrade caddy
# Or do a periodic full system update
sudo apt upgrade
When the Caddy package is updated via APT, the system automatically stops the old process safely, replaces the binary, reloads the systemd configuration, and restarts the Caddy service automatically.
Uninstallation Process #
If you decide to remove Caddy from your server, you need to understand the difference between a regular removal and a total purge that wipes all leftover configuration and SSL certificate data.
# Method 1: Remove Caddy but keep config files and certificate data
sudo apt remove caddy
# Method 2: Remove Caddy and all configuration files in /etc/caddy/
sudo apt purge caddy
# Additional Cleanup Steps (Optional)
# Remove the Caddy repository from the APT list so your system is clean again
sudo rm /etc/apt/sources.list.d/caddy-stable.list
sudo rm /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo apt update
# Remove SSL certificate data and logs (CAUTION: this action cannot be undone!)
# Only do this if you're sure you won't need your Let's Encrypt TLS certificate backups again.
sudo rm -rf /var/lib/caddy
sudo rm -rf /var/log/caddy
Troubleshooting Common Issues #
When running Caddy on Ubuntu/Debian, you may run into some common problems. Here’s a step-by-step guide to diagnosing and fixing them.
1. The Caddy Service Fails to Start #
If the systemd status shows failed, the first step is to look for the specific error code.
# Read the latest startup error logs
sudo journalctl -u caddy -n 50 --no-pager
If the log shows an error message like:
listen tcp :80: bind: address already in use or listen tcp :443: bind: address already in use
It means another application (like Apache, Nginx, or your own backend application) has already occupied port 80 or 443 before Caddy started. You can track down that application with:
# Find the PID and process name occupying port 80 or 443
sudo ss -tlnp | grep -E ':(80|443) '
# Or use lsof
sudo lsof -i :80
sudo lsof -i :443
If that application is an Apache or Nginx you no longer need, you can stop and disable it from startup:
# Stop Apache
sudo systemctl stop apache2 && sudo systemctl disable apache2
# Stop Nginx
sudo systemctl stop nginx && sudo systemctl disable nginx
# Start Caddy again
sudo systemctl start caddy
2. Failing to Obtain an SSL Certificate (HTTPS Access Warning) #
If your website is accessible via HTTP (http://domain.com) but triggers a security error when accessed via HTTPS (https://domain.com), this indicates Caddy failed to complete the domain ownership verification process (ACME challenge).
Check the ACME transaction logs:
# Filter logs related to TLS certificate issuance errors
sudo journalctl -u caddy --since "1 hour ago" | grep -iE 'error|acme|certificate|challenge'
Some common causes of this failure are:
- Port 80/443 blocked by firewall: Let’s Encrypt must connect back to port 80 on your server to verify the domain. Make sure port 80 is open in UFW and in your cloud provider’s control panel (like AWS Security Group or DigitalOcean Firewall).
- DNS hasn’t propagated yet: If the domain was just purchased or pointed, wait for DNS to fully propagate globally before forcing Caddy to request a certificate.
- Let’s Encrypt Rate Limit: If you trigger validation failure errors too often, Let’s Encrypt will temporarily block your requests. You can switch to ZeroSSL or wait out the block period.
3. File Permission Problems (Permission Denied 403) #
If your static website shows a 403 Forbidden error page, it means the Caddy process isn’t allowed to read files in your webroot directory.
Because Caddy runs as the caddy user, you need to grant proper read access to your website directory:
# Change the ownership of your webroot directory to the caddy user and group
sudo chown -R caddy:caddy /var/www/yoursite
# Grant safe read & execute permissions (755 for directories, 644 for files)
sudo find /var/www/yoursite -type d -exec chmod 755 {} \;
sudo find /var/www/yoursite -type f -exec chmod 644 {} \;
When to Switch to Alternatives / Not Use This #
Keep using the APT installation if:
✓ Your server is a single VM/VPS based on Ubuntu or Debian.
✓ You want security updates automatically integrated with the OS.
✓ Your configuration is standard and only uses Caddy's built-in modules.
✓ You want maximum performance directly on the system (bare-metal) without container overhead.
Consider other methods if:
✗ You're deploying in a distributed microservices environment managed via Docker.
✗ You need an external DNS provider module (like Cloudflare) to issue Wildcard SSL.
✗ Organization policy requires full application isolation using standard container images.
Summary #
- Official Repository — Always use the official Cloudsmith APT repository to ensure your Caddy package is authentic, safe, and easy to update.
- Zero-Downtime — Use
sudo systemctl reload caddyinstead ofrestartto apply configuration changes without disrupting active client traffic.- Validation Is Mandatory — Get into the habit of running
caddy validate --config /etc/caddy/Caddyfilebefore reloading to prevent runtime failures.- Port Access — Caddy runs safely as the non-root
caddyuser yet can bind ports 80/443 thanks to Linux Capabilities (CAP_NET_BIND_SERVICE) installed automatically by systemd.- TLS Persistence — Your TLS certificates live in
/var/lib/caddy/.local/share/caddy/. Secure and never delete this directory to avoid hitting Let’s Encrypt request limits.- Log Management — Use
sudo journalctl -u caddy -fto monitor server activity and SSL encryption processes in real time.