NSD
nonstop development
Back to notebook
GitOpsOKDKubernetesFactorioAutomationTampaDevs

Zero-Touch Factorio Updates: GitOps + OKD Magic

How I turned bi-weekly manual Factorio server updates into a fully automated, set-it-and-forget-it GitOps pipeline using GitHub Actions, nightly version checks, and OKD's native ImageStream + Deployment triggers. No more pod killing.

January 28, 20267 min read
Share:

Zero-Touch Factorio Updates: GitOps + OKD Magic

Featured Image

As a cloud maintainer for Tampa Devs, I'm always looking for ways to make our OKD Kubernetes cluster more fun and useful. One of our community gems is the Factorio server running that hosts a game for local players but also avaialble to anyone online .

For those who may not know, Factorio is addictive game where you building factories, optimizing belts, automating everything — but keeping the server on the latest version was a recurring pain. New releases were dropping every couple of weeks, and the old process involved manual Docker builds, pushes to GHCR, and pod deletes to force pulls.

Once the process was automated, Factorio took a pause for a few weeks before they pushed a new update until this month which the process finally got to see action. The result: zero-touch updates. GitHub Actions check Factorio's API nightly, bump the version file if needed, trigger a build to :latest, and OKD's ImageStream + Deployment triggers handle the rest. No SSH, no oc delete pod, no interruptions.

Everything lives in our public repo here:

https://github.com/TampaDevs/cloud-development-gitops-apps/tree/main/game-servers/factorio

Let's walk through how it evolved and how you can replicate it.

The Old Manual Way (Painful)

  1. Check Factorio release notes or get told that the server isn't updated and no one can connect
  2. Build new Docker image with updated version
  3. docker push ghcr.io/tampadevs/factorio:latest
  4. oc delete pod -n ontampa-devs-gaming -l app.kubernetes.io/name=factorio

It worked, but it was repetitive and error prone and I personally built the image locally and uploaded on Spectrum which was slow.

Phase 1 – Trigger Builds from a Version File

I introduced a single source-of-truth file: factorio-version.txt

2.0.73

A GitHub Action watches for changes to this file and builds/pushes the image. The sections below rely on a few key points. First is the 'on' section which tells Github when to run our action. In this case a change to the main (or 'dev' branch for testing) and then if we are editing one of our key Docker related files. We do our authentication and login with our secrets and then we usefactorio-version.txtfile as our variable to use for the version to download and run our Docker build. Once complete, we push the new image to GHCR.

name: 'Build and Push Factorio Docker Image'
on:
push:
# Trigger on pushes to 'main' and any branch starting with 'dev'
branches:
- main
- 'dev'
paths:
- 'game-servers/factorio/Dockerfile'
- 'game-servers/factorio/docker-entrypoint.sh'
- 'game-servers/factorio/factorio-version.txt'
...
jobs:
build-and-push-image:
name: Build and Push Factorio Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
# ... setup and login steps are fine ...
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Read version from factorio-version.txt
id: extract_version
run: |
VERSION=$(cat ./game-servers/factorio/factorio-version.txt)
echo "version_tag=${VERSION}" >> $GITHUB_OUTPUT
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/factorio
tags: |
# Your tagging logic is fine
type=raw,value=${{ steps.extract_version.outputs.version_tag }}-${{ github.event.inputs.suffix }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.suffix != '' }}
type=raw,value=${{ steps.extract_version.outputs.version_tag }}-base,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=${{ steps.extract_version.outputs.version_tag }}-dev,enable=${{ github.ref == 'refs/heads/develop' }}
type=sha,prefix=${{ steps.extract_version.outputs.version_tag }}-,format=short
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5
with:
context: ./game-servers/factorio
file: ./game-servers/factorio/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
FACTORIO_VERSION_ARG=${{ steps.extract_version.outputs.version_tag }}
cache-from: type=gha,scope=${{ github.workflow }}
cache-to: type=gha,scope=${{ github.workflow }},mode=max

Now I can bump the version from my phone, commit, and the image is ready in minutes.

Still manual: someone needs to restart the pod

Phase 2 – Nightly Version Detection

Factorio publishes latest releases at https://factorio.com/api/latest-releases.

I added a simple Node.js script in version-updater/check-and-update.js that:

  • Fetches the API
  • Compares against factorio-version.txt
  • If newer → overwrites the file and commits/pushes

Scheduled GitHub Action runs every night:

name: Check Factorio Latest Version
on:
schedule:
- cron: '0 3 * * *' # 3 AM UTC daily
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.PAT_FOR_PUSH }} # needs write access
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
working-directory: game-servers/factorio/version-updater
- run: node check-and-update.js
working-directory: game-servers/factorio/version-updater
- name: Commit & Push if changed
run: |
git config user.name "TampaDevs Bot"
git config user.email "bot@tampadevs.org"
git add factorio-version.txt
git commit -m "chore: update Factorio to latest" || echo "No changes"
git push

Chain reaction achieved: nightly check → version bump → build trigger → new image in GHCR.

Phase 3 – Hands-Off Deploys with OKD ImageStream + Triggers

This is where OKD shines.

We use an ImageStream that points to our remote GHCR image and has importPolicy.scheduled: true:

apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
name: factorio-base
spec:
tags:
- name: latest
from:
kind: DockerImage
name: ghcr.io/tampadevs/factorio:latest
importPolicy:
scheduled: true # ← enables periodic polling (cluster default: 15 min)
importMode: PreserveOriginal

Every ~15 minutes (cluster-wide default in OKD 4.x), the image controller checks GHCR. If :latest now points to a new digest, it imports the update → the ImageStreamTag refreshes.

The Deployment watches this via the special annotation:

apiVersion: apps/v1
kind: Deployment
metadata:
name: factorio-base
annotations:
image.openshift.io/triggers: >-
[{"from":{"kind":"ImageStreamTag","name":"factorio-base:latest"},
"fieldPath":"spec.template.spec.containers[?(@.name==\"factorio\")].image",
"paused":false}]
spec:
template:
spec:
containers:
- name: factorio
image: ghcr.io/tampadevs/factorio@sha256:... # gets auto-updated
imagePullPolicy: Always

When the ImageStream updates, OKD automatically patches the Deployment's pod template with the new digest → triggers a rollout (Recreate strategy in our case). Game server restarts cleanly with the latest version, persistent volume intact.

Why This Wins

* Pure GitOps: Everything declarative in the repo.

* Zero toil: No manual steps after initial setup.

* Reliable timing: Nightly checks + 15-min import window = always reasonably current.

* Community extensible: Fork it for Minecraft, Valheim, or any container that needs latest-image behavior.

Want to run your own? Jump into the repo, deploy via Helm, or PR your improvements.

Let's keep automating the fun stuff.

Questions, ideas, or want help onboarding to the OKD cluster? Hit me up in TampaDevs Discord.

Happy building (and mining) 🚀

/

Enjoyed this post?

Get more build logs and random thoughts delivered to your inbox. No spam, just builds.