Roll Your Own Static Site Host on VPS with Caddy Server

This blog post will teach you how to set up a static host on a virtual private server with Ubuntu, Caddy server, SSL, and SFTP access.

I recently had to work on a project with some interesting requirements:

  • Host a static website
  • With a user-friendly file upload process
  • Running on a custom domain
  • With an SSL certificate
  • And have root access to the server (for…reasons)

Most of those requirements would actually be very easy to fulfill with a 3rd party provider. But that last one. That’s the tricky one. Most service providers don’t give you root level access. Which means I’d have to do this all myself on a Virtual Private Server (VPS).

And I did, so I thought I’d share in case you (or future me) ever have these same needs.

Provision VPS

First things first, you’ll need your own VPS. I used Linode. It’s very fast, reliable, and affordable. Plus, if you don’t have an account, they usually have some promo that can get you started with some free credits.

Yay!

(Full disclosure: I work for Akamai which now owns Linode, but this is my blog and I get to write whatever I want. I recommend them because I like them, not because I work for Akamai. But it does help that I get to use it for free. So for the sake of transparency, there may be some bias, but I hope you use them because they’re good.)

If you have a Linode account, you can go to cloud.linode.com/linodes to create a new server. Or if you have an account somewhere else, do the same there. The rest should be mostly the same.

We need to provision a new server, determine the Linux distro, location, and resources. I went with Ubuntu (what I’m used to) in Fremont, CA (closest to my users/me) on the Nanode 1 GB instance (cheapest one they got; $5; lol).

For a static site, it’s probably good to integrate a CDN as well, but that was not in the cards for this project.

The last thing is setting up access to your server. You’ll have to set up the root user’s password, but I’d highly recommend generating SSH keys. If you do, you won’t need to remember the password.

Either way, once your server is provisioned, you should be able to see the server’s IP address (like 50.100.7.293). You can use that to SSH into the server by opening up your terminal and typing:

ssh root@50.100.7.293

The first time you do this, you might be asked if you trust server. You do. Trust me.

If you did not use a SSH keys, you’re a naughty child and will be punished by having to type the password any time you want to log in. If you did use SSH keys, congrats. You should be in the server already. (Seriously, you really should use SSH keys)

The last step here is technically optional, but it’s a good idea to run the update command (if you’re logged in as “root” you don’t need the sudo, but meh, habit):

sudo apt update

Setup Caddy Web Server

Caddy is an open source web server written in Go. It works well for static hosting, reverse proxies, and more, and one of the coolest features is that it has HTTPS built in.

To be honest, I thought this part was going to be a lot more difficult than it turned out to be, but Caddy is great. First, we’ll follow the instructions to install Caddy on our server (note that each line is a separate command):

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
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 apt update
sudo apt install caddy

And with that, you should be able to go to your server’s IP address and see the default Caddy website.

Connect Domain

This is another step that might be slightly different for you depending on your DNS provider, but the gist should be the same.

Log in to wherever you manage DNS from (likely your domain name registrar). Modify your DNS settings by adding a new A record. Set the name to whatever you want the subdomain to be, or use ‘@’ for the root domain, and set the value to your server’s IP address.

Once that’s done, you can go to your domain, and you should see the default Caddy website. Conveniently, they even include example next steps on the page. We’ll be following those shortly, but with a small modification.

Create SFTP Access

In my project, I wanted to give another person access to the server to be able to upload files. They didn’t want to use the terminal, but they were familiar enough with FTP. So my solution was to give them SFTP access by creating a new user.

This was also easier than I had originally thought it might be:

adduser username

This will prompt you to set up a password and some optional extra details about the user. Once that’s done, you can share the username and password with whomever you like to give them access to the server.

Then you can use an FTP client like Filezilla that supports SFTP. Just be sure to use the sftp:// protocol in the URL (sftp://50.100.7.293 or sftp://your.domain.com).

I like this system because it’s great for folks that prefer a GUI, and it’s possible to restrict the user’s access to only their home folder.

Serve The Website

Alright, onto the last step. We need to tell Caddy how to serve our website. Caddy can be used for several different things like a reverse proxy or running php_fastcgi, but today I’m focusing on serving static files. For that, we’ll need a folder to use as the root.

Caddy’s default webpage suggests putting the folder at /var/www/html, but I like to create a folder matching the domain, inside the user’s home directory. That way we can support multiple domains and it’s relatively intuitive what each folder is responsible for.

You can create a folder with Filezilla, or if you’re in the terminal with this command:

sudo mkdir /home/username/your.domain.com

Next, you’ll need an index.html file in there. Again, you can use Filezilla, the terminal, or even VS Code. Here’s a command to create an index.html file and write some HTML into it:

echo '<h1>Nugget is a good dog</h1>' > /home/username/your.domain.com/index.html

It’s a very basic website, but it really delivers.

Ok, now we have everything we need to run our website. We just need to tell Caddy where the website lives. We can do that by editing the default Caddyfile:

sudo nano /etc/caddy/Caddyfile

The file should contain a configuration block opening with :80 and setting up a static server from the folder /usr/share/caddy. There are also several commented lines with examples of other things you can do. To learn even more, check out the Caddyfile tutorial.

For our website, we need to replace the :80 with our domain, and the root server folder with the one we created a moment ago. It should look something like this:

your.domain.com {
  root * /home/username/your.domain.com
  file_server
}

You’ll want to swap out your details, but once you’ve done so, the last step is to reload the Caddy service.

systemctl reload caddy

After a few moments, you should be able to reload your website and see the new index.html file contents being loaded there.

If you want to make changes in the future, you just need to replace the HTML file, not restart the server.

Rad! But that’s not all. There’s a couple other cool things that Caddy takes care of for us:

  • Provisioning an SSL certificate
  • Installing it on the server
  • Redirecting from HTTP to HTTPS
  • Renewing SSL certificates before expiration

And all of that comes out of the box. I’ve really been impressed by how nice and easy it is to work with compared to some of the hoops I’ve had to jump through in the past.

That’s not so say it’s better than an alternative like NGINX. But it’s certainly easier. I’m not an expert enough to say, but for my projects, it’s more than good enough.

Next Steps

That’s as far as we’re going to go today, but I thought I’d leave you with some other ideas on what you could do next.

Learn more about Caddy

We really just scratched the surface on what Caddy can do, but I’d encourage you to learn more by heading over to caddyserver.com. One thing, for example, that we did not do was set compression on our server. That would be a great performance improvement. You can read more of the available options in the docs.

Limit user access

As I mentioned when we created the SFTP user account, that user may have access to a lot more than they need. If you just want to give someone access to edit the contents of their home folder, or just the website folder, you can do that. It’s probably a good security practice.

Setup firewall rules

Along the same lines of security, our server currently allows public access to any port. If you’re to run several applications, it might be a good idea to limit what port can be reach publicly and only allow access through a reverse proxy.

Fortunately, Ubuntu has the built-in UncomplicatedFirewall tool which is, well, an uncomplicated firewall. You can check the status of the firewall with this command:

sudo ufw status

By default, it should be ‘inactive’. You can activate it with the command:

sudo ufw enable

If you do enable it, make sure you open up access for the ports you’ll need including 22, 80, and 443. Those allow access for SSH, HTTP, and HTTPS protocols respectively.

If you enable the firewall and exit without opening up SSH access, you may lock yourself out of the server. Don’t ask me how I know.

Anyway, you can allow access to those ports with these commands:

sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https

(You could also explicitly provide the port numbers)

You can do even more with this firewall, but that’s beyond the scope of today’s post.

Automate deployments

As it stands, making changes to the website is a bit of a manual process. And one thing that many services offer today are ways to automatically deploy your changes when they land in a Git repo.

I haven’t actually built this myself, but it would be super cool to do, and I think I’ve got a simple concept down. You could set up a web server that can listen for webhooks. Then use your CI/CD pipeline (maybe GitHub Actions) to ping that webhook whenever a new change arrives on the production branch.

When the webhook is hit, it can trigger a bash script that uses Git to pull down the changes, run any necessary build command, and copy the production artifacts over to the website folder.

I’m sure it sounds easier than it is and I missed some things, but you get the idea. If you do build it, let me know. I’d love to check it out.

But for now, that’s all I’ve got. Adios amigos!

Thank you so much for reading. If you liked this article, please share it. It's one of the best ways to support me. You can also sign up for my newsletter or follow me on Twitter if you want to know when new articles are published.


Originally published on austingil.com.