How to serve a static site using Caddy
- Install Caddy on server
- Check Caddy is running
- Put the static files on the server
- A quick detour: start with HTTP
- Write a simple Caddyfile
- Use the new Caddyfile
- It isn't working...
- Edit Caddyfile to serve via HTTPS on your domain
- It isn't working... (round 2)
- TLDR
- Useful things
Here's a "how to" for serving a static site from Caddy, with a side of debugging advice and a few sysadmin tips.
When I started using Caddy, I read all the introductory documentation, including the quick start guides about Caddyfiles and serving static files. But I still didn't understand how simple things fit together and it took a while to work out how to serve a static site.
I'd also become used to automated deployments and was surprised by simple things like "Where should the files go on the server??" The how to covers a few of those topics as well.
As ever, skip to the TLDR if you just want the Caddyfile.
Install Caddy on server
The official website has good instructions for installing Caddy on your server. For Linux, the most common server OS, the instructions mostly involve running commands to add a new package repository.
Since there's some copy-pasting, it's worth following that old sysadmin
trick: type the comment symbol #
at the start of the line,
then paste the command. Because the command is a comment, it won't run
unexpectedly even if a newline gets copied into the terminal. When
you're sure the command is right, remove the #
comment
symbol and run.
Check Caddy is running
By default, Caddy runs in the background via systemd. Caddy seems to automatically start itself after installation, but it's worth double-checking using
$ systemctl status caddy
# Start of output
# ● caddy.service - Caddy
# Loaded: loaded (/lib/systemd/system/caddy.service; enabled; vendor preset: enabled)
# Active: active (running) since Fri 2022-06-03 07:32:56 UTC; 1 weeks 5 days ago
# Docs: https://caddyserver.com/docs/
Most of the time, you'll want Caddy to be running and enabled. (Where "running" means that Caddy is on and "enabled" means that Caddy will automatically start after a server reboot.) Luckily, these seem to be the default settings.
Put the static files on the server
For the purposes of the article, I'm going to assume the static site has a really simple directory structure:
static-site/
index.html
a-post.html
styles.css
At this stage, there are a couple of decisions to make.
Decision one is how you'll put files onto the server. There are lots of
ways of doing that, so it's really down to your preferences. For simple
static sites, I like to make the directory a git repo, clone it on the
server and git pull
to get updates. Depending on your
situation, all sorts of things from rsync
to a CI/CD
pipeline could be decent options.
Decision two is where the static site directory will live. I've found it
most convenient to put the static site directory inside the directory
/var/www
, e.g. /var/www/my-static-site
. It
seems to be a common location, so there's a decent chance it will be
familiar to other developers. Log files often go into the nearby
/var/log
(explained later), which keeps everything close
together in the directory structure.
Once you've decided where the files are going and how you'll do it, put the static files onto the server.
After copying the files, make sure the caddy
user can read
the static site directory. If the caddy user can't access the directory,
you'll get get permission errors when Caddy tries to access and serve
the files.
For those learning Linux,
ls -l
is a useful way of checking the owner and group
permissions of the directory. If the output is scary and unfamiliar,
it's worth reading this short overview of
file permissions.
A quick detour: start with HTTP
For simple static sites, I tend to start by serving the files via HTTP and without a domain.
A great feature of Caddy is the "automatic HTTPS", which means that Caddy gets the TLS certificate and keeps it renewed. But there's a gotcha - if you make lots of mistakes while setting up, you'll hit the rate limits of certificate providers. The Caddy documentation suggests the longest ban is 1 week.
So for a brand new Caddyfile I tend to check that everything works over HTTP. When I'm confident in the basic setup, I move to HTTPS.
An alternative approach is to use a certificate authority's staging URL while you're getting ready. Then swap to the certificate authority's main URL when going into production. It's worth considering for more complex sites, espeially if you might send or receive sensitive data during the initial setup.
Write a simple Caddyfile
At this point Caddy is running but not actually serving anything. It's time to write a Caddyfile, which will tell Caddy how to serve the site.
The default Caddyfile, created by Caddy on installation, is
/etc/caddy/Caddyfile
. Edit that file to add your own
settings.
This particular Caddyfile serves a static site via port 80 (HTTP, not HTTPS).
:80 {
root * /var/www/my-static-site
encode gzip
file_server {
hide .git
}
log {
output file /var/log/caddy/my-static-site.log
}
header {
?Cache-Control "max-age=1800"
}
}
Caddy has good defaults, so there's a lot less boilerplate than I tend to see in Nginx configs. You might even find that some of these settings are irrelevant to you and can be removed.
The :80 is a "block" and everything inside it is scoped to that block. You can have different blocks for different domains, perhaps serving different static sites.
In this case, Caddy is told to act as a file server, returning files
from the directory /var/www/my-static-site
. Caddy will
return the index.html when someone goes to your root URL. (The user
doesn't have to include index.html at the end of the URL.)
Inside the file_server
block is a line that says Caddy
should hide the .git
directory. In other words, Caddy
shouldn't serve those files and users shouldn't be able to navigate to
https://example.com/.git
The line encode gzip
means the files will be compressed
using gzip and the Content-Encoding in the
representation header
will say so. Compressing the response is recommended but not required.
The log block specifies what should happen to access logs. (The access
logs are different to the Caddy runtime logs, which are accessed through
systemd, meaning
systemctl status caddy
.) In this case, the access logs will
be saved to a file. Caddy creates its own log folder at
/var/log/caddy
and ensures the caddy user owns the
directory, so I've found it simplest to put the access logs there. Caddy
will use its
defaults for log rotation.
The header block specifies the Cache-Control, a.k.a. the header that tells browsers how long they should cache resources. Caddy usually passes through the Cache-Control header from the upstream server, which might be something like Gunicorn or uWSGI if you have a Python web application. But in this case there's no upstream server and no default Cache-Control. If you're minimising and versioning assets like CSS (e.g. with a hash in the file name), you might prefer to vary the Cache-Control so those files are cached forever. The Caddy docs have a few examples of this. Setting the Cache-Control is recommended but not essential.
Use the new Caddyfile
When your Caddyfile is ready, use
systemctl reload caddy
to tell Caddy to use the new
version. Your static site should appear at
http://server.ip.address
(It's the server's IP address because no domain was specified and it's http:// because there's no certificate yet.)
It isn't working...
This doesn't always work. Some tips for debugging:
-
Check Caddy's runtime logs using
systemctl status caddy
-
In the summary at the top, is Caddy "active (running)"? If it
isn't, systemd isn't running Caddy. Perhaps you restarted the
server and didn't have Caddy set to "enabled" (auto-restarting).
If that's the case, try
systemctl start caddy
. If that isn't the problem, you'll need to restart Caddy and carefully read the information that systemd provides (e.g. about why it can't run Caddy), then do some Googling. - Are there any runtime logs with "level": "error"? If yes, Caddy is running but has some internal error. Most often I've found the Caddyfile is invalid. Careful reading of the error messages usually reveals the reason.
-
In the summary at the top, is Caddy "active (running)"? If it
isn't, systemd isn't running Caddy. Perhaps you restarted the
server and didn't have Caddy set to "enabled" (auto-restarting).
If that's the case, try
-
Can you access the static site from the server itself?
-
On the server, try
curl localhost
(or equivalent using something like wget). Do you get the expected response? If that works but you can't access the site in your browser, it suggests that Caddy it serving the files but they can't be seen outside the server. That suggests a firewall error. -
Do you have
ufw
running? (Check usingufw status
.) - Does your cloud provider have a firewall whose settings must be changed via the web interface?
-
On the server, try
Edit Caddyfile to serve via HTTPS on your domain
If you can see your site in the browser, it's time to serve the site via HTTPS on your domain.
In your domain name provider's web interface, you'll need to set up the DNS A record and possibly other records. (If you aren't sure what that means, don't worry. It's what tells the world that a particular domain points to a particular IP address. The ins and outs are beyond the scope of this how to, but your domain name provider probably has simple instructions for setting up your DNS records.)
Once the DNS records are setup, edit the Caddyfile to use the domain name rather than port 80.
The diff will look something like this:
- :80 {
+ mydomain.com {
root * /var/www/my-static-site
encode gzip
file_server {
hide .git
}
log {
output file /var/log/caddy/my-static-site.log
}
header {
?Cache-Control "max-age=1800"
}
}
Only the start of the block has changed.
In this example, the site address is https://mydomain.com
.
If you've decided to use the www subdomain, the diff should instead
begin:
- :80 {
+ www.mydomain.com {
Tell Caddy to use the new version of the file by running
systemctl reload caddy
.
Wait a few moments and check the runtime logs using
systemctl status caddy
. Hopefully you'll see messages about
Caddy sucessfully getting a TLS certificate.
Check you can access https://mydomain.com from a web browser, replacing "mydomain" with your own domain.
It isn't working... (round 2)
Hopefully you tested everything earlier via HTTP and everything now works via HTTPS.
If the HTTP version was fine but the HTTPS version isn't working, the
most likely culprit is a failed ACME challenge. Check the runtime logs
using
systemctl status caddy
.
If the logs confirm the challenge failed, check your DNS records and the server firewall. In particular, are you allowing traffic on port 443, the standard port for HTTPS traffic?
It's also worth re-reading the first It isn't working... section and going through those debugging steps again.
These are by far the most common problems that I've encountered using Caddy, so hopefully your static site will be up and running in no time.
TLDR
The TLDR feels redundant on this one because the entire Caddyfile was included several times in the article. But here it is anyway.
Edit the default Caddyfile, which is /etc/caddy/Caddyfile
# /etc/caddy/Caddyfile
mydomain.com {
root * /var/www/my-static-site
encode gzip
file_server {
hide .git
}
log {
output file /var/log/caddy/my-static-site.log
}
header {
?Cache-Control "max-age=1800"
}
}
Use the command systemctl reload caddy
to tell Caddy to use
the new version of the Caddyfile.
Useful things
- Many pages in the Caddy docs have an "Examples" section at the end. Many examples show common patterns and give an idea of how people use a particular feature.
- If you're considering a content delivery network (CDN) but don't have a favourite, TechRadar has a useful writeup about CDN providers. Some CDN providers have a generous free tier.
- This link was in the main article, but Jack Wallen's short overview of Linux file permissions is unusually good, so I'm including it again.