To take advantage of a low-resources VPS, I wanted to serve a static page but still have the ability to update that static page from a git push.

Caddy seemed like the best choice for a web server to back this. Not only is it lightweight, but it also has built-in SSL certificate setup with Let’s Encrypt.

Getting it to work was a bit of a challenge due to the general “you can do anything” Swiss army knife documentation common to open source projects. Here’s how I configured my VPS to pull from a private github repository whenever there is a push.

Install Caddy

Head to the Caddy website, and click “Download.” Pick your options, but make sure to include the git package.

Unpack the resulting tarball into a central location - I picked /etc/caddy and will keep using it from now on in this tutorial.

Create a user for Caddy

It’s not a good idea to run Caddy as root, so don’t. I created a new user with no home directory, and disabled its login shell:

useradd -M caddy
chsh -s /bin/false caddy

Next, you want to make sure permissions are correct. As root, let the caddy user take ownership of the /etc/caddy directory.

chown -R caddy /etc/caddy

Last, in order to bind to ports below 1024 as a ‘regular’ user, you should flag the caddy executable to enable this.

setcap cap_net_bind_service=+ep /etc/caddy/caddy

Note that you will need to re-run this command if you ever change the permissions of /etc/caddy again (e.g. you are tinkering with ownership).

Configure service with systemd

In order to run caddy as a service, you’ll want to tell systemd what is up.

I mostly followed this guide but ended up with the following caddy.service file in /etc/systemd/system:

Description=Caddy Web Server

ExecStart=/etc/caddy/caddy -agree -email PUT YOUR EMAIL HERE


Don’t forget to replace the obvious all-caps section with your own email address, otherwise caddy will not be able to ask Let’s Encrypt for help getting a certificate.

Configure Caddyfile to serve a page

I wanted to make sure at this point that caddy was installed properly, so I built the following Caddyfile in /etc/caddy/Caddyfile. {
    root /www/

Obviously, replace this with your domain.

Then I created a directory and put a test file into the directory:

mkdir -p /www/
echo Hello World >> /www/
chown -R caddy /www/

Start caddy and test

systemd needs to be told about its new charge. You can do so like this:

systemctl enable caddy

Then you can start caddy like this:

service caddy start

Hopefully at this point all has gone well!

You should see the following things:

  • /etc/caddy should now have a .caddy directory inside it that has some SSL certificates and other gubbins
  • service caddy status should report something positive
  • Navigating to your domain on port 80 should return the hello world string.

Congratulations! You have a working caddy instance serving static content from an unprivileged user.

Wire to the Github webhook

In order to have the page content update whenever there is a git push, we can use the “web hook” functionality of GitHub.

Broadly, whenever you do a push (or some other git commands), GitHub will reach out and punt your web server at a place of its choosing to tell it that the repo has updated, and it should pull a new version.

Create an SSH key for deployment

If you have a private repo for your content, like I did, you’ll need to do this.

First, run the shell command ssh-keygen and tell it to put the generated keys in /etc/caddy. Remember where those keys end up.

Next, navigate to the GitHub “Deploy Keys” page by opening your repository on the website, clicking on Settings, and then navigating to “Deploy Keys.”

Last, open up the ‘public’ key file (usually indicated by a *.pub extension) and copy and paste the contents into the GitHub “Deply Keys” page. I made the deploy key’s permissions read-only, which is a good idea in general.

Save it, and you should now have the key you just created authorized as a deploy key.

Create a GitHub webhook

While still on the GitHub settings page for your repository, navigate to Webhooks.

Create a new webhook, pointing at your domain, such as Remember both this URL and the secret key that you enter here.

Configure Caddy to use the GitHub repository and webhook

First, make sure the git binary is installed. If you’re on Debian or Ubuntu it’s as easy as doing apt-get install git. If you’re not, there is probably an equivalent for your distribution.

Open the Caddyfile and add configuration for the git plugin, so that it looks sort of like this in the end: {
        root /www/
        git {
                key /etc/caddy/YOUR_GENERATED_KEY_LOCATION
                hook /github-says-hello YOUR_WEBHOOK_SECRET

Replace the obvious all-caps text with the secret you defined in the last step. Keep in mind if your secret has special characters, you will need to put quote marks around it.

Delete all the stuff from the directory where you created the temporary index.html file in a previous step. It needs to be clear in order for git clone to succeed.

Restart Caddy through systemd, and check journalctl for success. It should have worked! Now you can update your git repo and have the changes magically appear a few seconds later in your caddy machine.

Troubleshooting and Snags

Can’t bind to port :80, :443

You need to re-do the setcap command from above. It might have gotten wiped away when a permission was updated.

Can’t clone the git repo because it can’t make /home/caddy/.ssh

I had a problem where the $HOME environment variable didn’t make it from the systemd service config for caddy all the way to the call that caddy made to git.

My hacky solution was to use usermod -d to reset the home directory of caddy to be /etc/caddy such that it had a place to put the .ssh/known_hosts file that it made the first time it connected to GitHub. I believe this may be a bug in caddy-git or possibly just me misusing systemd.