FreeBSD.software
Home/Guides/How to Set Up a FreeBSD Web Server with Caddy
tutorial·2026-04-09·10 min read

How to Set Up a FreeBSD Web Server with Caddy

Complete guide to setting up Caddy web server on FreeBSD: Caddyfile configuration, automatic HTTPS, reverse proxy, PHP-FPM integration, and comparison with NGINX.

How to Set Up a FreeBSD Web Server with Caddy

Caddy is a modern web server written in Go that handles HTTPS automatically. It obtains and renews TLS certificates from Let's Encrypt without any manual configuration. On FreeBSD, Caddy runs as a native service and integrates well with the system's package and rc.d infrastructure. This guide covers installation, Caddyfile configuration, automatic HTTPS, reverse proxy setups, PHP-FPM integration, and how Caddy compares to NGINX.

Why Caddy

Caddy's primary advantage is zero-configuration HTTPS. Point it at a domain, and it handles certificate issuance, renewal, OCSP stapling, and HTTP-to-HTTPS redirect automatically. No certbot cron jobs, no certificate path configuration, no renewal scripts.

Other notable features:

  • Caddyfile: a human-readable configuration format
  • JSON API for dynamic configuration without restarts
  • Built-in reverse proxy with health checks and load balancing
  • HTTP/2 and HTTP/3 (QUIC) support by default
  • Automatic certificate management via ACME protocol
  • Single static binary with no external dependencies
  • Extensible through Go plugins

Prerequisites

  • FreeBSD 14.0 or later
  • Root access
  • A domain name with DNS pointing to your server's IP (required for automatic HTTPS)
  • Ports 80 and 443 available (not used by another web server)

Step 1: Install Caddy

Install from the FreeBSD package repository:

sh
pkg install caddy

This installs the Caddy binary, a default Caddyfile, and an rc.d service script.

Verify Installation

sh
caddy version caddy list-modules

Enable at Boot

sh
sysrc caddy_enable="YES"

File Locations

| Path | Purpose |

|------|---------|

| /usr/local/bin/caddy | Caddy binary |

| /usr/local/etc/caddy/Caddyfile | Main configuration file |

| /var/log/caddy/ | Log directory |

| /var/db/caddy/data/ | Certificate and ACME data storage |

| /var/db/caddy/config/ | Auto-saved JSON config |

| /usr/local/www/ | Suggested document root location |

Step 2: Basic Caddyfile Configuration

The Caddyfile is Caddy's configuration format. It is concise and readable compared to NGINX or Apache configuration files.

Serve a Static Site

Edit /usr/local/etc/caddy/Caddyfile:

sh
example.com { root * /usr/local/www/example.com file_server }

That is it. When Caddy starts, it will:

  1. Obtain a TLS certificate for example.com from Let's Encrypt
  2. Redirect HTTP to HTTPS
  3. Serve files from /usr/local/www/example.com
  4. Enable HTTP/2 and HTTP/3

Create the document root:

sh
mkdir -p /usr/local/www/example.com echo "<h1>Caddy works</h1>" > /usr/local/www/example.com/index.html chown -R www:www /usr/local/www/example.com

Start Caddy:

sh
service caddy start

Local Development (No HTTPS)

For local testing without a public domain, use localhost or :port:

sh
:8080 { root * /usr/local/www/mysite file_server }

Or use Caddy's internal CA for local HTTPS:

sh
localhost { root * /usr/local/www/mysite file_server }

Caddy generates a self-signed root CA certificate for localhost. You can trust it in your browser for local development.

Step 3: Automatic HTTPS

Caddy's automatic HTTPS works when three conditions are met:

  1. The site address in the Caddyfile is a publicly resolvable domain name
  2. Ports 80 and 443 are accessible from the internet
  3. DNS A/AAAA records point to the server

How It Works

When Caddy encounters a domain name in its configuration:

  1. It checks if a valid certificate exists in /var/db/caddy/data/caddy/certificates/
  2. If not, it starts the ACME challenge (HTTP-01 or TLS-ALPN-01)
  3. Let's Encrypt validates domain ownership
  4. Caddy stores the certificate and key
  5. Caddy sets up automatic renewal (before expiry)
  6. OCSP stapling is enabled automatically

Multiple Domains

sh
example.com, www.example.com { root * /usr/local/www/example.com file_server } blog.example.com { root * /usr/local/www/blog file_server }

Each domain gets its own certificate. Caddy handles all of them automatically.

Using Wildcard Certificates

Wildcard certificates require DNS-01 challenge. Caddy supports DNS providers via plugins:

sh
*.example.com { tls { dns cloudflare {env.CF_API_TOKEN} } root * /usr/local/www/{labels.2} file_server }

This requires building Caddy with the DNS provider module (see custom builds below).

Disabling Automatic HTTPS

For specific sites or development:

sh
http://example.com { root * /usr/local/www/example.com file_server }

Prefixing with http:// explicitly disables HTTPS for that site.

Step 4: Reverse Proxy

Caddy is an excellent reverse proxy. The syntax is minimal.

Basic Reverse Proxy

Proxy all traffic to a backend application:

sh
app.example.com { reverse_proxy localhost:3000 }

Caddy automatically handles HTTPS on the frontend and proxies to the HTTP backend.

With Path Matching

sh
example.com { reverse_proxy /api/* localhost:8080 root * /usr/local/www/frontend file_server }

Load Balancing

Distribute traffic across multiple backends:

sh
app.example.com { reverse_proxy localhost:3001 localhost:3002 localhost:3003 { lb_policy round_robin health_uri /health health_interval 10s } }

WebSocket Proxy

Caddy proxies WebSocket connections transparently:

sh
ws.example.com { reverse_proxy localhost:8080 }

No special configuration is needed. Caddy detects the Upgrade header and handles the WebSocket handshake.

Proxy Headers

Pass original client information to the backend:

sh
app.example.com { reverse_proxy localhost:3000 { header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto {scheme} } }

Caddy sets X-Forwarded-For and X-Forwarded-Proto by default, but you can customize header behavior.

Step 5: PHP-FPM Integration

Caddy works well with PHP-FPM for serving PHP applications.

Install PHP-FPM

sh
pkg install php83 php83-extensions php83-curl php83-gd \ php83-mbstring php83-mysqli php83-xml php83-zip \ php83-opcache php83-session php83-ctype php83-dom sysrc php_fpm_enable="YES" service php-fpm start

Configure Caddyfile for PHP

sh
example.com { root * /usr/local/www/example.com php_fastcgi unix//var/run/php-fpm.sock file_server }

The php_fastcgi directive is a shorthand that configures:

  • FastCGI transport to PHP-FPM
  • Index file resolution (index.php)
  • PATH_INFO splitting
  • Try files with fallback to index.php

WordPress Configuration

sh
wordpress.example.com { root * /usr/local/www/wordpress php_fastcgi unix//var/run/php-fpm.sock file_server # WordPress permalink support @notStatic { not path /wp-admin/* /wp-includes/* /wp-content/* not file } rewrite @notStatic /index.php # Block access to sensitive files @blocked { path /xmlrpc.php path /.htaccess path /wp-config.php } respond @blocked 403 }

Laravel Configuration

sh
laravel.example.com { root * /usr/local/www/laravel/public php_fastcgi unix//var/run/php-fpm.sock file_server encode gzip # Laravel routing try_files {path} {path}/ /index.php?{query} }

Step 6: Compression and Caching

Enable Compression

sh
example.com { encode gzip zstd root * /usr/local/www/example.com file_server }

Caddy supports gzip and zstd compression. It negotiates the best encoding with the client automatically.

Cache Headers for Static Assets

sh
example.com { root * /usr/local/www/example.com @static { path *.css *.js *.png *.jpg *.jpeg *.gif *.svg *.woff2 } header @static Cache-Control "public, max-age=31536000, immutable" file_server }

Step 7: Security Headers

Add security headers globally or per site:

sh
example.com { header { Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "SAMEORIGIN" Referrer-Policy "strict-origin-when-cross-origin" Permissions-Policy "camera=(), microphone=(), geolocation=()" -Server } root * /usr/local/www/example.com file_server }

The -Server directive removes the Server header from responses.

Step 8: Logging

Access Logging

sh
example.com { log { output file /var/log/caddy/example.com-access.log { roll_size 100mb roll_keep 10 roll_keep_for 720h } format json } root * /usr/local/www/example.com file_server }

Caddy supports JSON and console log formats. JSON is easier to parse with tools like jq, Elasticsearch, or Loki.

View Logs

sh
# Tail access logs tail -f /var/log/caddy/example.com-access.log | python3 -m json.tool # Filter for errors cat /var/log/caddy/example.com-access.log | \ python3 -c "import sys,json; [print(json.dumps(l)) for l in (json.loads(line) for line in sys.stdin) if l.get('status',0) >= 400]"

Step 9: The JSON API

Caddy exposes a REST API for dynamic configuration. The admin endpoint listens on localhost:2019 by default.

sh
# View current configuration curl http://localhost:2019/config/ | python3 -m json.tool # Reload configuration from Caddyfile caddy reload --config /usr/local/etc/caddy/Caddyfile # Add a route dynamically (JSON API) curl -X POST http://localhost:2019/config/apps/http/servers/srv0/routes \ -H "Content-Type: application/json" \ -d '{ "match": [{"host": ["new.example.com"]}], "handle": [{"handler": "static_response", "body": "Hello"}] }'

The API enables configuration changes without restarts. This is useful for dynamic environments, CI/CD pipelines, and service meshes.

Step 10: Custom Caddy Builds

The packaged Caddy binary includes core modules. For additional modules (DNS providers, rate limiting, etc.), build a custom binary using xcaddy:

sh
pkg install go go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest # Build with Cloudflare DNS plugin xcaddy build --with github.com/caddy-dns/cloudflare # Replace system binary service caddy stop cp caddy /usr/local/bin/caddy service caddy start

Caddy vs NGINX

| Feature | Caddy | NGINX |

|---------|-------|-------|

| Auto HTTPS | Built-in, zero-config | Requires certbot + cron |

| Config Format | Caddyfile (concise) | nginx.conf (verbose) |

| HTTP/3 | Built-in | Requires compile flag |

| Live Config Reload | JSON API (no restart) | nginx -s reload |

| Reverse Proxy | Built-in, simple syntax | Built-in, more options |

| Performance (static) | Good | Excellent |

| Performance (proxy) | Good | Excellent |

| Memory Usage | Higher (Go runtime) | Lower (C) |

| Plugin Ecosystem | Growing | Mature, extensive |

| Community Size | Smaller | Very large |

| Learning Curve | Low | Medium |

Choose Caddy when:

  • You want automatic HTTPS without configuration
  • You need a simple reverse proxy
  • You value configuration simplicity
  • You want HTTP/3 support without custom compilation
  • You are building a small to medium traffic site

Choose NGINX when:

  • You need maximum static file performance
  • You require advanced load balancing features
  • You need fine-grained caching control
  • You are running high-traffic infrastructure
  • You need a well-known configuration format that your team knows

For a detailed NGINX guide, see NGINX Production Setup on FreeBSD.

Troubleshooting

Certificate Issuance Fails

sh
# Check Caddy logs for ACME errors tail -50 /var/log/caddy/caddy.log # Verify DNS resolves to this server host example.com # Verify ports 80 and 443 are accessible sockstat -4 -l | grep -E ':80|:443'

Common causes: DNS not pointing to the server, port 80 blocked by firewall, another service already on port 80/443.

Caddy Fails to Start

sh
# Validate configuration caddy validate --config /usr/local/etc/caddy/Caddyfile # Test with verbose output caddy run --config /usr/local/etc/caddy/Caddyfile

Permission Issues

Caddy runs as the www user by default on FreeBSD. Ensure document roots and log directories are accessible:

sh
chown -R www:www /usr/local/www/example.com chown -R www:www /var/log/caddy chown -R www:www /var/db/caddy

PHP-FPM Connection Refused

Verify PHP-FPM is running and the socket exists:

sh
service php-fpm status ls -la /var/run/php-fpm.sock

Ensure the PHP-FPM pool is configured to listen on the same socket path used in the Caddyfile. Check /usr/local/etc/php-fpm.d/www.conf:

sh
listen = /var/run/php-fpm.sock listen.owner = www listen.group = www

Frequently Asked Questions

Does Caddy support .htaccess files?

No. Caddy does not read .htaccess files. All configuration is done in the Caddyfile or via the JSON API. If you are migrating from Apache, you need to translate .htaccess rules to Caddyfile directives.

Is Caddy fast enough for production?

Yes. Caddy handles thousands of concurrent connections efficiently. For pure static file serving, NGINX is faster. For most web applications behind a reverse proxy, the difference is negligible. Caddy is used in production by organizations of all sizes.

Can I use Caddy with Let's Encrypt staging?

Yes. Useful for testing without hitting rate limits:

sh
example.com { tls { ca https://acme-staging-v02.api.letsencrypt.org/directory } root * /usr/local/www/example.com file_server }

How do I use Caddy with a non-standard port?

sh
example.com:8443 { root * /usr/local/www/example.com file_server }

Note that automatic HTTPS still works, but HTTP-01 challenge requires port 80 to be accessible for verification.

Can Caddy serve as a load balancer?

Yes. The reverse_proxy directive supports multiple backends with health checks, round-robin, least connections, and other policies. For simple load balancing, Caddy works well. For complex traffic management, consider HAProxy.

How do I restrict access by IP?

sh
admin.example.com { @blocked not remote_ip 10.0.0.0/8 192.168.1.0/24 respond @blocked 403 reverse_proxy localhost:8080 }

Does Caddy support rate limiting?

Not in the default build. Install the caddy-ratelimit module via xcaddy for rate limiting capabilities.

How do I proxy to a Unix socket?

sh
example.com { reverse_proxy unix//var/run/app.sock }

Can I run Caddy in a FreeBSD jail?

Yes. Caddy runs well inside jails. Ensure the jail has network access and that certificate storage paths (/var/db/caddy/) are persistent across jail restarts.

How do I migrate from NGINX to Caddy?

Translate your NGINX server blocks to Caddy site blocks. The most significant differences: remove SSL certificate configuration (Caddy handles it), translate location blocks to matchers, and replace proxy_pass with reverse_proxy. The simplification is substantial for most configurations.

Get more FreeBSD guides

Weekly tutorials, security advisories, and package updates. No spam.