Back to notes

How I Keep Our Factorio Server Updated With GitOps And OKD

How I replaced manual Factorio server updates with GitHub Actions, nightly version checks, and OKD ImageStream deployment triggers.

/opsJan 31, 20265 min read

As a cloud maintainer for Tampa Devs, I am always looking for ways to make our OKD Kubernetes cluster more fun and useful. One of our community gems is the Factorio server we run for local players and anyone else who wants to join.

For those who may not know, Factorio is an addictive game about building factories, optimizing belts, and automating everything. Keeping the server on the latest version was the 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) 🚀