Setting Up Caddy
Now that DNS is set up, I needed a web server to run as a proxy. This allows us to use domain names with standard web ports (80 & 443) instead of all the unique 4 digit ports we'll eventually have everywhere. This is basically an extension to the convenience we started by setting up a DNS server.
The Software: Caddy
Caddy is a web server like Apache and Nginx. What makes it different is that it has Let's Encrypt certificates by default and a very simple configuration format. I know that Let's Encrypt can be set up in other web servers, but it's built into Caddy and there are plugins that connect to DNS services to automate the renewal. Normally, using Let's Encrypt requires you to have port 80 open on the server (which in our case, would also require me to open it on my firewall). By using a DNS plugin, I can skip opening my firewall, and still get all the benefits of Let's Encrypt.
There is a caveat to using plugins in Caddy. It's written in the Go programming language, and Go doesn't have a native way to dynamically connect to plugins that are built separately of the main program. Developers have handled this in various ways. Caddy requires you to build the software with the plugins built in into the main software. Caddy provides instructions for doing that in the terminal, but you can also do that from their own Downloads page. I used the latter because I didn't feel like doing it the terminal way.
I get my domains from Porkbun, and use their provided DNS servers. However, a lot of DNS plugins have been written by the community, and yours is probably supported.
I went to the Downloads page, and did the following:
- Set my platform to Linux amd64
- Searched for porkbun in the features and modules search bar
- Selected the
caddy-dns/porkbun
module (you'll see a blue border around it when selected) - Clicked the blue Download button.
The website built and downloaded a custom Caddy binary which I renamed to caddy
. I then rsynced the file to my home server into the root directory. Where I began work on...
Setting up the Container
Unlike other software, I need to build my own container since I'm using a custom binary. I skip a lot of the work by using the official Caddy image as a base, and simply replacing the binary with the one I got form the Caddy website. In my /root
folder, I created a place for custom Dockerfiles. Though because I'm using Podman, I named it containerfiles
. Podman's default build file is a Containerfile (because it's not Docker), but the format is the same and Podman can still use a Dockerfile without any change in process.
Inside my containerfiles
folder, I created a caddy
folder and moved my custom caddy binary into it. I also created a Containerfile
that with the following:
FROM docker.io/library/caddy:2.9.1-alpine
COPY caddy /usr/bin/caddy
RUN chmod +x /usr/bin/caddy
As you can see, I'm starting with the official Caddy image. Note that the image is versioned. You'll want to use the same version image as the version of Caddy you're running. If you're unsure what version of Caddy you downloaded, you can make it executable with chmod +x caddy
and then run caddy version
. It'll output something like v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=
.
The important part is to grab the version number without the v
and put that into the container URL. I'm using version 2.9.1 so I'm pulling the image docker.io/library/caddy:2.9.1-alpine
.
After that, I copy my Caddy binary into the container at the right location, and make sure it's executable with the appropriate RUN
command.
Finally, build the new image with Podman:
podman build -t caddy-proxy .
Make sure to be in the same folder as the Containerfile
. The -t caddy-proxy
is what gives the image its name. I chose something different that the official name so I can tell the difference at a glance.
With our image built, I created a new file at /etc/containers/systemd/caddy.container
and put the following in it:
[Unit]
Description=Caddy Proxy Server
[Install]
WantedBy=multi-user.target default.target
[Service]
Restart=always
ExecReload=podman exec systemd-caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
[Container]
Image=caddy-proxy:latest
Volume=/srv/caddy/conf:/etc/caddy:Z
Volume=caddy-data.volume:/data
PublishPort=80:80
PublishPort=443:443
You'll notice that our Image
is simply a name, and not a full URL. Since I'm not publishing the image anywhere, I just use the name and Podman knows to look locally first.
Our first Volume
tells the container to look for the configuration files in /srv/caddy/conf
, and our second Volume
is a Podman volume because it just contains data that I want to persist, but don't really need to manage.
Because this is not a pod, we need to set our ports here with PublishPort
. We have two that we care about 80 and 443 for http and https. I'm including both just in case there's a situation where I don't want or can't use https. That hasn't happened so far, but you never know.
You might notice something different here. In Our [Service]
group we have an ExecReload
setting. Those familiar with SystemD will recognize it as a standard Exec setting along with Start and Stop. Additionally, a feature of Caddy is the ability to reload the config file without restarting or dropping requests. Here I've combined them together so I have access to that feature. We do that with the podman exec
command (docker has an identical feature). This command gives users the ability to run a shell command against a running container. By default, containers set up using these container files have an ID of systemd-name
with the name being the name of the container. So our full command tells podman to tell our Caddy container to run the reload command. Which is pretty neat.
Next, I created my caddy volume at /etc/containers/systemd/caddy-data.volume
and gave it the following:
[Volume]
Now to configure Caddy. Using our naming pattern, I created a folder for my config file at /srv/caddy/conf
. Inside there I create a new config file called Caddyfile
, and gave it this:
{
acme_dns porkbun {
api_key {api key}
api_secret_key {api secret}
}
}
# DNS Admin
pdns.custom.com {
reverse_proxy 172.18.10.204:9191
}
There's two main components to this. The first is the global config, which is the first set of curly braces ( { ... }
). Inside there is the config setting for using DNS for Let's Encrypt certificates. If you're using a different plugin, then you'll need to look at that plugin's docs to see what you should put here.
If you have some things that will be running as http, and you don't want Caddy auto-redirecting to https, then you will want to add auto_https disable_redirects
just above the acme_dns setting. It'll look like this:
{
auto_https disable_redirects
acme_dns porkbun {
api_key {api key}
api_secret_key {api secret}
}
}
All sites will still be https by default, so you'll need to change one thing to make it http. Which I will get to in a moment.
The second part of the config is the site definitions. Each service will be a different site in Caddy with their own configs. Good news is that they're simple. Here's my config for the PowerDNS Admin:
# DNS Admin
pdns.custom.com {
reverse_proxy 10.10.10.10:9191
}
The line that starts with a #
is a comment. I suggest heavily commenting your config to help remind you of the service it's for. Next, we start by writing out the domain we're using for the site. Here I've put pdns.custom.com
. If you're using a private TLD, it'll be something like pdns.internal
.
After that, we add curly braces, and inside the curly braces we put our site config. Most of the sites will be set up as a reverse proxy. We can see that in our config above using the reverse_proxy
setting with a value of 10.10.10.10:9191
. If you recall from yesterday, our PowerDNS Admin UI is running on port 9191. You may also notice that the IP is not localhost ( 127.0.0.1
). The reason for that is because the container is basically a tiny computer itself. When you point it to 127.0.0.1
, the container thinks it's referencing itself rather than the server where it is running. So we put the full IP of the server itself.
Technically, there's another address we could use, but that goes into Podman networking. That is a tad complicated, and not very worth our time. This works, and is easy to remember.
Earlier, you may recall I mentioned running some sites as http, and not https. I'm not doing that currently, but if you need to it's easy to do. Simply add http://
before the domain name. So for our DNS site from above, it would look like:
# DNS Admin
http://pdns.custom.com {
reverse_proxy 10.10.10.10:9191
}
See? Very simple.
Starting Our Web Server
With everything set up, we start like last time by refreshing SystemD with:
systemctl daemon-reload
Then we start our new Caddy container with:
systemctl start caddy
We still have the same functions as last time to manage it with:
systemctl restart caddy
systemctl stop caddy
systemctl status caddy
Additionally, using our ExecReload setting from earlier gives us the reload command for reloading our Caddy config.
systemctl reload caddy
And if we run into any issues, we can check the logs through journalctl:
journalctl -xeu caddy.service
The Conclusion
Our web proxy is all set up. All we need to do is go back and log into our PowerDNS Admin (using the IP address still), and set DNS records that are being proxied to point at this server. Once you have a pdns
subdomain pointed to Caddy you can log into the PowerDNS Admin using its domain name.
With our core software in place, we're ready to start setting up the fun stuff. Our first service: Jellyfin!