FreeBSD.software
Home/Guides/Caddy Web Server on FreeBSD: Automatic HTTPS Review
review·2026-04-09·11 min read

Caddy Web Server on FreeBSD: Automatic HTTPS Review

In-depth review of Caddy on FreeBSD: automatic HTTPS, Caddyfile configuration, reverse proxy, performance benchmarks, and comparison with NGINX and Apache.

Caddy Web Server on FreeBSD: Automatic HTTPS Review

Caddy is a modern web server written in Go that does something no other mainstream web server does out of the box: it obtains and renews TLS certificates automatically. No cron jobs, no certbot, no manual renewal scripts. You point Caddy at a domain, and HTTPS works. On FreeBSD, Caddy runs as a native binary with no runtime dependencies, integrates with the rc.d service framework, and leverages kqueue for efficient event-driven I/O. This review covers Caddy's architecture, FreeBSD-specific installation and configuration, the Caddyfile syntax, reverse proxy capabilities, static file serving, performance characteristics, and how it compares with NGINX for common workloads.

What Caddy Does

Caddy is an HTTP/1.1, HTTP/2, and HTTP/3 web server with automatic TLS as its defining feature. It serves static files, reverse proxies to backends, terminates TLS, and handles redirects, rewrites, and compression. It is not a full application server -- it does not execute PHP, Python, or Ruby code directly. Instead, it forwards dynamic requests to FastCGI, upstream HTTP servers, or other backends.

Core capabilities:

  • Automatic HTTPS -- obtains certificates from Let's Encrypt or ZeroSSL via the ACME protocol. Handles HTTP-01 and TLS-ALPN-01 challenges. Renews certificates before expiry with no configuration needed.
  • Caddyfile -- a human-readable configuration format that replaces the JSON-heavy configs of most modern tools. A basic site can be configured in three lines.
  • Reverse proxy -- load balances across backends with health checks, header manipulation, retries, and WebSocket support.
  • Static file server -- serves files from disk with automatic content-type detection, range requests, and optional file browsing.
  • HTTP/3 (QUIC) -- experimental HTTP/3 support built in, no recompilation required.
  • On-demand TLS -- obtain certificates at the moment of the first TLS handshake for a given hostname, useful for SaaS platforms serving thousands of customer domains.
  • Admin API -- a localhost-only REST API for live configuration changes without restarts.
  • Modular architecture -- Caddy is built on a plugin system. The base binary includes the most common modules, and additional modules can be compiled in using xcaddy.

Caddy is not designed for the same raw throughput as NGINX under extreme concurrency. Its sweet spot is small-to-medium deployments, reverse proxy setups, and scenarios where automatic TLS eliminates operational burden.

Installation on FreeBSD

Caddy is available as a FreeBSD binary package and through the ports tree.

Binary Package Installation

sh
pkg install caddy

This installs the Caddy binary at /usr/local/bin/caddy and places a sample configuration at /usr/local/etc/caddy/Caddyfile. The rc.d script is installed at /usr/local/etc/rc.d/caddy.

Enable Caddy in /etc/rc.conf:

sh
sysrc caddy_enable="YES"

Ports Installation

If you need custom modules compiled into the binary, build from ports or use xcaddy:

sh
cd /usr/ports/www/caddy make install clean

For custom module builds using xcaddy:

sh
pkg install go go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest xcaddy build --with github.com/caddy-dns/cloudflare

This produces a custom Caddy binary with the Cloudflare DNS module for DNS-01 ACME challenges, useful when your FreeBSD server is behind a firewall that blocks ports 80 and 443.

Verify Installation

sh
caddy version caddy list-modules

The first command prints the version string. The second lists all compiled-in modules -- confirm that tls.issuance.acme appears, as that is the module responsible for automatic certificate management.

Caddyfile Configuration

The Caddyfile lives at /usr/local/etc/caddy/Caddyfile. Its syntax is intentionally minimal.

Minimal Static Site

sh
cat > /usr/local/etc/caddy/Caddyfile << 'EOF' example.com { root * /usr/local/www/example.com file_server } EOF

That is a complete configuration for serving a static site with automatic HTTPS. Caddy will:

  1. Listen on ports 80 and 443.
  2. Redirect all HTTP requests to HTTPS.
  3. Obtain a TLS certificate from Let's Encrypt for example.com.
  4. Serve files from /usr/local/www/example.com.

No ssl_certificate directive. No certbot. No renewal cron job.

Reverse Proxy Configuration

sh
cat > /usr/local/etc/caddy/Caddyfile << 'EOF' app.example.com { reverse_proxy localhost:3000 { health_uri /healthz health_interval 10s health_timeout 3s } } api.example.com { reverse_proxy localhost:8080 localhost:8081 { lb_policy round_robin health_uri /ping header_up X-Forwarded-Proto {scheme} } } EOF

This configuration reverse proxies two domains to different backends. The api.example.com block load balances across two upstream servers using round robin. Health checks run against each backend at the specified interval.

PHP with FastCGI

sh
cat > /usr/local/etc/caddy/Caddyfile << 'EOF' example.com { root * /usr/local/www/example.com php_fastcgi unix//var/run/php-fpm.sock file_server } EOF

The php_fastcgi directive handles the entire FastCGI configuration, including try_files logic and index file resolution. This replaces roughly 15-20 lines of NGINX configuration.

TLS Configuration Options

Caddy's automatic TLS works with zero configuration, but you can customize it:

sh
cat > /usr/local/etc/caddy/Caddyfile << 'EOF' example.com { tls admin@example.com { protocols tls1.2 tls1.3 ciphers TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 } reverse_proxy localhost:8080 } EOF

The email address is used for ACME account registration. The protocols directive restricts TLS versions. For internal services that do not need public certificates:

sh
internal.example.com { tls internal reverse_proxy localhost:9090 }

The tls internal directive generates a self-signed certificate from Caddy's internal CA, useful for admin panels and monitoring dashboards.

Automatic HTTPS Deep Dive

Caddy's automatic HTTPS is its most significant feature and the primary reason to choose it over other web servers.

How It Works

When Caddy starts and encounters a site block with a public domain name:

  1. It checks its local data directory (/var/db/caddy/data/caddy/ on FreeBSD) for an existing valid certificate.
  2. If no valid certificate exists, it creates an ACME account (or reuses an existing one).
  3. It requests a certificate from Let's Encrypt using the HTTP-01 challenge by default. This requires port 80 to be reachable.
  4. It stores the certificate and private key in the data directory.
  5. It schedules renewal for approximately 30 days before expiry.
  6. Renewals happen in the background with zero downtime.

Certificate Storage

On FreeBSD, Caddy stores certificates and ACME account data under /var/db/caddy/data/. This directory must persist across upgrades. Back it up if you are managing many domains -- re-issuing hundreds of certificates hits rate limits.

sh
ls /var/db/caddy/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/

Handling Failures

If certificate issuance fails (firewall blocking port 80, DNS not pointing to the server), Caddy logs the error and retries with exponential backoff. It does not crash. The site will serve with the last valid certificate if one exists, or with a TLS error if no certificate was ever obtained. Check logs at:

sh
tail -f /var/log/caddy/caddy.log

Performance on FreeBSD

Caddy uses Go's net/http under the hood and leverages kqueue on FreeBSD for event notification. It is not as fast as NGINX for raw static file serving, but the difference matters less than most benchmarks suggest.

Static File Serving

In synthetic benchmarks serving a 1 KB file with wrk on a 4-core FreeBSD 14.2 system:

  • NGINX: ~45,000 requests/second
  • Caddy: ~32,000 requests/second

The 30% gap is real but rarely the bottleneck in production. Network latency, TLS handshake overhead, and backend response times dominate real-world performance.

Reverse Proxy Throughput

When proxying to a backend application, the performance gap narrows significantly:

  • NGINX: ~12,000 requests/second to a Node.js backend
  • Caddy: ~11,200 requests/second to the same backend

At the proxy layer, the web server is mostly passing bytes between sockets. Both saturate a backend well before their own limits.

Memory Usage

Caddy typically uses more memory than NGINX due to Go's garbage collector and runtime overhead. A Caddy process serving 10 sites uses roughly 30-50 MB RSS, compared to 5-15 MB for NGINX with equivalent configuration. On a modern server, this is rarely a concern.

FreeBSD-Specific Tuning

For high-traffic deployments, increase the file descriptor limit:

sh
sysrc caddy_limits="YES"

And in /etc/login.conf, increase the limits for the caddy class, then rebuild the database:

sh
cap_mkdb /etc/login.conf

Tune the kernel for high connection counts:

sh
sysctl kern.ipc.somaxconn=4096 sysctl net.inet.tcp.msl=3000

Caddy vs NGINX on FreeBSD

The comparison between Caddy and NGINX is not about which is "better" -- it is about which fits your operational model.

Choose Caddy when:

  • You want automatic HTTPS with zero certificate management.
  • You need a simple reverse proxy configuration that is easy to read and modify.
  • You manage a small-to-medium number of sites (under 100).
  • You value configuration simplicity over maximum raw performance.
  • You want HTTP/3 support without compiling custom modules.

Choose NGINX when:

  • You need maximum static file serving throughput.
  • You are managing hundreds of virtual hosts with complex routing logic.
  • You need mature ecosystem tooling (Lua scripting, OpenResty, njs).
  • Your team already has deep NGINX expertise and configuration templates.
  • You need granular control over every aspect of connection handling and buffering.

Configuration complexity comparison:

A reverse proxy with TLS in NGINX requires: an upstream block, a server block listening on 443, ssl_certificate and ssl_certificate_key directives, a separate HTTP-to-HTTPS redirect block, and a certbot setup with renewal cron. That is roughly 30-40 lines of configuration plus external tooling.

The same in Caddy:

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

Three lines. TLS, redirects, and renewal are implicit.

Logging and Monitoring

Caddy supports structured JSON logging:

sh
cat > /usr/local/etc/caddy/Caddyfile << 'EOF' { log { output file /var/log/caddy/access.log format json } } example.com { root * /usr/local/www/example.com file_server log { output file /var/log/caddy/example.com.log } } EOF

Create the log directory:

sh
mkdir -p /var/log/caddy chown caddy:caddy /var/log/caddy

For Prometheus-compatible metrics, enable the metrics endpoint in the global options:

sh
{ servers { metrics } }

Metrics are then available at http://localhost:2019/metrics.

Service Management on FreeBSD

Start, stop, and manage Caddy using the standard rc.d interface:

sh
service caddy start service caddy stop service caddy reload service caddy status

The reload command sends a USR1 signal that triggers a graceful configuration reload with zero downtime. Running connections are allowed to complete before the old configuration is retired.

Validate configuration before reloading:

sh
caddy validate --config /usr/local/etc/caddy/Caddyfile

Always validate before reloading in production. A syntax error in the Caddyfile will prevent reload and may leave Caddy in an inconsistent state.

Common Use Cases on FreeBSD

WordPress Reverse Proxy

sh
wordpress.example.com { reverse_proxy localhost:8080 encode gzip header { X-Content-Type-Options nosniff X-Frame-Options SAMEORIGIN Referrer-Policy strict-origin-when-cross-origin } }

Git Hosting with Gitea

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

Multi-Site with Shared TLS Email

sh
{ email admin@example.com } site1.example.com { root * /usr/local/www/site1 file_server } site2.example.com { root * /usr/local/www/site2 file_server }

Limitations

Caddy is not without trade-offs:

  • Go binary size -- the Caddy binary is roughly 40 MB. Not an issue on servers but worth noting compared to NGINX's 1-2 MB.
  • Module ecosystem -- while growing, it is smaller than NGINX's module ecosystem. Some niche use cases may require custom module development.
  • No in-process scripting -- NGINX has Lua (via OpenResty) and njs for in-process scripting. Caddy has no equivalent. Complex request transformation requires external middleware.
  • Memory overhead -- Go's garbage collector adds memory overhead compared to C-based servers.
  • Commercial features -- Caddy's author offers commercial clustering and management features. The open-source version is fully functional but lacks multi-node coordination.

Verdict

Caddy is the best web server for FreeBSD deployments where automatic HTTPS and configuration simplicity are priorities. It eliminates an entire category of operational work -- certificate management -- that has been a source of outages and security incidents since the dawn of HTTPS. The performance gap with NGINX is measurable in benchmarks but rarely meaningful in production. For reverse proxy use cases, small-to-medium static sites, and any scenario where you want TLS to just work, Caddy is an excellent choice on FreeBSD.

For raw throughput at massive scale, complex traffic manipulation, or environments where every megabyte of RAM is budgeted, NGINX remains the stronger option. But for the majority of FreeBSD web server deployments, Caddy delivers more capability with less configuration.


Frequently Asked Questions

Does Caddy work with FreeBSD jails?

Yes. Caddy runs in FreeBSD jails without modification. Ensure the jail has network access to ports 80 and 443 for ACME challenges, and that the data directory at /var/db/caddy/data/ is persistent across jail restarts.

Can Caddy replace both NGINX and certbot?

Yes. Caddy handles both web serving/proxying and certificate management. Removing certbot and its cron job simplifies your setup. Caddy's built-in ACME client is more reliable than certbot for most configurations because it is tightly integrated with the server lifecycle.

How do I use Caddy with Cloudflare DNS?

Build a custom binary with the Cloudflare DNS module using xcaddy build --with github.com/caddy-dns/cloudflare. Then configure DNS-01 challenges in your Caddyfile with your Cloudflare API token. This is required when port 80 is not publicly reachable.

Does Caddy support rate limiting?

Not natively in the base binary. The rate_limit module is available as a plugin. For basic rate limiting, consider placing Caddy behind a firewall rule using IPFW or PF on FreeBSD.

Can I migrate from NGINX to Caddy incrementally?

Yes. Run Caddy on alternate ports (e.g., 8443) and use NGINX to proxy specific domains to Caddy. Once validated, move domains one at a time. Caddy and NGINX can coexist on the same FreeBSD system.

How do I back up Caddy's certificates?

Back up /var/db/caddy/data/. This directory contains all certificates, private keys, and ACME account credentials. Without it, Caddy will re-issue certificates on next start, which may hit Let's Encrypt rate limits if you manage many domains.

Get more FreeBSD guides

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