Set up Caddy, a modern web server with automatic HTTPS certificates from Let's Encrypt, zero-config HTTP/2, and built-in reverse proxy capabilities for production applications.
Prerequisites
- Root or sudo access
- Domain name pointed to your server
- Ports 80 and 443 accessible
What this solves
Caddy is a modern web server that automatically handles HTTPS certificates, HTTP/2, and reverse proxy configuration without complex setup. Unlike traditional web servers that require manual SSL certificate management, Caddy automatically obtains and renews Let's Encrypt certificates, making it perfect for production deployments where you need secure, reliable web hosting with minimal maintenance overhead.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest package information and security updates.
sudo apt update && sudo apt upgrade -y
Install required dependencies
Install curl and other tools needed to add the official Caddy repository and verify package signatures.
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
Add Caddy official repository
Add the official Caddy repository to ensure you get the latest stable version with automatic updates through your package manager.
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
Install Caddy web server
Install the Caddy package which includes the web server binary, systemd service unit, and default configuration structure.
sudo apt install -y caddy
Configure firewall rules
Open HTTP (80) and HTTPS (443) ports to allow web traffic. Caddy uses port 80 for ACME HTTP-01 challenges and port 443 for HTTPS traffic.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Create basic Caddy configuration
Create a basic Caddyfile configuration that serves a simple website with automatic HTTPS. Replace example.com with your actual domain name.
example.com {
root * /var/www/html
file_server
encode gzip
log {
output file /var/log/caddy/access.log
format json
}
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
Create web root directory
Create the directory structure for your website files with proper ownership and permissions for the Caddy user.
sudo mkdir -p /var/www/html
sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/www/html
sudo chown -R caddy:caddy /var/log/caddy
sudo chmod 755 /var/www/html
sudo chmod 755 /var/log/caddy
Create sample web page
Create a simple HTML page to test your Caddy installation and verify automatic HTTPS is working correctly.
Caddy Server - Working!
Caddy Web Server
✅ Server is running successfully!
Automatic HTTPS is enabled with Let's Encrypt certificates.
HTTP/2 and modern security headers are configured.
sudo chown caddy:caddy /var/www/html/index.html
sudo chmod 644 /var/www/html/index.html
Validate Caddy configuration
Test your Caddyfile syntax before starting the service to catch any configuration errors early.
sudo caddy validate --config /etc/caddy/Caddyfile
Enable and start Caddy service
Enable Caddy to start automatically on boot and start the service immediately. Check the status to ensure it's running correctly.
sudo systemctl enable caddy
sudo systemctl start caddy
sudo systemctl status caddy
Configure reverse proxy
Set up reverse proxy configuration
Configure Caddy to proxy requests to backend applications. This example shows proxying to a Node.js app on port 3000 and an API server on port 8080.
example.com {
reverse_proxy localhost:3000
encode gzip
log {
output file /var/log/caddy/access.log
format json
}
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
}
}
api.example.com {
reverse_proxy localhost:8080 {
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}
}
encode gzip
log {
output file /var/log/caddy/api-access.log
format json
}
}
Configure load balancing
Set up load balancing across multiple backend servers with health checks and failover capabilities.
app.example.com {
reverse_proxy {
to localhost:3000 localhost:3001 localhost:3002
lb_policy round_robin
health_uri /health
health_interval 30s
health_timeout 5s
}
encode gzip
log {
output file /var/log/caddy/app-access.log
format json
}
}
Add path-based routing
Configure different backend services based on URL paths, useful for microservices architectures.
example.com {
# Serve static files for the main site
handle /assets/* {
root * /var/www/html
file_server
}
# Proxy API requests to backend
handle /api/* {
reverse_proxy localhost:8080
}
# Proxy admin panel to different service
handle /admin/* {
reverse_proxy localhost:9000 {
header_up X-Forwarded-User {http.request.header.X-User}
}
}
# Default handler for main application
handle {
reverse_proxy localhost:3000
}
encode gzip
log {
output file /var/log/caddy/access.log
format json
}
}
Reload Caddy configuration
Apply the new configuration without downtime using Caddy's graceful reload feature.
sudo systemctl reload caddy
sudo systemctl status caddy
Configure virtual hosts
Set up multiple domains
Configure multiple websites with different domains, each with their own document root and configuration.
# Primary website
example.com, www.example.com {
root * /var/www/example.com
file_server
encode gzip
# Redirect www to non-www
@www host www.example.com
redir @www https://example.com{uri} permanent
log {
output file /var/log/caddy/example.com.log
format json
}
}
Blog subdomain
blog.example.com {
reverse_proxy localhost:2368 # Ghost blog
encode gzip
log {
output file /var/log/caddy/blog.log
format json
}
}
Development site
dev.example.com {
root * /var/www/dev.example.com
file_server
# Basic auth for development site
basicauth {
developer $2a$14$hEm9U2CRyCXcZhKRlBF.1OZdHFEKF9TgC7RHdL6Z8qJ4aZeWxXoVy
}
log {
output file /var/log/caddy/dev.log
format json
}
}
Create directory structure
Create separate directories for each virtual host with proper ownership and permissions.
sudo mkdir -p /var/www/example.com
sudo mkdir -p /var/www/dev.example.com
sudo chown -R caddy:caddy /var/www/example.com
sudo chown -R caddy:caddy /var/www/dev.example.com
sudo chmod -R 755 /var/www/example.com
sudo chmod -R 755 /var/www/dev.example.com
Generate basic auth password
Create a hashed password for basic authentication on the development site using Caddy's built-in hash command.
caddy hash-password --plaintext 'your-secure-password'
Performance optimization and monitoring
Configure advanced caching
Set up intelligent caching headers and compression for optimal performance across different content types.
example.com {
root * /var/www/html
# Cache static assets aggressively
@static path .css .js .png .jpg .jpeg .gif .ico .svg .woff .woff2 .ttf .eot
header @static {
Cache-Control "public, max-age=31536000, immutable"
Expires "1 year"
}
# Cache HTML with shorter duration
@html path *.html
header @html {
Cache-Control "public, max-age=3600"
}
file_server
encode gzip brotli
# Security headers
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
}
log {
output file /var/log/caddy/access.log {
roll_size 100MiB
roll_keep 5
roll_keep_for 30d
}
format json
level INFO
}
}
Set up log rotation
Configure logrotate to manage Caddy log files and prevent disk space issues. This configuration is similar to what you'd use for centralized log management.
/var/log/caddy/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0644 caddy caddy
postrotate
systemctl reload caddy
endscript
}
Configure system resource limits
Optimize Caddy's systemd service for production workloads with appropriate resource limits and security hardening.
[Service]
Resource limits
LimitNOFILE=65536
LimitNPROC=4096
Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ReadWritePaths=/var/log/caddy /var/lib/caddy
Performance optimization
Environment=CADDY_AGREE=true
Environment=CADDY_API=localhost:2019
sudo mkdir -p /etc/systemd/system/caddy.service.d
sudo systemctl daemon-reload
sudo systemctl restart caddy
Enable Caddy admin API
Configure the admin API for monitoring and dynamic configuration updates. Restrict access to localhost for security.
{
admin localhost:2019
log default {
output file /var/log/caddy/caddy.log
format json
level INFO
}
}
example.com {
# Your site configuration here
root * /var/www/html
file_server
encode gzip
}
Verify your setup
Test your Caddy installation and configuration with these verification commands.
# Check Caddy service status
sudo systemctl status caddy
Verify Caddy version and build info
caddy version
Test configuration syntax
sudo caddy validate --config /etc/caddy/Caddyfile
Check SSL certificate status
caddy list-certificates
Test HTTP to HTTPS redirect
curl -I http://example.com
Test HTTPS with SSL info
curl -I https://example.com
Check admin API (if enabled)
curl http://localhost:2019/config/
Monitor real-time logs
sudo tail -f /var/log/caddy/access.log
Check process and memory usage
sudo ps aux | grep caddy
sudo systemctl show caddy --property=MainPID,MemoryCurrent,CPUUsageNSec
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Service fails to start | Configuration syntax error | sudo caddy validate --config /etc/caddy/Caddyfile to check syntax |
| Let's Encrypt certificate fails | Domain not pointing to server or port 80 blocked | Verify DNS records and firewall rules for ports 80/443 |
| Permission denied on web files | Incorrect file ownership | sudo chown -R caddy:caddy /var/www/html and chmod 644 for files, 755 for directories |
| Reverse proxy connection refused | Backend service not running | Check backend service: sudo netstat -tlnp | grep :3000 |
| High memory usage | Large log files or no log rotation | Configure log rotation and check /var/log/caddy/ disk usage |
| Admin API not accessible | API disabled or listening on wrong interface | Add admin localhost:2019 to global Caddyfile block |
Next steps
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
# Script configuration
readonly SCRIPT_NAME="$(basename "$0")"
readonly CADDY_USER="caddy"
readonly WEB_ROOT="/var/www/html"
readonly LOG_DIR="/var/log/caddy"
readonly CADDYFILE="/etc/caddy/Caddyfile"
# Global variables
DOMAIN=""
PKG_MGR=""
PKG_INSTALL=""
FIREWALL_CMD=""
# Cleanup function for rollback
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop caddy 2>/dev/null || true
systemctl disable caddy 2>/dev/null || true
rm -rf "$WEB_ROOT" "$LOG_DIR" 2>/dev/null || true
if [ "$PKG_MGR" = "apt" ]; then
apt remove --purge -y caddy 2>/dev/null || true
rm -f /etc/apt/sources.list.d/caddy-stable.list
rm -f /usr/share/keyrings/caddy-stable-archive-keyring.gpg
elif [ "$PKG_MGR" = "dnf" ]; then
dnf copr disable @caddy/caddy -y 2>/dev/null || true
dnf remove -y caddy 2>/dev/null || true
fi
echo -e "${RED}Cleanup completed.${NC}"
}
trap cleanup ERR
# Usage function
usage() {
cat << EOF
Usage: $SCRIPT_NAME <domain> [options]
Arguments:
domain Domain name for the Caddy server (required)
Options:
-h, --help Show this help message
Examples:
$SCRIPT_NAME example.com
$SCRIPT_NAME mysite.example.org
EOF
exit 1
}
# Logging functions
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Check if running as root/sudo
check_privileges() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
}
# Detect distribution and set package manager
detect_distro() {
if [ ! -f /etc/os-release ]; then
log_error "Cannot detect Linux distribution"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
# Check if dnf exists, fallback to yum
if ! command -v dnf &> /dev/null; then
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
fi
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_success "Detected $PRETTY_NAME using $PKG_MGR"
}
# Validate domain name
validate_domain() {
if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
log_error "Invalid domain name: $DOMAIN"
exit 1
fi
}
# Parse command line arguments
parse_args() {
if [ $# -eq 0 ]; then
usage
fi
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
usage
;;
-*)
log_error "Unknown option: $1"
usage
;;
*)
if [ -z "$DOMAIN" ]; then
DOMAIN="$1"
else
log_error "Too many arguments"
usage
fi
;;
esac
shift
done
if [ -z "$DOMAIN" ]; then
log_error "Domain name is required"
usage
fi
}
# Update system packages
update_system() {
echo -e "${BLUE}[1/9] Updating system packages...${NC}"
if [ "$PKG_MGR" = "apt" ]; then
apt update && apt upgrade -y
else
$PKG_INSTALL epel-release 2>/dev/null || true
$PKG_MGR update -y
fi
log_success "System updated successfully"
}
# Install dependencies
install_dependencies() {
echo -e "${BLUE}[2/9] Installing required dependencies...${NC}"
if [ "$PKG_MGR" = "apt" ]; then
$PKG_INSTALL debian-keyring debian-archive-keyring apt-transport-https curl gnupg2
else
$PKG_INSTALL curl dnf-plugins-core 2>/dev/null || $PKG_INSTALL curl
fi
log_success "Dependencies installed successfully"
}
# Add Caddy repository
add_caddy_repo() {
echo -e "${BLUE}[3/9] Adding Caddy official repository...${NC}"
if [ "$PKG_MGR" = "apt" ]; then
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null
apt update
else
if command -v dnf &> /dev/null; then
dnf copr enable @caddy/caddy -y
else
yum install -y yum-plugin-copr
yum copr enable @caddy/caddy -y
fi
fi
log_success "Caddy repository added successfully"
}
# Install Caddy
install_caddy() {
echo -e "${BLUE}[4/9] Installing Caddy web server...${NC}"
$PKG_INSTALL caddy
log_success "Caddy installed successfully"
}
# Configure firewall
configure_firewall() {
echo -e "${BLUE}[5/9] Configuring firewall rules...${NC}"
if [ "$FIREWALL_CMD" = "ufw" ]; then
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
else
systemctl enable firewalld
systemctl start firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
fi
log_success "Firewall configured successfully"
}
# Create directories and files
create_directories() {
echo -e "${BLUE}[6/9] Creating web directories and files...${NC}"
mkdir -p "$WEB_ROOT"
mkdir -p "$LOG_DIR"
mkdir -p "$(dirname "$CADDYFILE")"
# Set proper ownership and permissions
chown -R "$CADDY_USER:$CADDY_USER" "$WEB_ROOT"
chown -R "$CADDY_USER:$CADDY_USER" "$LOG_DIR"
chmod 755 "$WEB_ROOT"
chmod 755 "$LOG_DIR"
log_success "Directories created successfully"
}
# Create Caddyfile
create_caddyfile() {
echo -e "${BLUE}[7/9] Creating Caddy configuration...${NC}"
cat > "$CADDYFILE" << EOF
$DOMAIN {
root * $WEB_ROOT
file_server
encode gzip
log {
output file $LOG_DIR/access.log
format json
}
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
# Create sample webpage
cat > "$WEB_ROOT/index.html" << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Caddy Server - Working!</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.status { color: #28a745; font-size: 24px; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<h1>Caddy Web Server</h1>
<p class="status">✅ Server is running successfully!</p>
<p>Automatic HTTPS is enabled with Let's Encrypt certificates.</p>
<p>HTTP/2 and modern security headers are configured.</p>
<p>Domain: <strong>$DOMAIN</strong></p>
</div>
</body>
</html>
EOF
chown "$CADDY_USER:$CADDY_USER" "$WEB_ROOT/index.html"
chmod 644 "$WEB_ROOT/index.html"
log_success "Caddy configuration created successfully"
}
# Start and enable Caddy service
start_caddy_service() {
echo -e "${BLUE}[8/9] Starting and enabling Caddy service...${NC}"
# Validate configuration first
if ! caddy validate --config "$CADDYFILE"; then
log_error "Caddy configuration validation failed"
exit 1
fi
systemctl daemon-reload
systemctl enable caddy
systemctl start caddy
log_success "Caddy service started and enabled"
}
# Verify installation
verify_installation() {
echo -e "${BLUE}[9/9] Verifying installation...${NC}"
# Check if service is running
if ! systemctl is-active --quiet caddy; then
log_error "Caddy service is not running"
systemctl status caddy --no-pager
exit 1
fi
# Check if listening on correct ports
if ! ss -tlnp | grep -q ":443"; then
log_warning "Caddy may not be listening on port 443 yet (this is normal on first start)"
fi
log_success "Caddy is running successfully"
echo
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${BLUE}Domain:${NC} $DOMAIN"
echo -e "${BLUE}Web root:${NC} $WEB_ROOT"
echo -e "${BLUE}Config file:${NC} $CADDYFILE"
echo -e "${BLUE}Log directory:${NC} $LOG_DIR"
echo
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Ensure DNS for $DOMAIN points to this server"
echo "2. Visit https://$DOMAIN to test automatic HTTPS"
echo "3. Place your website files in $WEB_ROOT"
echo "4. Edit $CADDYFILE for custom configuration"
echo "5. Run 'systemctl reload caddy' after config changes"
}
# Main function
main() {
check_privileges
parse_args "$@"
validate_domain
detect_distro
update_system
install_dependencies
add_caddy_repo
install_caddy
configure_firewall
create_directories
create_caddyfile
start_caddy_service
verify_installation
# Disable cleanup trap on successful completion
trap - ERR
}
main "$@"
Review the script before running. Execute with: bash install.sh