Tom Johnell

Homelab deployments: a tutorial made of prompts

Every now and then I get a hankering to write about my homelab, but there's so much to talk about that I end up just writing nothing. This is my attempt to stay on a smaller topic in the hopes it's less boring and potentially more useful. Additionaly, with the advent of LLMs, I kind of think it's a waste of time for step-by-step instructions. I hope to equip readers with useful prompts, and if they want to dive into the details, they can on their own.

So, in this post I am going to share how I deploy software to the internet (and intranet). This will include small static sites, open-source software, as well as more substantial webapps I develop on my own. For now, I am mostly skipping DNS and service ingress. We'll assume you have that set up. This post is more about the automation of deploying code changes.

Static Sites

Small static sites are definitely the easiest because no backend is necessary for them to function. Because of that, I don't even bother hosting them myself. I'd rather a service like Cloudflare (CF) serve the assets both because it's a simpler design and because Cloudflare will serve the assets a lot faster than my small machine in the middle of nowhere (Destin, FL currently). Cloudflare Pages is the perfect tool for this use case - you provide CF with a bundle of assets and it will provide a URL to host those assets. If you have a domain through CF (highly recommend CF Registrar), you can easily point to the CF Page with your own domain. LLMs have this all down pat, so I honestly do not think there's any point in explaining the details - instead I'll just share my prompts for the LLM.

Prerequisites

Prompts

1. Create the static site

Where: ~/projects/static-site

Prompt: Build a static site that does […]

If you're extra nerdy, you can be pickier about which static site generator to use and such. By the time it's done, it will have created a site that likely has an npm run build command and produces a distribution folder.

2. Build and deploy on merge into main

Where: ~/projects/static-site

Prompt: Create a GitHub repo for this project, push the latest changes, add GitHub Actions to build and deploy the site to Cloudflare Pages on merge into main. Provide instructions for how to retrieve any API keys I may need and where to safely store them.

The prompt is self-explanatory. You will need a CF API token and account ID in order to push the assets to CF Pages, and the LLM will provide helpful details for you on how to do that.

At this point you have the site built and deployed publicly to the world! If you want to use your own domain - just ask the LLM. When you need to make updates to the site, just make the changes and merge them into main. GH Actions will take care of the rest.

Self-hosted Applications

I use Docker Compose to deploy my applications to my homelab. I've resisted the temptation to use something more complex like Kubernetes even while wielding the power of tools like GPT Astra, because at the end of the day, I do not need the SLOs that those tools were built for - my SLO is "don't be randomly broken when I randomly need to use you". Deploying software changes is not random - I will allow some minuscule downtime while I deploy, and therefore I do not need replicas or rolling deploys. The same goes for running backups (future blog post).

My original deployment strategy to my homelab with Docker Compose was very rudimentary. SSH into the linux box, git pull + docker compose up -d in the project directory was all that was necessary. However, I wanted the satisfying deploy on merge into main like the static sites I described earlier. There are two main options for that - you either notify your homelab it's time to deploy (we'll call that the push method), or your homelab is constantly polling for changes (which we'll call the pull method).

As a software engineer with a few battle scars, I have learned that polling is really simple to set up but has some drawbacks. The first is the latency between when the event happens and when the change is received by the poller. If you want "near real-time", you have to poll at a pretty fast clip (5s). Your homelab polling GitHub every five seconds isn't super practical. The second is that polling by its nature requires a running daemon, and those can experience outages themselves. Not difficult to manage, but it is one more thing you have to manage.

The push method has its own complexities. You will solve the latency of receiving the event out of the gate because it will notify the server as soon as the event occurs, but the server has to have something there to receive that event. In the case of GitHub, it would be a webhook that is sent to your server, which is hosting a public webhook endpoint to receive the event and then take some action. For my homelab, I am averse to any public endpoint (though I have quite a few), so I wasn't a fan of this approach. Not only would it be public, but it would be an endpoint that has access to deploy my services - sure, I can lock that down, but it's still a risky exposure.

What I ultimately landed on was self-hosting my own GitHub Actions runner. It has the downside of polling where I have a daemon I now have to manage, but it receives jobs from GitHub in near real-time due to using HTTPS long-polling with a persistent connection. It has the added benefit that no public endpoint must be exposed.

Prerequisites

Prompts

1. Create a GH Actions Runner

Where: ~/projects/media-server

Prompt: Add a self-hosted GitHub Actions runner to my linux box with the name linux-box. Make sure the runner daemon self-heals if the linux box restarts or something else bad happens like OOM.

This will create a new GH Actions runner daemon on your server that can poll for jobs on the media-server repo (runners are registered per repo, or per org if you want to share one).

2. Auto-deploy on merge into main w/ GH runner

Where: ~/projects/media-server

Prompt: Set up a GH workflow to auto-deploy on merge into main using the linux-box runner. Never run on pull requests

At this point the LLM will create a deploy workflow via GH Actions that will run your typical deployment commands you've been running manually, including pulling latest changes from GH, decrypting secrets, and running docker compose up -d. Additionally, it will configure the workflow to trigger on changes to the media-server repo and run on your GitHub Actions runner.

So, now when you merge a change into media-server, your runner will immediately pick up the job and run the deployment workflow!

Important - Only use this type of workflow with private repos. You never want a deploy workflow running on your server that could have been potentially tampered with by someone else. I would not do this on a public repo - regardless of who has write access - just don't do it.

Personal Applications & Watchtower

For my personal applications, I have them live in separate, private GitHub repos. They have their own container images that are built on merge into main. Given these are personal applications, I do not pin specific versions in the docker-compose.yml described in the previous section. Instead, I have the images tagged as my-project:latest. I have also set up Watchtower on the media-server to watch for new images from those private repos and to immediately pull and deploy them as they are made available. The original Watchtower project died, so I am using nicholas-fedor/watchtower. I have that project tied to a specific version and have an LLM scan for anything nefarious any time it has updates. If you want a similar flow, here are some prompts you can use.

Prerequisites

Prompts

1. Build images

Where: ~/projects/my-project

Prompt: Set up a GH Action to build images on merge into main and tag with latest.

2. Auto-deploy latest images

Where: ~/projects/media-server

Prompt: Add nicholas-fedor/watchtower to my Docker Compose stack to auto-deploy only my-project on creation of a new latest image.

Disclaimer

You may have at some point wondered why I give such free rein to my agents to perform work on my linux servers. I literally give root access to my linux server to my agents. I have gotten way too much value out of taking this risk - it completely overshadows and outweighs any risk involved in the agents ruining something. I like to take risks - especially if they make my life easier. I understand the value of sandboxes and why they are necessary, and I may eventually go down that path myself, but for now, I just want to focus on making my life easier, not safer.

Conclusion

With LLM tooling, auto-deploying on merge into main is super simple. With just a few prompts, you can remove a lot of toil associated with running your own homelab. For static sites, stick with GitHub Actions & Cloudflare Pages. For everything else, I suggest Docker Compose and your own GitHub Actions runner. You can accomplish so many things, in a secure way, with just those two tools. Cheers.

#ai #homelab