Caddy is the modern HTTP/2-ready web server that uses simpler configuration than that of Nginx and Apache2. Its configuration server, the Caddyfile, supports virtual hosts like Nginx and Apache2. The most interesting part is like Caddy has the SSL-support baked into the system, in the sense that activating SSL only require one directive in the configuration as long as all requirements are met.

Installing Caddy

$ curl | bash
[] allow Caddy to use lower-level ports like port 80 and 443
$ sudo setcap cap_net_bind_service=+ep /usr/local/bin/caddy

Caddy as a systemd unit

File: caddy.service

Description=Caddy HTTP/2 Web Server

ExecStart=/usr/local/bin/caddy -agree=true -conf=/path/to/conf/Caddyfile


Be sure to replace $USER with the Caddy is associated with. After that, issue these commands.

$ sudo systemctl daemon-reload
$ sudo systemctl enable caddy.service
$ sudo systemctl start caddy.service


By default, when the configuration file is not explicitly specified, Caddy will look for Caddyfile in the folder when it is invoked. By adding -conf= argument, we can set where it should look for its configuration file. This is my configuration file for serving DokuWiki, Ghost, and Grav.

File: Caddyfile

## Ghost blog, NodeJS reverse-proxy with SSL {
  tls [email protected]
  proxy / localhost:2368 {
    header_upstream Host {host}
    header_upstream X-Real-IP {remote}
    header_upstream X-Forwarded-Proto {scheme}

## DokuWiki, listen to PHP at port 9000 {
  tls [email protected]
  fastcgi / php
  root /path/to/folder
  internal /data
  internal /conf
  internal /bin
  internal /inc

  rewrite /_media {
    r (.*)
    to /lib/exe/fetch.php?media={1}

  rewrite {
    to {path} {path}/ /doku.php?id={path_escaped}&{query}

## Serving Hugo (static HTML) {
  tls [email protected]
  root /path/to/folder

Pay attention to the tls directive: it is required for automatic SSL (works in conjunction with Let's Encrypt CA). If we do not wish to use the SSL, use tls off. Since we have granted Caddy access to lower level ports, Caddy can be started without sudo.

Another thing with PHP listening port or socket. Sometimes, listening port at 9000 does not work, so use the Unix socket instead.

fastcgi / /run/php/php7.2-fpm.sock php

Be sure to use the correct Unix socket and it depends on your PHP version.


The configuration above does not specific any logging capability. For a small website, it is understandable that logging is not that crucial. However, under certain cases like enabling the detection of intrusion or enabling the detection of potential (D)DoS attack, logging can be desirable.

Here we can specify caddy to store logs at our desired location. I choose HOME location because in the SystemD unit file I started caddy with the normal user, not root. So that makes sense. Here is the example logging (more information can be found here). This directive must be stored inside a virtual host definition:

## Ghost blog, NodeJS reverse-proxy with SSL {
  log /home/$USER/logs/access.log
    rotate {
    size 100  # rotate after 100 MB
    age 14    # keep log files for 14 days
    keep 10   # keep at most 10 log files
  tls [email protected]
  proxy / localhost:2368 {
    header_upstream Host {host}
    header_upstream X-Real-IP {remote}
    header_upstream X-Forwarded-Proto {scheme}

Fail2Ban Request Limit

Now that we have our access.log, the next course of action depends on the user. In my case, I use the generated access.log for Fail2Ban to monitor any sign of attack. Also, the access.log can be visualized with tools like GoAccess. Now, let's proceed with process to monitor our http server with Fail2Ban.

Create a new filter definition inside the folder /etc/fail2ban/filter.d:

File: caddy-req-limit.conf

failregex = ^<HOST> -.*"(GET|POST).*
ignoreregex =

And then, let's add new entry inside our /etc/fail2ban/jail.local file:

enabled = true
filter = caddy-req-limit
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
logpath = /home/nena/logs/caddy/access.log
findtime = 300
bantime = 7200
maxretry = 800

What's happening here is that fail2ban monitors all the request based on the definition in the filter. Here's the interesting part: it will only trigger the flag if there are more than 800 request (specified by maxretry = 800) in 300 seconds (specified by findtime = 300). This part depends on the user: if you are hosting a large and significant website, it makes sense to increase the number. But for a smaller site, I would even contest the idea of having this.