In this post I’m going to deploy a static website with
kamal-skiff, a Hetzner Cloud VM and Cloudflare.
I’m going to use GitHub and its private container registry
ghcr.io to upload docker images.
For this post let’s pick
<demo> as a sample GitHub username and the repository name. If you want to follow this guide, you need to replace those values with your own username and repository name.
Basic development setup
To begin with development, I’m creating a private GitHub repository called
gem install kamal-stiff, I’m running
$ skiff new demo && cd demo
to setup the project.
Now let’s commit push the generated files to the private
$ git init . $ git add . $ git commit -m 'initial commit' $ git remote add origin email@example.com:<YOUR GITHUB USERNAME>/<demo>.git $ git push -u origin main
Run the demo site with
skiff dev and open
localhost:4000. You can update the
index.html and see your changes after a browser reload.
Setting up ghcr.io
Instead of using Docker Hub as an image repository, I’m configuring the setup to use the GitHub container repository.
Therefore I need to change the
deploy.yml and point it to the GitHub container repository by replacing the whole existing
registry: configuration with:
registry: server: ghcr.io username: <username> password: - GITHUB_REGISTRY_TOKEN
Now I’m generating the
If you’re logged into GitHub you can reach it at https://github.com/settings/tokens.
For reading and writing packages to
ghcr.io we need to create a “classic” token.
To create a classic token with minimal permissions you actually need to visit this magic link, because just clicking the packages scopes will select more permissions than theoretically required. Check out the documentation to see a detailed explanation on how to create such a token and how to login the registry.
After creating the token, add it to
Before pushing a first image to
ghcr.io, I’m adding a label to our Dockerfile to connect the pushed image with our new private repostitory:
FROM nginx:latest LABEL org.opencontainers.image.source="https://github.com/<username>/<demo>"
You can test your setup by building and pushing an image to the repository with:
$ docker build . -t ghcr.io/<username>/<demo>:latest $ docker push ghcr.io/<username>/<demo>:latest
If everything worked correctly, you should see a new package under
packages correctly connected with your private git repository.
Creating the Hetzner VM
In this step I’ll create a Hetzner Cloud VM to run our container image.
After logging in to Hetzner Cloud, I’ll create a new Project called
I recommed to create a new
ed25519 SSH-Keypair to connect to your Hetzner VMs, follow this link for an explanation on how to do so.
As a next step, I’ll add a new server to the new
<demo> Hetzner cloud project. Click on “Add server” and configure a server with:
- Image Ubuntu 22.04
- Shared vCPU with ARM64, e.g. CAX11
- Public IPv4 address
- Add your newly generated SSH Public Key
servers: default ip
192.168.0.1 with your actual public Hetzner VM IP that was assigned to your VM.
After the server has been initialized, you should be able to ssh into it with
ssh root@<your server ip>.
If you can connect, you can continue with the next step.
Deploying with kamal-skiff
Before you can deploy your site, you need to create an access token to clone your GitHub repository. Skiff uses this token to pull the newest changes in the Git repository and updates the deployed site.
I’m using a “fine grained” token, with minimal permissions to access the
<demo> repository. Go to https://github.com/settings/personal-access-tokens/new and select:
- Repository access to “Only select repositories” and select
- Permissions “Contents” with “Access: Read-only”
Click on “Generate token” and add the following line to the
GIT_URL=https://<username>:<your generated token>@github.com/<username>/<demo>.git
You can run
skiff deploy and watch the site being deployed on your cloud VM. If everything works fine, you should see your site with
curl <your server ip>.
Adding a domain name and SSL with Cloudflare
I’m going to use Cloudflare to set up DNS and SSL for the freshly deployed website.
Go to the Cloudflare dashboard and register a domain name under
Domain Registration. In the next step you need to configure a DNS A-Record. Click on
Websites and click on your bought domain name. Now go to
DNS -> Records and create an A-Record with the name
example.com and your cloud vm’s ipv4 address. Keep the Proxy status to “proxied”.
After a few minutes, your site is reachable via your configured domain name.
If you check the
SSL/TLS config, you’ll see that the SSL encryption mode is set to
flexible. As the illustration shows, your traffic is encrypted between the browser and the cloudflare proxy, which is the reason the browser shows a green lock and no SSL warning. Congrats! 🎉
I spent some time in the past weeks playing around with Hetzner and Kamal and I’m actually very impressed by the simplicity and performance. I think
kamal-skiff is a good introduction to Kamal so I encourage you to give it a try as well. I think a similar tool to manage the maintenance and base configuration of VM tasks would be great. E.g. for setting up a secure base image, manage backups, harden SSH configs etc.
- It would be great if GitHub would allow more fine-grained permissions for the container registry access tokens.
- You should harden your SSH setup on your VM, configure a strict firewall and configure unattended upgrades.
- You should probably use a Cloudflare SSL setup that encrypts end to end instead of