Using Caddy web-server to enable HTTP/3 protocol
Introduction
Installing caddy
Installing caddy
Configuring caddy
Accessing logs
Using tcpdump
to inspect http/3
protocol
Using jq
to make logs readable as csv
Final note
Introduction
In this article we will install Caddy web server, in order to make use of the http/3
protocol on the hosted web-site.
Caddy will serve web-pages using the older http/1.1
protocol for older web browsers.
The server will also automatically renew the SSL web-site certificate and, when using http/3
, browsers will use TLS 1.3
, a secure protocol, and UDP
, instead of TCP
; this reduces latency and saves bandwidth.
The current apache
server only uses http/1.1
.
An apache
web-site is already configured and up at /var/www/html/public
.
The site has two public IP addresses, IPv4 and IPv6.
Installing caddy
Let’s install on the Debian 13 web server.
We will add the caddy
repository and package signing key to the Debian list:
sudo apt update
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/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 chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
We can check if the web server is installed:
$ caddy version
v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=
Configuring caddy
Let’s stop and disable apache
web server:
sudo systemctl stop apache2.service
sudo systemctl disable apache2.service
We will configure our website in the Caddy configuration file:
sudo vim /etc/caddy/Caddyfile
The file below:
- configures the web site for two domains
georgetech.co.uk
and itswww
subdomain - uses
/var/www/html/public
as the root website folder - has an
http 404
redirect for non-existent web pages (page not found) - logs are a maximum of 50 MiB, are kept for 168h/7 days, and the last 5 logs are kept
- the access log is in
json
format, which can be parsed by many applications and viajq
command
georgetech.co.uk, www.georgetech.co.uk {
root * /var/www/html/public
file_server
handle_errors {
@notFound {
expression {http.error.status_code} == 404
}
rewrite @notFound /404.html
respond "Redirecting to custom 404 page" 404
}
log {
output file /var/log/caddy/access.log {
roll_size 50mb
roll_keep 5
roll_keep_for 168h
}
format json
}
Let’s check if the web server is up via the command systemctl status caddy.service
:
admin@ip-172-31-34-228:~$ systemctl status caddy.service |tee
● caddy.service - Caddy
Loaded: loaded (/usr/lib/systemd/system/caddy.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-08-05 21:49:53 BST; 14h ago
Invocation: 8475274b89af4cfb8e5d8401ba8a1b98
Docs: https://caddyserver.com/docs/
Process: 2401 ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force (code=exited, status=0/SUCCESS)
Main PID: 1434 (caddy)
Tasks: 8 (limit: 1126)
Memory: 17M (peak: 30M)
CPU: 2.797s
CGroup: /system.slice/caddy.service
└─1434 /usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
Aug 06 11:07:41 ip-172-31-34-228 caddy[1434]: {"level":"info","ts":1754474861.3565729,"logger":"admin.api","msg":"load complete"}
Aug 06 11:07:41 ip-172-31-34-228 caddy[1434]: {"level":"info","ts":1754474861.358689,"logger":"admin","msg":"stopped previous server","address":"localhost:2019"}
We notice that the server is up, using the specified configuration file.
Accessing logs
Before checking the logs, let’s add our user to the caddy
group, so that we can view its logs without using sudo
or root
account privileges:
sudo usermod -aG caddy $USER
sudo chmod 750 /var/log/caddy
As per the below, the /var/log/caddy
log folder is owned by the caddy
user and group:
$ ls -lha /var/log/caddy/
total 456K
drwxr-x--- 2 caddy caddy 4.0K Aug 6 12:55 .
drwxr-xr-x 11 root root 4.0K Aug 5 19:59 ..
-rw-r----- 1 caddy caddy 442K Aug 6 12:49 access.log
We can check the latest server access log entries with the command:
tail -F /var/log/caddy/access.log | jq .
It will read the latest lines at the end of the log and make them more readable, as per the below:
{
"level": "info",
"ts": 1754480441.1481783,
"logger": "http.log.access.log0",
"msg": "handled request",
"request": {
"remote_ip": "54.36.148.83",
"remote_port": "27509",
"client_ip": "54.36.148.83",
"proto": "HTTP/2.0",
"method": "GET",
"host": "georgetech.co.uk",
"uri": "/",
"headers": {
"User-Agent": [
"Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)"
],
"Accept": [
"*/*"
],
"Accept-Encoding": [
"deflate, gzip, br"
],
"If-None-Match": [
"\"5abe-63abe50d1baed-gzip\""
],
"If-Modified-Since": [
"Fri, 25 Jul 2025 10:22:14 GMT"
]
},
"tls": {
"resumed": false,
"version": 772,
"cipher_suite": 4865,
"proto": "h2",
"server_name": "georgetech.co.uk"
}
},
"bytes_read": 0,
"user_id": "",
"duration": 0.000393785,
"size": 23341,
"status": 200,
"resp_headers": {
"Server": [
"Caddy"
],
"Alt-Svc": [
"h3=\":443\"; ma=2592000"
],
"Vary": [
"Accept-Encoding"
],
"Etag": [
"\"dburnyjr5gigi0d\""
],
"Content-Type": [
"text/html; charset=utf-8"
],
"Last-Modified": [
"Tue, 05 Aug 2025 20:12:19 GMT"
],
"Accept-Ranges": [
"bytes"
],
"Content-Length": [
"23341"
]
}
}
You can also use the command below to show the access log and navigate it via less
, with Page Up/Page Down, /
for searching, etc.
jq -C . /var/log/caddy/access.log|less -R
Let’s use curl
to access the website via http/3
:
curl -I --http3 https://georgetech.co.uk
We can confirm that the site is using this protocol as per the result:
HTTP/3 200
accept-ranges: bytes
content-length: 23341
date: Wed, 06 Aug 2025 11:48:09 GMT
server: Caddy
vary: Accept-Encoding
etag: "dburnyjr5gigi0d"
content-type: text/html; charset=utf-8
last-modified: Tue, 05 Aug 2025 20:12:19 GMT
Using tcpdump
to inspect http/3
protocol
Another way of confirming that http/3
is used is to run tcpdump
on the web-server to filter all UDP
traffic for port 443
.
sudo tcpdump -n udp port 443
The output below confirms that a client initiated a http/3
connection to the server:
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enX0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:49:04.021687 IP6 2a00:23ee:1160:41e2:5062:1a88:x:x.38694 > 2a05:d01c:b42:4600:4458:5312:ebc0:1110.443: UDP, length 1230
12:49:04.021874 IP6 2a00:23ee:1160:41e2:5062:1a88:x:x.38694 > 2a05:d01c:b42:4600:4458:5312:ebc0:1110.443: UDP, length 1230
12:49:04.022153 IP6 2a05:d01c:b42:4600:4458:5312:ebc0:1110.443 > 2a00:23ee:1160:41e2:5062:1a88:x:x.38694: UDP, length 37
Using jq
to make logs readable as csv
The script caddy-json2csv.sh
converts the logs from the json
format to csv
, so that it is easier to import in a spreadsheet:
#!/usr/bin/env bash
set -euo pipefail
# caddy-json2csv.sh
# Convert Caddy JSON access.log into CSV with ISO timestamps.
#
# Usage:
# ./caddy-json2csv.sh [ACCESS_LOG] [OUTPUT_CSV]
#
# Defaults:
# ACCESS_LOG=/var/log/caddy/access.log
# OUTPUT_CSV=./caddy_access.csv
ACCESS_LOG=${1:-/var/log/caddy/access.log}
OUTPUT_CSV=${2:-caddy_access.csv}
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq not found. Install jq ≥1.6 to get ISO timestamps." >&2
exit 1
fi
# CSV header: add iso_ts before raw epoch ts
echo "iso_ts,epoch_ts,level,remote_ip,client_ip,method,host,uri,status,size,duration,user_agent" > "$OUTPUT_CSV"
jq -r '
[
( .ts | todate ), # iso 8601 string
( .ts | tostring ), # raw epoch seconds
.level,
.request.remote_ip,
.request.client_ip,
.request.method,
.request.host,
.request.uri,
(.status | tostring),
(.size | tostring),
(.duration | tostring),
( (.request.headers["User-Agent"]? // []) | join(" ") )
]
| @csv
' "$ACCESS_LOG" >> "$OUTPUT_CSV"
echo "Wrote CSV with ISO timestamps to $OUTPUT_CSV"
To use it, run this example
./caddy-json2csv.sh /var/log/caddy/access.log mycaddy.csv
The corresponding csv
log file will contain the same information as the json
, ready to be imported as a comma-separated table:
iso_ts,epoch_ts,level,remote_ip,client_ip,method,host,uri,status,size,duration,user_agent
"2025-08-05T20:49:56Z","1754426996.7734258","info","x.y.63.44","x.y.63.44","GET","www.georgetech.co.uk","/","200","23341","0.003654646","Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0"
Final note
Make sure you open the firewall ports for incoming UDP
traffic on port 443
for 0.0.0.0./0
and ::/0
, to allow web-browser access to the site via http/3
.
In AWS EC2, the below show the additions to the security group associated with the EC2 instance, inbound rules:
Rule ID | Port Range | Protocol | Source | Description |
---|---|---|---|---|
sgr-0a264e17a2ea4d6c9 | 443 | UDP | 0.0.0.0/0 | HTTP/3 for Caddy web server |
sgr-040ceca60def0590c | 443 | UDP | ::/0 | HTTP/3 for Caddy web server |