Compare commits
No commits in common. "2abae63f4b8ada77a131d777ab8b0d0d1b056942" and "a89ad736b2b686ce6e3d6a327f0619c6c3a38a8a" have entirely different histories.
2abae63f4b
...
a89ad736b2
6 changed files with 1135 additions and 1028 deletions
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from "astro/config";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import { defineConfig } from "astro/config";
|
||||
import remarkEmoji from "remark-emoji";
|
||||
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
|
|
@ -30,7 +30,6 @@ export default defineConfig({
|
|||
],
|
||||
|
||||
adapter: cloudflare({
|
||||
imageService: "compile",
|
||||
platformProxy: {
|
||||
enabled: true,
|
||||
},
|
||||
|
|
|
|||
12
package.json
12
package.json
|
|
@ -17,17 +17,17 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/cloudflare": "^12.6.0",
|
||||
"@astrojs/mdx": "^4.3.0",
|
||||
"@astrojs/rss": "^4.0.12",
|
||||
"@astrojs/sitemap": "^3.4.1",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"@astrojs/cloudflare": "^12.2.1",
|
||||
"@astrojs/mdx": "^4.0.8",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/tailwind": "^6.0.0",
|
||||
"@cloudflare/workers-types": "^4.20241230.0",
|
||||
"@fontsource/fira-sans": "^5.1.1",
|
||||
"@fontsource/ubuntu": "^5.1.0",
|
||||
"@iconify-json/mdi": "^1.2.1",
|
||||
"@iconify-json/simple-icons": "^1.2.14",
|
||||
"astro": "^5.10.0",
|
||||
"astro": "^5.3.0",
|
||||
"astro-icon": "^1.1.4",
|
||||
"drizzle-orm": "^0.38.3",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
|
|
|
|||
2030
pnpm-lock.yaml
generated
2030
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,114 +0,0 @@
|
|||
---
|
||||
pubDate: "2025-06-21"
|
||||
title: "From Basement to Cloud: Migrating to Hetzner"
|
||||
description: "This post explains how to build a highly available infrastructure using Hetzner, Terraform, and Ansible—integrating services like Traefik for reverse proxying, Authentik for centralized authentication, and automated backups for seamless recovery."
|
||||
keywords:
|
||||
- hetzner
|
||||
- vps
|
||||
- ansible
|
||||
- terraform
|
||||
- timetagger
|
||||
- infrastructure
|
||||
---
|
||||
|
||||
Now that I graduated and started freelancing, I require some software infrastructure with higher availability guarantees than what I have from my basement.
|
||||
Typically, an ISP selling consumer-grade internet access guarantees uptime between 95% and 99%, which is ridiculously low. I've had several outages in the past, so I do not want to rely on my consumer-grade internet to host my infrastructure.
|
||||
A popular cloud alternative is Hetzner, a medium-sized German cloud provider whose principal office is just a few kilometers from my former employer in Unterföhring (Munich).
|
||||
Although they do not offer any SLA, which surprised me, people online have had positive experiences overall.
|
||||
Aside from that, their pricing is very competitive: if something goes wrong with Hetzner, I can still migrate away to another provider -- hopefully with minimal effort, which is what this post is about.
|
||||
|
||||
[Hetzner](https://www.hetzner.com/)
|
||||
|
||||
What kind of services do I need?
|
||||
|
||||
- Timetagger (time tracking)
|
||||
- VideoVault (video-on-demand platform I am currently developing)
|
||||
- Forgejo (self-hosted Git platform)
|
||||
- something for calendar, contacts, notes
|
||||
|
||||
Utilities:
|
||||
|
||||
- Authentik (single sign-on for all other services and potential client logins)
|
||||
- Traefik (reverse proxy)
|
||||
- Cloudflare-companion (DNS management)
|
||||
- ntfy (push notifications)
|
||||
|
||||
## Terraforming
|
||||
|
||||
Requesting a VM with code from Hetzner is trivial. They provide an awesome [Terraform provider](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs).
|
||||
One interesting detail is that I am using the Cloudflare provider to set up DNS records for the three zones I manage.
|
||||
This is useful because it allows me to CNAME onto these DNS records later.
|
||||
In my tfvars, I simply define:
|
||||
|
||||
```terraform
|
||||
cloudflare_dns_records = [{
|
||||
zone_id = "zone1"
|
||||
name = "hetzner-fsn1-vm-001.shiverpeak.xyz"
|
||||
type = "A"
|
||||
proxied = false
|
||||
},
|
||||
{
|
||||
zone_id = "zone2"
|
||||
name = "hetzner-fsn1-vm-001.daichendt.one"
|
||||
type = "A"
|
||||
proxied = false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Another noteworthy aspect is that I use the null resource to call Ansible playbooks to configure the VM.
|
||||
There are two playbooks: the first prepares the machine, like setting up another user, adding SSH keys, etc. And the second one is the centerpiece of this post: it installs and configures the services I need.
|
||||
|
||||
## Ansible Playbooking
|
||||
|
||||
All my secrets are stored in a vault file in `ansible/vars/vault.yml`. The configuration is stored in `ansible/group_vars/all.yml`.
|
||||
A separate role installs and configures every service.
|
||||
|
||||
### Traefiking
|
||||
|
||||
Traefik is a popular reverse proxy in the cloud-native ecosystem. I like it because I can keep the DNS and proxy configurations close to the application via Docker labels.
|
||||
Although Caddy is also quite simple to use, arguably even simpler than Traefik, it only provides a centralized configuration file (maybe something has changed there now?).
|
||||
Traefik will automatically create a TLS certificate with ACME - Let's Encrypt for every service configured with the label `traefik.http.routers.<service>.tls.certresolver=`.
|
||||
|
||||
The last step is to automatically configure DNS records for the services. Traefik does not have a native way to do this, but there is a handy project called [cloudflare-companion](https://github.com/tiredofit/docker-traefik-cloudflare-companion). After supplying it with an API key with access to all DNS zones, it will automatically create CNAME records on the previously defined A records.
|
||||
Getting the config right was a little tricky since I needed 3 different zones (and I did not bother to read the README, mhm). The full config is [here](https://git.shiverpeak.xyz/shiverpeak.xyz/infrastructure/src/branch/main/ansible/roles/cloudflare-companion/tasks/main.yml).
|
||||
Now, whenever a new service is added to the reverse proxy, the DNS records will be automatically created.
|
||||
|
||||
### Applicationing
|
||||
|
||||
All my applications are installed via Docker containers. Jeff Geerling provides a great [Ansible Role](https://github.com/geerlingguy/ansible-role-docker) for Docker, which I use to install Docker on the server.
|
||||
The installation of the remaining applications went uneventful. I store each application's configuration and data in `/mnt/user/appdata/<service>`, the same way as it is managed in Unraid.
|
||||
Locating every application's state inside one folder is a useful property for managing backups and restores; more on that later.
|
||||
|
||||
### Authenticating
|
||||
|
||||
Authentik is great! At work, I started to use Keycloak; however, I wanted to get my hands dirty with the newer, hipper kid on the block, Authentik.
|
||||
I've got a couple Google Titan Security keys for essentially no money (3 EUR per), so I can use them for passwordless login. Passwordless login allows me to simply click the key and be logged in without needing to enter a password or even a username. Pretty cool stuff! Following [this Video](https://www.youtube.com/watch?v=aEpT2fYGwLw) explains how to set up a passwordless login with Authentik.
|
||||
|
||||
Another feature I required is invitations! I do not want anyone to sign up by themselves, but I would like to be able to send a signup link to selected individuals.
|
||||
Although Authentik provides a [guide](https://docs.goauthentik.io/docs/users-sources/user/invitations), it is not actually enough information to get it to work.
|
||||
The guide is missing a crucial step: Creating a new Invitation Stage and linking it to the imported flow. Only then will the yellow warning in the Invitation's Menu disappear, and an invitation link will be generated.
|
||||
|
||||
VideoVault and Forgejo can be easily connected to Authentik using the OIDC provider. With just a few mouse clicks, copying over the credentials to the respective application's configuration is all that is needed.
|
||||
Timetagger was harder: it does not support OIDC and has its own authentication mechanism. Luckily, Authentik covers Proxy Authentication too, where it will act as a layer in front of the application, handling authentication and authorization and setting the respective headers, which will be interpreted by Timetagger to automatically sign in the user.
|
||||
Getting this to work was a bit tricky. In the end, I stuck with the embedded outpost. The full setup involved:
|
||||
|
||||
- Creating a new Application with Proxy Provider (forward auth single app) in Authentik
|
||||
- Creating a Traefik Middleware for forward Auth ([link](https://git.shiverpeak.xyz/shiverpeak.xyz/infrastructure/src/branch/main/ansible/roles/traefik/templates/dynamic-conf.yml.j2#L15-L30))
|
||||
- Assigning the middleware to the application ([link](https://git.shiverpeak.xyz/shiverpeak.xyz/infrastructure/src/branch/main/ansible/roles/timetagger/tasks/main.yml#L100))
|
||||
- Making sure all the URLs are correct ;)
|
||||
|
||||
Full ansible role for [Authentik](https://git.shiverpeak.xyz/shiverpeak.xyz/infrastructure/src/branch/main/ansible/roles/authentik).
|
||||
|
||||
### Backuping
|
||||
|
||||
The goal is to be able to destroy the infrastructure and recreate it from scratch, all automatically.
|
||||
I need to store the state of each application and restore it when the infrastructure is recreated.
|
||||
A simple backup mechanism, which utilizes Backblaze B2 buckets, creates a tar.gz archive of each application's state daily, uploads it to the bucket, and deletes old backups.
|
||||
When an application is reinstalled and detects its state folder is empty or missing, it will download the latest backup from the bucket and extract it to the state folder.
|
||||
Simple but useful.
|
||||
|
||||
## Final remarks
|
||||
|
||||
I will keep this post updated as I adjust and tweak the infrastructure.
|
||||
Thanks for reading thus far.
|
||||
|
|
@ -52,7 +52,7 @@ body {
|
|||
background-size: 100% 600px;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.8;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
strong,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
compatibility_flags = ["nodejs_compat"]
|
||||
node_compat = true
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue