Skip to content

Tech Stack Crash Course

This page explains the tools behind the platform for informatics lecturers who know GitHub and Docker but haven't worked with Kubernetes, Harbor, or ArgoCD.

The full picture

graph LR
    classDef known fill:#c8e6d0,stroke:#4a9e6f,color:#2a2a2a
    classDef new fill:#d4c1d6,stroke:#582E5F,color:#2a2a2a
    classDef result fill:#f5d5a8,stroke:#e67e22,color:#2a2a2a

    push["git push"]:::known
    gha["GitHub Actions<br/><i>build & test</i>"]:::known
    harbor["Harbor<br/><i>store image</i>"]:::new
    imgupd["Image Updater<br/><i>detect new version</i>"]:::new
    argocd["ArgoCD<br/><i>sync to cluster</i>"]:::new
    k8s["Kubernetes<br/><i>run containers</i>"]:::new
    live["App is live!<br/><i>prj2-2026-ateam<br/>.prod.fontysvenlo.dev</i>"]:::result

    push --> gha --> harbor --> imgupd --> argocd --> k8s --> live

tools you know    new tools    result

The rest of this page explains each purple box, starting with where it all runs.


Where it runs

FICT Educloud

The platform is hosted on Fontys ICT EduCloud, an internal cloud run by our colleagues in Eindhoven, based on Apache CloudStack. Think of it as a self-hosted alternative to AWS — we get virtual machines on demand, at no cost to the project.

See ADR-010 for why we chose EduCloud.

Kubernetes — the container orchestrator

On these VMs we run a Kubernetes cluster — a system that runs many containers across many machines and manages them for you.

You tell it what you want running, it figures out how.

Kubernetes key concepts

Concept Analogy What it does
Cluster The datacenter A set of machines (nodes) managed together
Node A single server One virtual machine in the cluster
Pod A running container The smallest deployable unit (usually = 1 Docker container)
Namespace An isolated workspace Groups resources per team with its own network boundary — pods in prj2-2026-ateam cannot reach pods in prj2-2026-bteam

Everything is defined as code (Infrastructure-as-Code or IaC) in a single git repository: FontysVenlo/developer-platform (lecturers are able to access).

What does platform code look like?

Generated with cloc $(git ls-files) --md, 9-4-2026:

Language Files Blank Comment Code
YAML 65 216 434 5645
Markdown 39 1002 0 2933
Bourne Shell 4 70 59 365
Dockerfile 2 12 13 17
TOML 1 0 0 9
SUM 111 1300 506 8969

Harbor — a container registry

When GitHub Actions runs docker build, it produces an image. To deploy it somewhere, that image needs to be stored in a registry — a place other systems can pull from.

Harbor is a private, self-hosted container registry running on our own infrastructure:

graph LR
    classDef known fill:#c8e6d0,stroke:#4a9e6f,color:#2a2a2a
    classDef new fill:#d4c1d6,stroke:#582E5F,color:#2a2a2a

    gha["GitHub Actions<br/><i>docker build</i>"]:::known
    harbor["Harbor<br/><i>private, self-hosted,<br/>per-team access</i>"]:::new

    gha -->|"docker push"| harbor
  • Self-hosted — runs on our network, no external dependencies;
  • Project-scoped — each PRJ2 team pushes to their own project (e.g., prj2-2026-ateam/backend).

See ADR-006 for the rationale.

What a PRJ2 team namespace looks like

Each PRJ2 team has its own namespace with a frontend and backend. The database is a shared PostgreSQL server in a separate namespace — each team gets their own database on it.

graph LR
    classDef infra fill:#b8d4f0,stroke:#438DD5,color:#2a2a2a
    classDef app fill:#d4c1d6,stroke:#582E5F,color:#2a2a2a
    classDef db fill:#f5d5a8,stroke:#e67e22,color:#2a2a2a

    internet["🌍 Internet"]
    traefik["Traefik<br/><i>HTTPS routing</i>"]:::infra

    subgraph ns ["prj2-2026-ateam namespace"]
        fe["Frontend<br/><i>Svelte</i>"]:::app
        be["Backend<br/><i>Java</i>"]:::app
        dbinit["db-init<br/><i>creates/resets schema</i>"]:::app
    end

    subgraph shared ["prj2-system namespace"]
        pg["PostgreSQL<br/><i>shared server,<br/>per-team database</i>"]:::db
    end

    internet -->|"prj2-2026-ateam.prod.fontysvenlo.dev"| traefik
    traefik --> fe
    traefik --> be
    be --> pg
    dbinit --> pg

Students never interact with Kubernetes directly (their first year). They push code, and the platform handles the rest.

The mental model

Docker is like cooking a meal and putting it in a container. Kubernetes is the restaurant kitchen that takes orders, runs multiple dishes in parallel, and serves them to the right table.


GitOps and ArgoCD — deployment without deploy scripts

This is the part that usually surprises people. There is no deploy command. Nobody runs kubectl apply or SSH-es into a server. Instead, we use GitOps.

GitOps in 30 seconds

The desired state of the infrastructure is defined in a Git repository. A controller watches that repository and continuously ensures the cluster matches what's in Git.

graph LR
    classDef git fill:#c8e6d0,stroke:#4a9e6f,color:#2a2a2a
    classDef controller fill:#d4c1d6,stroke:#582E5F,color:#2a2a2a
    classDef cluster fill:#d0d0e0,stroke:#1a1a2e,color:#2a2a2a

    git["📄 Git repo<br/><i>desired state</i>"]:::git
    argo["ArgoCD<br/><i>watches & compares</i>"]:::controller
    k8s["☸ Kubernetes<br/><i>actual state</i>"]:::cluster

    git -->|"defines"| argo
    argo -->|"syncs"| k8s
    k8s -.->|"drift detected?"| argo
    argo -.->|"revert to Git"| k8s

If someone changes something manually in the cluster, ArgoCD will revert it to match Git. Git is the single source of truth.

ArgoCD — the GitOps controller

ArgoCD is the tool that implements GitOps. It:

  1. Watches a Git repository containing Kubernetes manifests;
  2. Compares the desired state (Git) with the actual state (cluster);
  3. Syncs automatically when they differ.

ArgoCD Image Updater — closing the loop

How does ArgoCD know a new Docker image was pushed?

ArgoCD Image Updater polls Harbor every 2 minutes. When it finds a new image tag, it updates the Git repository. ArgoCD then picks up the change and deploys.

sequenceDiagram
    participant S as 👨‍🎓 Student
    participant GH as GitHub
    participant GHA as GitHub Actions
    participant H as Harbor
    participant IU as Image Updater
    participant Git as Git repo (infra)
    participant AC as ArgoCD
    participant K as ☸ Kubernetes

    S->>GH: git push
    GH->>GHA: trigger workflow
    GHA->>GHA: docker build
    GHA->>H: docker push (image)

    Note over IU,H: Every 2 minutes
    IU->>H: any new tags?
    H-->>IU: new tag for prj2-2026-ateam
    IU->>Git: update image version
    Git-->>AC: change detected
    AC->>K: sync (rolling update)
    K-->>S: app live at prj2-2026-ateam.prod.fontysvenlo.dev

Why GitOps instead of kubectl apply in CI?

With GitOps, the Git repo is always the truth. Rollback is just a git revert. No cluster credentials in CI pipelines — only Harbor push access. See ADR-002.


Observability — Grafana, Prometheus, Loki

Each team gets a Grafana dashboard — provisioned automatically at onboarding.

graph TB
    classDef app fill:#d4c1d6,stroke:#582E5F,color:#2a2a2a
    classDef obs fill:#f5d5a8,stroke:#e67e22,color:#2a2a2a
    classDef dash fill:#c8e6d0,stroke:#4a9e6f,color:#2a2a2a

    subgraph team ["prj2-2026-ateam namespace"]
        fe["Frontend"]:::app
        be["Backend"]:::app
    end

    prom["Prometheus<br/><i>scrapes metrics<br/>(CPU, memory, status)</i>"]:::obs
    loki["Loki<br/><i>collects logs<br/>(errors, exceptions)</i>"]:::obs
    grafana["Grafana<br/><i>dashboards</i>"]:::dash

    fe --> prom
    be --> prom
    fe --> loki
    be --> loki
    prom --> grafana
    loki --> grafana

What students see on their dashboard:

  • Application health — are frontend and backend running?
  • Resource usage — CPU and memory over time
  • Recent errors — application logs with exceptions highlighted

Onboarding your own app or student project

Interested in using the platform for your course or research project? Here's what the process looks like.

We're not at the point of having a self-service onboarding form yet, below is the reality for UoL1.1, with the assumption all students use the same application template. For more advanced use cases (UoL1.3?), we'll figure it out together.

Limitations

  • You dont have admin access to the cluster + tools - Lennart is the only one with credentials. This means only Lennart can onboard new applications and set certain things up. This is to be improved in the future.

What you need

Your application needs to be containerizable:

  • A Dockerfile per component (we have templates and can help you write one);
  • Github Actions workflow for building and pushing the image (in the classroom template);
    • Lennart can provide the credentials
  • The app should be configurable via environment variables (database URL, API endpoints, etc.);
    • in case of a classroom, like UoL1.1, we need some conventions for easy onboarding: <module>-<year>-<team>.

What we figure out together

  • What does your application look like? How many components, what tech stack, does it need a database?
    • Databases are somewhat special, how do you want to deal with persistent data?
    • Resources you expect your app to need (CPU, memory, storage, size of container images)?
  • What should students see? Dashboard with logs? Just a URL? Something else?
  • What level of control? Full golden path (like PRJ2) or more freedom for advanced students?

The work for Lennart

  • prepare the platform itself - there's already things I'd like to do differently, when projects become N > 1, increases the need to tackle

  • testing + lots of fiddling in yaml files to get things to work (certificates, ingress, secrets, resource limits, etc.)

    • first deploying a single instance of the app
    • then scripts for onboarding and offboarding a full class
  • documenting (both students and runbooks for ourselves)
  • creating and connecting the dashboard (logging, metrics)
  • defining the resources needed (cpu, memory, disk)

Include Lennart early

For UoL1.1 and beyond, please involve the 'platform team' early. We need lead time to prepare namespaces, pipelines, and dashboards.


Quick reference

Layer Tool One-liner
Code hosting GitHub Where code lives
CI GitHub Actions Build and test on push
Registry Harbor Private image storage
GitOps ArgoCD + Image Updater Desired state in Git, auto-sync to cluster
Orchestration Kubernetes Run and manage containers
Ingress Traefik + Let's Encrypt HTTPS routing and certificates
Observability Grafana + Prometheus + Loki Dashboards, metrics, logs
Secrets ExternalSecrets Inject credentials safely
Infrastructure EduCloud (CloudStack) Virtual machines hosting the cluster

Further reading