1. writing
  2. 20261024-deploy-to-vm.md

deploy-to-vm: a year of deploying with my own webhook

· go, devops

Last year, I wrote a small Go service called deploy-to-vm. It is deployed on the same VM with my other projects. It waits for GitHub to tell it about a new release, and puts that release live. I started working on it at a period that I was very bored with reviewing React code and wanted to learn Go. I haven’t thought about it, until, today. I moved this blog to a new domain, pushed a release, and, nothing happened.. This is a post about what I built, why, and what a year of not thinking about it taught me.

Why build it at all?

I have one small virtual machine on Hetzner, the smallest I could get at a great price. A home for my side projects.. Moving everything to a hosting platform would mean handing it my domains, and I like having all of it in one place that I control. I also like a good challenge, and making a fool of myself.

What I wanted was simple: push a commit and my changes are deployed. I tried a few things at that time and ended up using tags: push a tag, and a minute later the new version is live. No SSH, no copying files by hand (and no searching for a bunch of commands I keep forgetting every time).

How it works

The whole flow fits in one picture:

git tag prod.2026.10.24 && git push --tags

GitHub Actions - builds the site, attaches blog.<tag>.tar.gz to a release

GitHub - sends a "release" webhook

deploy-to-vm (on the VM)
  1. checks the webhook signature
  2. downloads the release assets into releases/<owner>/<repo>/<tag>/
  3. unpacks the .tar.gz
  4. links the files into the site's folder
  5. reloads nginx (or restarts the app with pm2)
  6. sends a message in my Discord server saying it's done!

The release side is a small GitHub Action I also wrote, create-release-with-assets, which builds the site and attaches the archive to the release. On the VM, a config file tells deploy-to-vm which repository goes where:

{
  "repositories": [
    {
      "name": "blog",
      "owner": "cemreyavuz",
      "sourceType": "static-webapp",
      "targetDir": "/var/www/blog/html",
      "targetType": "nginx"
    }
  ]
}

What a year of running it taught me

It worked. I pushed tags, sites updated, I forgot it existed. That is the best thing I can say about it, and also how it got me.

deploy-to-vm downloads the release files from GitHub with an access token. Somewhere in the past year, that token expired. Today GitHub sent the webhook as usual, deploy-to-vm tried to download the archive, got a 401, and answered GitHub with a 500:

Failed to download assets: Error downloading asset, status code: 401

Nobody told me. The service sends me a message when a deploy succeeds, but not when it fails. I only noticed because my new site wasn’t showing up. I didn’t even remember where the token lived. Was it a systemd service? An environment variable? A dotenv file? I wrote all of this a year ago, and none of it was written down. In the end, the fix took a minute: a new token in the .env file and a pm2 restart. Finding the problem took much longer.

Would I do it again?

Yes! I love building things, and I honestly have fun when I try to fix it a year later. It teaches me new things every time and I get the chance to use what I learned during the year to add new features. Building your own tool is fun and almost always doable.

(Just make it tell you when it fails, and write down how to run it 🙄)