Twenty Stacks Sparked by One TREK
Introduction
A few days ago, while browsing GitHub Trending, I came across a project called TREK. It lets a group of people plan routes on a map, book accommodation, list budgets, and then share the finished plan for others to keep editing. I clicked into the demo site and played around for a bit. It felt pretty smooth.
The itineraries for trips with my girlfriend have always been pieced together in Obsidian, with several daily notes linked back and forth. The more I wrote, the messier it got, and by the day of departure it was usually still just a rough "where to go in the morning, where to go in the afternoon" outline. As soon as I saw TREK, I wanted to run an instance myself. I could use the author's demo directly, but putting travel plans on someone else's server feels a bit off. Sooner or later, I would have to deploy it myself.
And since I was going to deploy it myself anyway, I might as well spin up a new VPS while I was at it.
Netcup ARM
Choosing a VPS was more agonizing than I expected. DigitalOcean / Linode cost more than twice as much for similar specs; small domestic providers are cheap, but outbound port 25 and ICP filing are two potential landmines, so I would rather not touch them if I can avoid it; Hetzner has great value for money, but its ARM node locations are not very flexible. In the end, I circled back to Netcup because it was convenient.
The exact model I chose was the VPS 2000 ARM G11: 10 vCores, 16 GB RAM, 512 GB NVMe, 2.5 Gbps network, at €13.41/month including tax. On the x86 side, the same specs would cost at least twice as much. Traffic is flatrate; only if the 24-hour moving average exceeds 2 TB does it get throttled to 200 Mbps. My tiny little site is nowhere near that.
Before placing the order, I Googled around and found a -50% coupon, cutting the first payment straight down to a little over €6.7. Renewals go back to the regular price. I do not really mind this kind of "half off for the first term" trick; the discount for the first year is already more than good enough.
There were four data centers to choose from: Nuremberg, Vienna, Amsterdam, and Manassas on the US East Coast. I eventually picked Manassas, because common routes from China had noticeably lower ping to it than to the three European locations. As for mail IP reputation, all my outbound mail goes through a Resend HTTPS bridge anyway (more on that later), so I do not rely on the local IP for delivery. No need to stay in a European data center just for that.
From Zero to a Running TREK
It took about five minutes from placing the order to receiving the login details. The first thing I did was not install software, but lock down SSH: disable root login, disable password login, change the port, and set ufw to deny incoming traffic by default. After running fail2ban for two days and checking the logs, I found that before changing port 22 there had been 5000+ brute-force attempts in a day; after changing it, the number dropped to single digits. On a freshly installed machine, the people outside are more eager than you are.
The ARM64 image ecosystem has improved a lot over the past couple of years. Among the twenty-odd stacks I installed later, Caddy, Postgres, Stalwart, SnappyMail, Open WebUI, Vaultwarden, Dagu, Glance, Karakeep, and Paperless all had official multi-architecture images, so docker pull just worked. The only slightly troublesome one was the Forgejo runner. The base image was fine on ARM64, but I needed to run docker compose inside the runner as a deployment tool, so I had to add docker-cli and docker-compose-plugin myself. In the end, I used buildx to push an ARM64 image to my own Forgejo registry, and the runner could pull it directly when starting up.
Finally, it was TREK's turn. The repository mauriceboe/TREK provides an official docker-compose.yml. I copied it over and changed a few lines:
- Restricted the ports so they are only visible to the web Docker network
- Generated a new ENCRYPTION_KEY with openssl rand -base64 32
- Set DATABASE_URL to SQLite, which is plenty for my own use
- Added the external network web so it could connect to Caddy
On the Caddyfile side, one line of reverse_proxy trek
finished the job. From docker compose up -d to seeing the login page appear at trek.shinya.click, the whole thing took less than 15 minutes. Looking at the empty dashboard, I registered the first account, and the next thing I instinctively started thinking about was not "where should the itinerary start", but "what else can I cram onto this machine". I know I have this problem.Once I Started Installing Things, I Could Not Stop
The late-night tinkering mostly began from that thought. Since the specs were generous anyway, letting them sit idle felt like a waste, so I might as well move over all the things I had long wanted to self-host.
The next morning, I casually installed Forgejo to bring code hosting back under my own roof, and I also took the opportunity to renovate the blog, which I will talk about separately below. For mail, I used a trio of Stalwart + SnappyMail + a Resend bridge I wrote myself. On the AI side, I put CLIProxyAPI in front to unify my OpenAI / Claude / Gemini subscriptions, then layered Open WebUI on top for my own use. After that came passwords, cloud storage, image hosting, notes, read-it-later, RSS, and PDF toolkits — basically everything that could be self-hosted got a pass. SSO is handled by Authelia, tying this whole batch of services together so one login works everywhere. Backups are run by Dagu with a DAG that creates encrypted snapshots to R2 every day.
After gradually installing everything, the overall structure looked roughly like this:

All entry traffic goes through Cloudflare SaaS smart DNS resolution back to origin; Caddy is the only outward-facing TLS endpoint and reverse-proxies to the corresponding container based on the Host header; services that require login all go through Authelia forward_auth; outbound mail goes through the Resend HTTPS API bridge to work around Netcup's restrictions; backups are daily full encrypted snapshots to Cloudflare R2.
Looking back at the Dockge panel with twenty-odd stacks all glowing green is pretty satisfying, but I also admit that a fair part of this was deployment addiction kicking in: I originally only wanted one TREK, and ended up with an entire self-hosted infrastructure setup. TREK is just one small box in that diagram.
Migrating the Blog While I Was at It
Previously, the blog was Astro + retypeset, with Markdown files pushed to trigger CI builds and publishing. It had worked fine for more than a year, but a few things had always bothered me: even fixing a typo required commit, push, and waiting for a build; adding lightweight pages like "friends" or "standalone pages" meant messing with the file tree again; travel posts with lots of photos made builds absurdly slow.
Since the new machine was already set up, I decided to redo the blog at the same time.
The new stack is SvelteKit 2 + adapter-node SSR + UnoCSS. Visually it keeps the retypeset look, but the styles were rewritten with UnoCSS. The content backend was replaced with PocketBase, with 7 collections for posts, tags, travelogues, travelogue days, travelogue photos, standalone pages, and friend links, all stored in SQLite. i18n was changed to coexist in three languages: zh / en / ja. I wrote a migration script to dump the Markdown exported from the old Astro setup into PB in one go: 123 posts, 4 travelogues, 148 travel photo records, plus pages and friends, all landed in the database.
Deployment was changed to blue-green: two containers, blog-blue and blog-green, stay resident at all times. The active color is marked by /opt/app/blog/active, and Caddy imports the reverse-proxy upstream from /opt/app/caddy-blog/upstream.caddy. After CI pushes a new image, it runs switch.sh, starts the target color, waits for the healthcheck, updates the import file, and caddy reload switches traffic over. Failures trigger automatic rollback. With this in place, the whole publishing process changed from "push and wait three minutes" to "edit in the PB admin panel, save, and it takes effect immediately".
The admin panel is a SvelteKit internal SPA mounted under /admin and protected by Authelia forward_auth. Write requests from admin to PB go through Caddy's same-origin reverse proxy at /api/pb/*, with the token injected by Caddy, so neither the browser nor the git repository ever touches that key. PB writes trigger a JS hook that POSTs to an internal endpoint in the blog container, invalidating server-side cache and then calling the Cloudflare API once to purge edge cache. Public pages are still mostly served from the CDN.
The entire migration took two evenings from start to finish, and went surprisingly smoothly.
Wrapping Up
After tinkering for several days, I still have not made a single plan in TREK = =