Set up Caddy 2 web server with automatic HTTPS certificates from Let's Encrypt and PHP-FPM integration for dynamic content. Includes virtual hosts, security headers, and systemd service configuration.
Prerequisites
- Root or sudo access
- Domain name pointing to server
- Ports 80 and 443 accessible from internet
What this solves
Caddy 2 is a modern web server that automatically obtains and renews HTTPS certificates from Let's Encrypt without manual configuration. Unlike traditional web servers that require separate SSL certificate management, Caddy handles HTTPS automatically while providing excellent performance and a simple configuration format called the Caddyfile.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of all packages.
sudo apt update && sudo apt upgrade -y
Install required packages
Install curl and basic tools needed for the Caddy installation process.
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
Add Caddy official repository
Add the official Caddy repository to get the latest stable version with automatic updates.
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 2
Install Caddy from the official repository to ensure you get the latest version with all security updates.
sudo apt install -y caddy
Install PHP-FPM
Install PHP-FPM to handle dynamic PHP content. This installs PHP 8.1+ with the FastCGI Process Manager for optimal performance.
sudo apt install -y php-fpm php-mysql php-curl php-json php-mbstring
Create web root directory
Create the directory structure for your websites and set appropriate ownership and permissions.
sudo mkdir -p /var/www/html/example.com
sudo chown -R caddy:caddy /var/www/html
sudo chmod -R 755 /var/www/html
Create a basic Caddyfile configuration
Create the main Caddy configuration file that defines how your web server handles requests and enables automatic HTTPS.
{
# Global options
email admin@example.com
admin localhost:2019
}
HTTP to HTTPS redirect
http://example.com {
redir https://example.com{uri} permanent
}
Main site with automatic HTTPS
example.com {
root * /var/www/html/example.com
# Enable file server for static content
file_server
# PHP-FPM integration
php_fastcgi unix//run/php/php-fpm.sock {
index index.php
}
# Security headers
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
Strict-Transport-Security "max-age=31536000; includeSubDomains"
Referrer-Policy strict-origin-when-cross-origin
}
# Rate limiting
rate_limit {
zone static_files {
key {remote_host}
events 100
window 1m
}
}
# Logging
log {
output file /var/log/caddy/example.com.log {
roll_size 100mb
roll_keep 5
roll_keep_for 720h
}
}
}
Create log directory
Create the log directory for Caddy and set proper permissions so Caddy can write log files.
sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy
sudo chmod 755 /var/log/caddy
Create test PHP file
Create a simple PHP file to test that PHP-FPM integration is working correctly.
<?php
phpinfo();
?>
sudo chown caddy:caddy /var/www/html/example.com/index.php
sudo chmod 644 /var/www/html/example.com/index.php
Configure PHP-FPM pool
Configure PHP-FPM to work optimally with Caddy by adjusting the process manager and socket permissions.
# Find PHP-FPM version
php --version | head -1
Edit the www pool config (adjust path for your PHP version)
sudo nano /etc/php/8.*/fpm/pool.d/www.conf
Update these settings in the PHP-FPM pool configuration:
user = caddy
group = caddy
listen.owner = caddy
listen.group = caddy
listen.mode = 0660
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
Enable and start services
Enable both Caddy and PHP-FPM services to start automatically on boot and start them now.
sudo systemctl enable --now caddy
sudo systemctl enable --now php*-fpm
sudo systemctl status caddy
sudo systemctl status php*-fpm
Configure virtual hosts
Add additional virtual hosts to serve multiple domains from the same server.
sudo mkdir -p /var/www/html/blog.example.com
sudo chown -R caddy:caddy /var/www/html/blog.example.com
Add the virtual host configuration to your Caddyfile:
# Add this to your existing Caddyfile
blog.example.com {
root * /var/www/html/blog.example.com
file_server
php_fastcgi unix//run/php/php-fpm.sock
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
}
}
Configure firewall
Open the necessary ports for HTTP and HTTPS traffic. Caddy needs both ports to handle redirects and serve content.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
Reload Caddy configuration
Reload Caddy to apply the new configuration. Caddy will automatically obtain SSL certificates from Let's Encrypt.
sudo systemctl reload caddy
Verify your setup
Check that both Caddy and PHP-FPM are running properly and that SSL certificates were obtained successfully.
sudo systemctl status caddy
sudo systemctl status php*-fpm
caddy version
Check if certificates were obtained
sudo ls -la /var/lib/caddy/.local/share/caddy/certificates/
Test configuration syntax
sudo caddy validate --config /etc/caddy/Caddyfile
View recent logs
sudo journalctl -u caddy --lines=20
Visit https://example.com in your browser to confirm the SSL certificate is working and PHP information is displayed.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | PHP-FPM not running or wrong socket path | sudo systemctl start php*-fpm and check socket path in Caddyfile |
| Certificate not obtained | Domain not pointing to server or ports blocked | Check DNS records and firewall rules for ports 80/443 |
| Permission denied errors | Wrong file ownership | sudo chown -R caddy:caddy /var/www/html |
| Caddy won't start | Configuration syntax error | sudo caddy validate --config /etc/caddy/Caddyfile |
| PHP files download instead of execute | PHP-FPM not configured or wrong MIME types | Check php_fastcgi directive and restart services |
Next steps
- Install and configure NGINX with HTTP/3 and modern security headers
- Setup nginx reverse proxy with SSL certificates and security hardening
- Configure Caddy advanced load balancing with health checks
- Setup Caddy with Docker containers and automatic SSL
- Configure Caddy security headers and WAF rules
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Usage function
usage() {
echo "Usage: $0 <domain> [email]"
echo " domain: Your domain name (e.g., example.com)"
echo " email: Email for Let's Encrypt (optional, defaults to admin@domain)"
exit 1
}
# Error handler
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop caddy 2>/dev/null || true
systemctl stop php-fpm 2>/dev/null || true
systemctl stop php8.1-fpm 2>/dev/null || true
systemctl stop php8.2-fpm 2>/dev/null || true
exit 1
}
trap cleanup ERR
# Check arguments
if [ $# -lt 1 ] || [ $# -gt 2 ]; then
usage
fi
DOMAIN="$1"
EMAIL="${2:-admin@${DOMAIN}}"
# Validate domain format
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
echo -e "${RED}Invalid domain format: $DOMAIN${NC}"
exit 1
fi
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root or with sudo${NC}"
exit 1
fi
echo -e "${GREEN}Installing Caddy 2 with PHP-FPM for domain: $DOMAIN${NC}"
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
PKG_UPGRADE="apt upgrade -y"
PHP_FPM_SERVICE="php*-fpm"
PHP_FPM_SOCK="/run/php/php*-fpm.sock"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf check-update || true"
PKG_UPGRADE="dnf update -y"
PHP_FPM_SERVICE="php-fpm"
PHP_FPM_SOCK="/run/php-fpm/www.sock"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum check-update || true"
PKG_UPGRADE="yum update -y"
PHP_FPM_SERVICE="php-fpm"
PHP_FPM_SOCK="/run/php-fpm/www.sock"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
# Step 1: Update system
echo -e "${YELLOW}[1/10] Updating system packages...${NC}"
$PKG_UPDATE
$PKG_UPGRADE
# Step 2: Install required packages
echo -e "${YELLOW}[2/10] Installing required packages...${NC}"
if [ "$PKG_MGR" = "apt" ]; then
$PKG_INSTALL debian-keyring debian-archive-keyring apt-transport-https curl gnupg2
else
$PKG_INSTALL curl gnupg2 dnf-plugins-core
fi
# Step 3: Add Caddy repository
echo -e "${YELLOW}[3/10] 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
dnf copr enable @caddy/caddy -y 2>/dev/null || {
# Fallback for RHEL-based systems without copr
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /etc/pki/rpm-gpg/RPM-GPG-KEY-caddy
cat > /etc/yum.repos.d/caddy.repo << 'EOF'
[caddy-stable]
name=Caddy Stable
baseurl=https://dl.cloudsmith.io/public/caddy/stable/rpm/el/$releasever/$basearch
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-caddy
enabled=1
gpgcheck=1
EOF
}
fi
# Step 4: Install Caddy
echo -e "${YELLOW}[4/10] Installing Caddy 2...${NC}"
$PKG_INSTALL caddy
# Step 5: Install PHP-FPM
echo -e "${YELLOW}[5/10] Installing PHP-FPM...${NC}"
$PKG_INSTALL php-fpm php-cli php-mysql php-curl php-json php-mbstring php-xml php-zip
# Step 6: Configure PHP-FPM socket path for different distros
echo -e "${YELLOW}[6/10] Configuring PHP-FPM...${NC}"
if [ "$PKG_MGR" != "apt" ]; then
# Configure PHP-FPM to use socket on RHEL-based systems
PHP_FPM_CONF="/etc/php-fpm.d/www.conf"
if [ -f "$PHP_FPM_CONF" ]; then
sed -i 's/listen = 127.0.0.1:9000/listen = \/run\/php-fpm\/www.sock/' "$PHP_FPM_CONF"
sed -i 's/;listen.owner = nobody/listen.owner = caddy/' "$PHP_FPM_CONF"
sed -i 's/;listen.group = nobody/listen.group = caddy/' "$PHP_FPM_CONF"
sed -i 's/;listen.mode = 0660/listen.mode = 0660/' "$PHP_FPM_CONF"
sed -i 's/user = apache/user = caddy/' "$PHP_FPM_CONF"
sed -i 's/group = apache/group = caddy/' "$PHP_FPM_CONF"
mkdir -p /run/php-fpm
chown caddy:caddy /run/php-fpm
fi
fi
# Step 7: Create web directory structure
echo -e "${YELLOW}[7/10] Creating web directory structure...${NC}"
mkdir -p "/var/www/html/$DOMAIN"
chown -R caddy:caddy /var/www/html
chmod -R 755 /var/www/html
# Step 8: Create Caddyfile
echo -e "${YELLOW}[8/10] Creating Caddyfile configuration...${NC}"
mkdir -p /var/log/caddy
chown caddy:caddy /var/log/caddy
chmod 755 /var/log/caddy
# Determine PHP-FPM socket path
if [ "$PKG_MGR" = "apt" ]; then
PHP_SOCK_PATH="unix//run/php/php*-fpm.sock"
else
PHP_SOCK_PATH="unix//run/php-fpm/www.sock"
fi
cat > /etc/caddy/Caddyfile << EOF
{
email $EMAIL
admin localhost:2019
}
http://$DOMAIN {
redir https://$DOMAIN{uri} permanent
}
$DOMAIN {
root * /var/www/html/$DOMAIN
file_server
php_fastcgi $PHP_SOCK_PATH {
index index.php
}
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
Strict-Transport-Security "max-age=31536000; includeSubDomains"
Referrer-Policy strict-origin-when-cross-origin
}
log {
output file /var/log/caddy/$DOMAIN.log {
roll_size 100mb
roll_keep 5
roll_keep_for 720h
}
}
}
EOF
chown caddy:caddy /etc/caddy/Caddyfile
chmod 644 /etc/caddy/Caddyfile
# Step 9: Create test PHP file
echo -e "${YELLOW}[9/10] Creating test PHP file...${NC}"
cat > "/var/www/html/$DOMAIN/index.php" << 'EOF'
<?php
echo "<h1>Caddy 2 + PHP-FPM is working!</h1>";
echo "<p>Current time: " . date('Y-m-d H:i:s') . "</p>";
phpinfo();
?>
EOF
chown caddy:caddy "/var/www/html/$DOMAIN/index.php"
chmod 644 "/var/www/html/$DOMAIN/index.php"
# Step 10: Start and enable services
echo -e "${YELLOW}[10/10] Starting and enabling services...${NC}"
systemctl enable php-fpm
systemctl start php-fpm
systemctl enable caddy
systemctl start caddy
# Configure firewall
if command -v ufw &> /dev/null && ufw status | grep -q "Status: active"; then
ufw allow 80/tcp
ufw allow 443/tcp
elif command -v firewall-cmd &> /dev/null && systemctl is-active firewalld &> /dev/null; then
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
fi
# Verification
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${GREEN}Verifying services...${NC}"
if systemctl is-active --quiet caddy; then
echo -e "${GREEN}✓ Caddy is running${NC}"
else
echo -e "${RED}✗ Caddy is not running${NC}"
fi
if systemctl is-active --quiet php-fpm; then
echo -e "${GREEN}✓ PHP-FPM is running${NC}"
else
echo -e "${RED}✗ PHP-FPM is not running${NC}"
fi
echo ""
echo -e "${GREEN}Setup complete!${NC}"
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Point your domain $DOMAIN to this server's IP address"
echo "2. Visit https://$DOMAIN to test the installation"
echo "3. Replace /var/www/html/$DOMAIN/index.php with your website files"
echo "4. Caddy will automatically obtain SSL certificates from Let's Encrypt"
echo ""
echo -e "${YELLOW}Useful commands:${NC}"
echo "- Check Caddy status: systemctl status caddy"
echo "- Reload Caddy config: systemctl reload caddy"
echo "- View Caddy logs: journalctl -u caddy -f"
echo "- Edit Caddyfile: nano /etc/caddy/Caddyfile"
Review the script before running. Execute with: bash install.sh