Free cookie consent management tool by TermsFeed Update cookies preferences

Kubernetes - step by step

1 Our showcase application: FAIRDOM Seek

Seek is an extensive Data Management platform mostly aimed at Life Sciences but that can be used in any domain. It is a good example for going from bare metal to full cloud setup as:

  • it is already available as container and bare-metal installation, with a complete Docker compose which is still easy to understand,
  • it generally relies on several containers (4 with the default docker compose), several volumes, workers (i.e. side process that run regularly for doing batch works), a comprehensive search using Solr, and object storage,
  • it can be run with the unique Seek container (using a sqlite database within the container) so can go from really simple (for discovery or tutorials) to rather large (with load balancing, external DB, backups) which will allow us to go from a simple and stupid (in a Kubernetes context) case to full setups with most of the cases suitable for production.

1.1 Seek and its components

FAIRDOM-SEEK is a Ruby on Rails web application. In its standard Docker Compose deployment it is composed of four services that must all be running for the platform to be fully operational:

  • seek — the Rails application itself, served by Puma on port 3000. It handles all HTTP traffic, the JSON:API, authentication, and asset management.
  • seek-mysql — a MySQL 8.4 database that stores all structured metadata: users, projects, assets, permissions, job queues, etc.
  • seek-solr — an Apache Solr 8.11 instance (custom image fairdom/seek-solr:8.11) that powers full-text and faceted search across all platform assets.
  • seek-workers — a dedicated worker container running the same image as the main app but started with a different entrypoint. It processes background jobs (file content extraction, email delivery, Solr re-indexing, remote data fetching…) via Delayed Job, and runs scheduled tasks (feed refresh, sitemap generation…) via Supercronic.

Uploaded binary content (data files, model files, SOPs…) is stored outside the database in a filestore volume (seek-filestore), which in a basic setup is simply a local directory but can be swapped for any network-attached or S3-compatible object store in larger deployments.

An optional fifth service, OpenLink Virtuoso, can be added to expose a SPARQL endpoint over the RDF knowledge graph that SEEK builds from its metadata, enabling Linked Data queries for FAIR-compliance scenarios.

flowchart TD
    Browser -->|HTTP| seek["seek (Rails / Puma :3000)"]
    seek --> db["seek-mysql (MySQL 8.4)"]
    seek --> solr["seek-solr (Solr 8.11 · :8983)"]
    seek --> fs["seek-filestore (volume · binary assets)"]
    workers["seek-workers (Delayed Job + Supercronic)"] --> db
    workers --> solr
    workers --> fs
    seek -.->|optional| virtuoso["seek-virtuoso (Virtuoso 7 · :8890 SPARQL)"]

The table below lists all named Docker volumes and their role:

Volume Mounted into Contents
seek-filestore seek, workers Uploaded research assets
seek-mysql-db seek-mysql MySQL data directory
seek-solr-data seek-solr Solr index data
seek-cache seek Rails fragment and asset cache
seek-virtuoso-data seek-virtuoso (optional) Virtuoso triple store

1.2 The default Docker Compose

The default Seek docker compose looks a bit complex, but is straight forward if looked element by element.

x-shared:
  seek_base: &seek_base
    # build: .
    image: fairdom/seek:main

    restart: always
    environment: &seek_base_env
      RAILS_ENV: production
      SOLR_PORT: 8983
      SOLR_HOST: solr
      RAILS_LOG_LEVEL: info # debug, info, warn, error or fatal
    env_file:
      - docker/db.env
    volumes:
      - seek-filestore:/seek/filestore
      - seek-cache:/seek/tmp/cache
    depends_on:
      db:
        condition: service_healthy
      solr:
        condition: service_started


services:
  db:
    # Database implementation, in this case MySQL
    image: mysql:8.4
    container_name: seek-mysql
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --log-error-verbosity=1
    restart: always
    stop_grace_period: 1m30s
    env_file:
      - docker/db.env
    volumes:
      - seek-mysql-db:/var/lib/mysql
    healthcheck:
      test: [ "CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER -p$$MYSQL_PASSWORD --silent" ]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s


  seek:
    # The SEEK application
    <<: *seek_base
    container_name: seek
    command: docker/entrypoint.sh
    ports:
      - "3000:3000"
    environment:
      <<: *seek_base_env
      NO_ENTRYPOINT_WORKERS: 1
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:3000/up" ]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s

  seek_workers:
    # The SEEK delayed job workers
    <<: *seek_base
    container_name: seek-workers
    environment:
      <<: *seek_base_env
      QUIET_SUPERCRONIC: 1
    command: docker/start_workers.sh
    healthcheck:
      test: [ "CMD", "bash", "script/check_worker_pids.sh" ]
      retries: 5

  solr:
    image: fairdom/seek-solr:8.11
    container_name: seek-solr
    restart: always
    environment:
      SOLR_JAVA_MEM: -Xms512m -Xmx1024m
    volumes:
      - seek-solr-data:/var/solr/
    entrypoint:
      - docker-entrypoint.sh
      - solr-precreate
      - seek
      - /opt/solr/server/solr/configsets/seek_config

volumes:
  seek-filestore:
    external: true
  seek-mysql-db:
    external: true
  seek-solr-data:
    external: true
  seek-cache:
    external: true

1.2.1 Shared Base Configuration

x-shared:
  seek_base: &seek_base
    image: fairdom/seek:main
    restart: always
    environment: &seek_base_env
      RAILS_ENV: production
      SOLR_PORT: 8983
      SOLR_HOST: solr
    env_file:
      - docker/db.env
    volumes:
      - seek-filestore:/seek/filestore
      - seek-cache:/seek/tmp/cache
    depends_on:
      db:
        condition: service_healthy
      solr:
        condition: service_started

This is a YAML anchor — not a service, but a reusable template - x- meaning “extension”. Both seek and seek_workers inherit from it via <<: *seek_base. It sets the shared image, always-restart policy, environment variables pointing to Solr, and the two shared volumes for uploaded files and cache. Crucially, it enforces startup order: the app won’t start until the database passes its health check and Solr has at least started.

1.2.2 The Database (db)

db:
  image: mysql:8.4
  command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
  restart: always
  stop_grace_period: 1m30s
  healthcheck:
    test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER -p$$MYSQL_PASSWORD --silent"]
    interval: 10s
    retries: 5
    start_period: 20s

A standard MySQL 8.4 container, forced into utf8mb4 encoding (required for full Unicode support). The stop_grace_period: 1m30s gives MySQL time to flush and close connections cleanly on shutdown. The health check pings the server every 10 seconds, with a 20-second warm-up window — other services won’t start until this passes.

1.2.3 The Web App (seek)

seek:
  <<: *seek_base
  command: docker/entrypoint.sh
  ports:
    - "3000:3000"
  environment:
    <<: *seek_base_env
    NO_ENTRYPOINT_WORKERS: 1

This is the main Rails web process, inheriting the shared base and exposing port 3000 to the host. The NO_ENTRYPOINT_WORKERS: 1 flag tells the entrypoint script not to start background workers here — those are intentionally split off into the next service.

1.2.4 The Background Workers (seek_workers)

seek_workers:
  <<: *seek_base
  command: docker/start_workers.sh
  environment:
    <<: *seek_base_env
    QUIET_SUPERCRONIC: 1
  healthcheck:
    test: ["CMD", "bash", "script/check_worker_pids.sh"]
    retries: 5

A separate container running SEEK’s delayed job workers, using the same image but a different entrypoint. QUIET_SUPERCRONIC: 1 suppresses verbose cron-job logging. Splitting workers from the web process is good practice — it lets each scale and restart independently.

1.2.5 The Search Engine (solr)

solr:
  image: fairdom/seek-solr:8.11
  environment:
    SOLR_JAVA_MEM: -Xms512m -Xmx1024m
  volumes:
    - seek-solr-data:/var/solr/
  entrypoint:
    - docker-entrypoint.sh
    - solr-precreate
    - seek
    - /opt/solr/server/solr/configsets/seek_config

A pre-configured Solr image maintained by the SEEK team. The entrypoint uses solr-precreate to initialise a core named seek from a bundled config set on first run. Java heap is bounded between 512 MB and 1 GB — a sensible default for a small-to-medium deployment.

1.2.6 Volumes

volumes:
  seek-filestore:
    external: true
  seek-mysql-db:
    external: true
  seek-solr-data:
    external: true
  seek-cache:
    external: true

All four volumes are marked external: true, meaning Compose expects them to already exist on the host — it will not create or delete them automatically. This is a deliberate safety measure: running docker compose down -v won’t wipe your data.

1.3 What could benefit from a cloud setup

The benefits that can be expected from a cloud setup depends mostly on the scale of the platform and the expected traffic. As such, they are listed from the most general benefits to those that will be more relevant for larger platforms or more needs:

  • High availability: Cloud providers often have multiple data centers and availability zones, which can help ensure that your application remains available even if one data center experiences an outage. This can be achieved through load balancing and failover mechanisms. This is especially beneficial is the Kubernetes cluster is managed by an IT team (on site or not), even if they are not directly involved in the platform management: if one node of the cluster goes down, Kubernetes will automatically move the application to another node, ensuring that it remains available without any manual intervention. If the crash has to be investigated, it can be done later on without any rush, and without the need to restore the platform from a backup.
  • Relatively simple load balancing: Kubernetes has built-in support for load balancing, which can help distribute traffic across multiple instances of your application. This can help improve performance and ensure that your application can handle increased traffic. Note that load balancing can also be handled by a reverse proxy such as Nginx, but it is generally easier to set up and manage with Kubernetes, especially if you are using a managed Kubernetes service that includes load balancing as part of the offering.
  • Using a S3 storage, provide a direct access to the data, with a better performance and a better security than if the data is stored on a local volume. This is especially beneficial for large files, as the upload and download will be faster, and the storage will be more secure and scalable. The access can be temporary (given by token) and limited to a specific file, which can be useful for security and for sharing data with external collaborators.
  • Scalability: Cloud platforms can automatically scale resources up or down based on demand. This means that if your application experiences a sudden increase in traffic, the cloud can allocate more resources to handle the load, and then scale back down when traffic decreases.

1.4 A pragmatic approach of Cloud setup

The main question of which setup to choose is a matter or benefit/cost:

  • How much time will be needed for the training, the setup, the maintenance,
  • How secure is the setup, what solution is available for backup,
  • Which complexity can be managed, as a high complexity can also give a security risk.

As such, it is often not beneficial to use the whole capability of a Cloud setup, but to pick what will be a real benefit and what need to be ignored in order to keep things manageable. It is also important to consider which level of availability, security and scalibility is really needed. Going from a 99% uptime (the platform need to be available 99% of the time) to 99.9% might involve a very high cost, needing several instance behind a load balancer, using a distributed database with automatic replication of data, having a redundant identity server… Going from 99.9% to 99.99% will have an even greater cost.

2 A quick overview of Kubernetes

2.1 Good free instructory resources

2.1.1 Tutorials and sandboxes

2.1.2 Simple Kubernetes setup

On Windows, the simplest way to run Kubernetes is probably using Docker Desktop. A simple Kubernetes setup is part of the options.

On Linux, there are lightweight setup using various approached. The main ones are:

Warning

It is important to consider that, if some of these setup can be used in production (such as Talos or k3s), Kubernetes is a complex system and should be probably be managed by sys-admin persons, for ensuring fault-tolerance, backups and security.

2.2 Limits of Kubernetes, complexity and many moving parts

Kubernetes is an ensemble of small, decoupled components. Each one is relatively simple to understand in isolation, and the overall architecture is logically consistent. However, a functional setup requires a large number of these components working together, which introduces significant operational complexity.

A typical deployment involves multiple layers: workloads (Pods, Deployments, StatefulSets), networking (Services, Ingress, DNS), configuration management (ConfigMaps, Secrets), storage (PersistentVolumes and PersistentVolumeClaims), and access control (RBAC). On top of that, there are cluster-level components such as the scheduler, controller manager, API server, and etcd, which are usually abstracted away but still influence behavior.

The “many moving parts” become especially apparent when troubleshooting or evolving a system. A single application request might traverse an Ingress controller, a Service, kube-proxy rules, and finally reach a Pod that depends on mounted volumes and injected configuration. If something breaks, the root cause could lie in any of these layers—or in the interactions between them.

This complexity is amplified by the ecosystem around Kubernetes. Most real-world setups rely on additional tools: Helm charts for packaging, operators for managing stateful applications, monitoring stacks (e.g., Prometheus), logging pipelines, and CI/CD integrations. Each tool introduces its own abstractions and configuration formats.

Some tools such as K9s can help to have a better overview of the cluster and its elements, but is is important to clearly define and probably document your setup, to ensure that you can understand it later on and that other people can understand it as well.

Note

That will mostly define the cost of using Kubernetes.

If you have access to a fully managed cluster, with a good documentation and a good support, the complexity will be limited to the parts you are responsible for (probably at least the application deployment, but maybe also a database, a log manager, …).

If you need to manage your cluster, the cost might exceed the benefits and you might prefer a simpler solution, such as Docker, Docker Swarm, Proxmox (see the main step-by-step guide for more information).

2.3 Terminology & main elements

A first glance on Kubernetes can be very confusing. A big part of this confusion is due to a lot of “moving” parts with confusing names.

A full list of terms can be found on the official documentation.

The maim terms/elements you should know are:

  • Cluster: the whole Kubernetes setup composed of several worker machines, called nodes. Some managed Kubernetes can support several clusters, so for instance give a cluster for one user.
  • Node: one worker machine in Kubernetes, like a server. Can be a physical machine or a virtual machine.
  • Pod: the smallest object in Kubernetes, that will run one or several containers. The containers can be run by docker or other container engines.
  • Service: a fixed network endpoint to a container in Kubernetes. It allows to connect to an application in a Pod, without having this connection defined into the pod. Kubernetes often has one extra layer of access (compared to Docker Compose for instance) allowing a full decoupling of all elements. That makes it easier to change the configuration later on (automatically due to the need or from an user update).
  • Ingress: HTTP/HTTPS entry point that routes external traffic to Services (usually via hostnames/paths).
  • Gateway: newer, more flexible API to control traffic entering the cluster (successor-style model to Ingress).
  • Label: labels are used for Kubernetes to find the different elements.
  • Kubectl: the command line tool to communicate with the Kubernetes control center. With it you can create the object, update them, list them…
  • Manifest: the JSON or YAML file that define a Kubernetes object. Objects are all elements of Kubernetes: Deployments, Services, ReplicaSet, DaemonSet, …

And generally, the applications are deployed as:

  • ReplicatSet: ask for a set numbers of Pods. Kubernetes will try to have the set number of pods running, and will restore this which might become unhealthy. The pods can be on any node and it is not guaranteed that Kubernetes will be able to get the right number of pods (if there isn’t enough resources typically). It is possible to influence on which node the pods will run using affinities, see Section 2.16
  • Deployment: manages a ReplicaSet and provides rolling updates. It allows you to update an application without downtime by gradually replacing old Pods with new ones. If something goes wrong, you can roll back to a previous version.
  • DaemonSet: ensures that one Pod runs on each (or some) node. It is typically used for system-level services such as monitoring agents, log collectors, or networking tools that need to run everywhere.
  • StatefulSet: manages Pods that need stable, unique identities and persistent storage. It is used for stateful applications such as databases or distributed systems that need predictable network names or data persistence.
  • Job: runs Pods to completion. It is used for tasks that need to run once or a fixed number of times, such as batch processing or data import. When the Job finishes successfully, the Pods are not restarted.
  • ConfigMap: stores non-confidential configuration data as key-value pairs. Applications can use ConfigMaps to configure themselves without having to rebuild their containers.
  • Secret: similar to ConfigMap, but used to store sensitive information such as passwords, tokens, or SSH keys. Secrets are stored in base64-encoded form and can be mounted into Pods or used as environment variables. Be aware that they are not an encrypted, only encoded, so the security is ensured by having the secret saved in a private place. As such it is only moderately secured.

The main elements are represented below, without the network element (Service and Ingres/Gateway)

Pod controllers (manage Pods)
├─ Deployment
│  └─ ReplicaSet
│     └─ Pod(s) - Container(s), generally only one
├─ DaemonSet
│  └─ Pod (one per Node) - Container(s), generally only one
├─ StatefulSet
│  └─ Pod(s) with identity + storage - Container(s), generally only one
└─ Job
   └─ Pod(s) until completion - Container(s), generally only one

Or with a graph:

graph TD
  Cluster --> Node
  Node --> Pod
  Pod --> Container

  Deployment --> ReplicaSet --> Pod
  DaemonSet --> Pod
  StatefulSet --> Pod
  Job --> Pod

  Service --> Pod
  ConfigMap --> Pod
  Secret --> Pod

A very simple setup will be an application with 3 replicas that will be distributed on 3 nodes:

flowchart LR
subgraph ControlPlane[Control Plane]
RS1[ReplicaSet Controller]
end
subgraph NodeA[Node A]
P1[Pod v1]
end
subgraph NodeB[Node B]
P2[Pod v1]
end
subgraph NodeC[Node C]
P3[Pod v1]
end


RS1[ReplicaSet v1 replicas=3] --> P1
RS1 --> P2
RS1 --> P3

But Kubernetes does not guarantee where your replicas will be, so several pods might be on the same node. If a deployment does not define a ReplicaSet, then only one Pod is created.

flowchart LR
subgraph ControlPlane[Control Plane]
RS1[ReplicaSet Controller]
D1[Deployment w/o ReplicatSet]
end
subgraph NodeA[Node A]
P1[Pod v1]
end
subgraph NodeB[Node B]
P2[Pod v1]
P3[Pod v1]
end
subgraph NodeC[Node C]
P4[Pod 2 v1]
end


RS1[ReplicaSet v1 replicas=3] --> P1
RS1 --> P2
RS1 --> P3

D1 --> P4

Stateful Sets are used when it is needed to know which pod is where. For instance a replicated database might want to know which is the main pod (generally the first one) and all the others so it can manage them and select the next valid pod if the main is down:

flowchart LR
subgraph ControlPlane[Control Plane]
RS1[StatefulSet Controller]
end
subgraph NodeA[Node A]
P1[Pod v1 Example-1]
end
subgraph NodeB[Node B]
P2[Pod v1 Example-2]
end
subgraph NodeC[Node C]
P3[Pod v1 Example-3]
end

RS1[StatefulSet v1 replicas=3 Name=Example] --> P1
RS1 --> P2
RS1 --> P3

DaemonSets are for defining a Pod that need to be once on each Node. A common used is with centralised Log monitoring, i.e. reading the local log to populate a central log database to have a clear overview, as we want to monitor each nodes but do not want to have several agents (which will compete to read the local log file):

flowchart TB
    User --> Load_Balancer --> Service

    subgraph K8s_Cluster[Kubernetes Cluster]

        %% Application layer
        Service --> Deployment
        Deployment --> ReplicaSet

        ReplicaSet --> AppPod1
        ReplicaSet --> AppPod2

        LogAgent1_def["DaemonSet Manifest"]

        %% Nodes
        subgraph Node1["Worker Node 1"]
            AppPod1
            LogAgent1
        end

        subgraph Node2["Worker Node 2"]
            AppPod2
            LogAgent2
        end

        LogAgent1_def --> LogAgent1
        LogAgent1_def --> LogAgent2

        %% Log collection
        LogAgent1 --> AppPod1
        LogAgent2 --> AppPod2
    end

    %% Centralized logging
    subgraph Logging_Backend[Logging Backend]
        Graylog["Graylog"]
    end

    %% Log shipping
    LogAgent1 --> Graylog
    LogAgent2 --> Graylog

2.4 Everything decoupled

How Kubernetes makes everything through API and labels Why there is so many extra layers compared to docker compose -> full decoupling.

2.5 Most configuration is declarative

Kubernetes follows a declarative model: instead of describing how to perform actions, you define the desired state of your system (for example, how many replicas of a Pod should run, which image to use, or how services are exposed). The control plane continuously reconciles the actual state with this desired state and makes adjustments automatically.

This approach makes deployments more predictable, easier to version, and resilient to failures, since Kubernetes will always try to restore the declared configuration.

Here is a minimal Kubernetes Deployment that runs an NGINX container:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment        # Name of the Deployment
spec:
  replicas: 2                   # Desired number of Pods
  selector:
    matchLabels:
      app: nginx                # How the Deployment finds its Pods
  template:
    metadata:
      labels:
        app: nginx              # Labels applied to Pods
    spec:
      containers:
        - name: nginx           # Container name
          image: nginx:1.25     # Container image
          ports:
            - containerPort: 80 # Port exposed by the container

Explanations:

  • apiVersion / kind: Defines that this object is a Deployment using the stable apps/v1 API.
  • metadata.name: Unique name of the Deployment in the namespace.
  • spec.replicas: Kubernetes will ensure that exactly 2 Pods are running at all times.
  • spec.selector: Tells the Deployment which Pods it manages, based on labels.
  • spec.template: Template used to create Pods.
  • metadata.labels: Labels assigned to Pods (must match the selector).
  • spec.containers: Defines the container(s) running in each Pod.
  • image: The container image to run.
  • ports: Informational field describing exposed ports.

Once applied (kubectl apply -f deployment.yaml), Kubernetes will continuously ensure that:

  • 2 Pods are running,
  • they match this configuration,
  • and if a Pod crashes, it is automatically recreated.

This illustrates the declarative model: you describe the desired state, and Kubernetes handles the rest. At the beginning of using Kubernetes, it can be confusing to stop an application: if you delete a Pod declared in a deployment (for instance), it will be recreated automatically, as requested. To effectively stop the application, you need to modify or delete the top-level object (for example, a Deployment) that defines the desired state.

2.6 The various objects for your application

While the Pod is the smallest element in Kubernetes, it is directly started only for testing. If you define a pod or create it using kubectl, it will only run once and will not be restarted if something fails. As such, it is like docker run if started manually, or docker compose if started using a manifest, and offers no benefit — just the added complexity of the Kubernetes setup.

A pod is using a container engine, which does not have to be docker.

A ReplicaSet asks for a set number of pods, and Kubernetes will ensure that these pods are always running. If one pod becomes unhealthy or crashes, Kubernetes automatically removes it and creates a new one.

A Deployment is what you usually use for applications. It manages ReplicaSets and provides updates and rollbacks. You can think of it as “the normal way” to run an app on Kubernetes — one that should always be available and easy to update.

A Job is used for tasks that need to run once and then stop, for example, a script that imports some data or cleans up files. Kubernetes ensures that the job runs successfully to completion.

A DaemonSet runs one pod per machine (node). This is often used for background services such as system monitoring or log collection that must run everywhere.

A StatefulSet is like a Deployment but for applications that need to keep their identity and data across restarts — such as databases. Each pod in a StatefulSet has a fixed name and can have its own storage attached.

A CronJob is a definition of jobs that will run periodically following a cron format. When needed they are well explained in the official documentation

2.7 Visual overview

Here’s a simple way to picture how Kubernetes manages your applications:

+-------------------------------------------------------------------------------------------------+
|                                            Deployment                                           |
|                        (defines the app, version, and update strategy)                          |
|                                                                                                 |
|   +-----------------------------------------------------------------------------------------+   |
|   |                                        ReplicaSet                                       |   |
|   |                       (keeps the right number of Pods running)                          |   |
|   |                                                                                         |   |
|   |   +------------------------+   +------------------------+   +-------------------------+ |   |
|   |   |          Pod 1         |   |          Pod 2         |   |         Pod 3           | |   |
|   |   | (runs app in container)|   | (runs app in container)|   | (runs app in container) | |   |
|   |   +------------------------+   +------------------------+   +-------------------------+ |   |
|   +-----------------------------------------------------------------------------------------+   |
+-------------------------------------------------------------------------------------------------+
  • Deployment: Defines what you want to run and how to update it.
  • ReplicaSet: Ensures the right number of pods are running.
  • Pods: Run the actual container (your app).

If a pod fails, the ReplicaSet creates a new one. If you update your app, the Deployment creates a new ReplicaSet with the new version and removes the old one once everything runs correctly.

2.8 Updating an application

Applications are still running in containers, so updating means updating the container image. If Deployments are used, the update is managed automatically by Kubernetes: it follows an update strategy to start new containers while stopping and removing the old ones. This allows smooth, gradual updates with no downtime.

You can also roll back easily if something goes wrong, as Kubernetes keeps the previous version ready.

2.9 Where do you store the data

Kubernetes itself also stores data — but not application data.

The cluster state (what pods exist, their configuration, secrets, etc.) is stored in etcd, a distributed key-value database used internally by Kubernetes. This data is critical for the cluster to function, but it is not meant for your application data.

Containers are temporary — when they stop, everything inside disappears. For most applications, you need to keep data somewhere safe and separate from the containers. The common way to store data in Kubernetes is in volumes.

A Volume is a piece of storage that can be attached to a pod. It can be on the same machine, on a network drive, or in the cloud. Kubernetes makes sure that your pods can access this storage whenever they run.

If you need data that must stay linked to a specific pod (for example, for a database), you can use a PersistentVolume and a PersistentVolumeClaim. These ensure that even if your pods are restarted or moved to another machine, your data stays available and safe.

In short:

  • Pods are temporary.
  • Data should live in Volumes or PersistentVolumes.
  • Kubernetes handles the connection between the app and its data storage.

But you can also store your data in a database or an object storage (such as an S3 implementations). They might be part of the Kubernetes cluster, in another Kubernetes cluster, or fully external. Their access and usage will generally be the same in all cases. When within a Kubernetes cluster, they use volumes of their own and their setup could involve setting-up the volumes (see the note below). Some data storage (such as Postgresql) might be managed by an operator. Operators are software extensions of Kubernetes, that can define, as their name implies, operations on the cluster. In that case, you generally set-up the database and the operator as instructed by the operator documentation, and you will then have the database managed by the operator. They could take care of data replication, failure management, …, and are generally the prefered way to set-up a data storage on Kubernetes when something more than a simple volume is needed.

2.9.1 A note about automatic storage (StorageClass)

Kubernetes can automatically create persistent storage for you — but only if the cluster has been set up with a storage backend.

When you create a PersistentVolumeClaim (PVC), Kubernetes checks if there is a StorageClass available. The StorageClass defines how to create the actual storage — for example, on cloud disks, local drives, or network storage. If one is available, Kubernetes provisions the storage automatically and attaches it to your pod.

If no StorageClass or backend is configured, the claim will just wait forever (in Pending state), and your pod won’t start. So persistence only works if the cluster administrator configured it in advance.

In short:

  • Cloud clusters (AWS, Google, Azure) usually have a StorageClass ready by default.
  • Local or on-premise clusters might need to install one manually (for example, a local-path-provisioner or NFS).

2.9.2 Visual overview of automatic storage provisioning

+-----------------------------------------------+
|                 Your Pod                      |
|        (requests storage via PVC)             |
+-----------------------------------------------+
                      |
                      v
+-----------------------------------------------+
| PersistentVolumeClaim (PVC)                   |
| "I need 10 GB of storage, please."            |
+-----------------------------------------------+
                      |
                      v
+-----------------------------------------------+
| StorageClass (defines how to create storage)  |
| Example: AWS EBS, Google PD, NFS, Local Path  |
+-----------------------------------------------+
                      |
                      v
+-----------------------------------------------+
| PersistentVolume (PV)                         |
| Actual disk or network storage created        |
| automatically by the provisioner              |
+-----------------------------------------------+

If a suitable StorageClass is present, the system automatically creates a PersistentVolume and binds it to your claim. Otherwise, the claim just stays pending until an administrator provides a volume manually.

2.9.3 Visual overview of storage

+----------------------------+
|          Pod               |
|  (runs your container app) |
|          |                 |
|          | uses            |
|          v                 |
|      +--------+            |
|      | Volume |------------+--------------+
|      +--------+            |              |
+----------------------------+              |
                                            |
                           +--------------------------------+
                           | PersistentVolumeClaim (PVC)    |
                           |  (requests storage from pool)  |
                           +--------------------------------+
                                            |
                           +--------------------------------+
                           | PersistentVolume (PV)          |
                           | (actual disk / network storage)|
                           +--------------------------------+
  • Pod: The running container that needs to read or write data.
  • Volume: The connection point between the app and the storage.
  • PersistentVolumeClaim (PVC): A request for storage.
  • PersistentVolume (PV): The real storage space (disk, cloud volume, etc.).

This structure lets you move or restart pods without losing any data — Kubernetes will reconnect the storage automatically.

2.10 How Kubernetes keeps things running

Kubernetes is designed to make sure your applications stay up and running, even if something fails. It constantly watches all your pods and checks if they are healthy.

If a pod crashes, Kubernetes automatically restarts it or creates a new one. If a whole machine (called a node) goes down, Kubernetes moves your pods to another available machine.

You don’t need to manually restart or move anything — Kubernetes takes care of it. This is one of its biggest advantages compared to running containers manually: you describe what you want to run, and Kubernetes makes sure it stays that way.

2.10.1 Configuration values

ConfigMaps and Secrets

Regular values can be stored in a ConfigMap, while values that should be hidden are stored in Secrets. The main idea of Secrets is to keep your confidential values out of the regular configuration, in a separate, access-controlled resource. Secret values are encoded in Base64 to avoid issues with special characters, but this encoding does not provide real security, as it can easily be decoded. For actual protection, Secrets should be combined with additional measures such as RBAC (see below), encryption at rest, and external secret management systems.

ConfigMaps and Secrets can both be consumed by Pods as environment variables or mounted as files inside containers, making them flexible mechanisms for injecting configuration into applications at runtime.

For more details, see the official Kubernetes documentation:

Role-Based Access Control (RBAC)

Kubernetes RBAC (Role-Based Access Control) is used to regulate who can access or modify resources such as ConfigMaps and Secrets. RBAC works by defining Roles (or ClusterRoles) that specify allowed actions (e.g., get, list, create) on specific resources, and RoleBindings (or ClusterRoleBindings) that assign these permissions to users, groups, or service accounts. Proper RBAC configuration is essential to ensure that only authorized components or users can read or modify sensitive data like Secrets, thereby strengthening the overall security model of the cluster.

For more details, see the official documentation:

2.11 Namespace

Using namespaces allows to have several “virtual” clusters within the same physical cluster. It is a way to separate different environments (for instance, development and production) or different users (for instance, one namespace per user). It can also be used for separating different applications, but it is generally better to use labels for this purpose.

Namespaces are also useful for managing access and permissions, as you can define different roles and policies for each namespace.

2.12 Network access

The network access to the application can be managed through Services, Ingress or Gateway. Services are used for internal access, while Ingress and Gateway are used for external access. The main difference between Ingress and Gateway is that Ingress is a simpler and more limited solution, while Gateway is more flexible and powerful, but also more complex to set-up and manage. Gateway is the newer solution and is generally recommended for new setups, but Ingress can still be used for simple setups or for compatibility with older Kubernetes versions.

2.13 LoadBalancing

Load balancing in Kubernetes is, on the side of the application, using several replicas of the application, which can be managed by a Deployment or a StatefulSet. On the side of the network access, it is generally managed by a Service of type LoadBalancer, which will create an external load balancer and route the traffic to the different replicas of the application. It is also possible to use an Ingress or a Gateway for managing the load balancing, but it is generally easier to use a Service of type LoadBalancer for this purpose.

2.14 Several Deployments

For managing several kind of deployments for an application, 2 complementary solutions can be used (with many other solutions as external tools):

  • kustomize is a tool for managing Kubernetes configurations, which allows to have a base configuration and then create different overlays for different environments (for instance, development, staging and production). It is a good solution for managing different configurations for the same application, as it allows to keep the common configuration in a single place and to only define the differences in the overlays. A simple example of kustomize configuration can be found in the official documentation.

  • helm is both a template engine and a package manager for Kubernetes, which allows to define a chart for an application, and then create different releases for different environments. It is a good solution for managing different versions of the same application, as it allows to keep the common configuration in a single place and to only define the differences in the releases. A simple example of helm configuration can be found in the official documentation. Helm charts can also be used for managing the deployment of an application, as it allows to define the different components of the application (for instance, the database, the web server, the cache, …) in a single chart, and then create different releases for different environments. They are recommended for managing complex applications with many components, as they allow to keep the configuration organized and to easily manage the different versions of the application. Unfortunately, they can be a bit complex to set-up and manage, especially for simple applications, and they can also create some issues with the management of the configuration, as it is not always clear which values are defined in the chart and which values are defined in the release.

2.15 Artifact Hub & Local repository

Helm charts can be stored on Artifact Hub, a centralized registry of charts, or on any registry that can also be set-up locally.

To use a chart from a registry, the helm command is used:

helm install mylocalwordpress bitnami/wordpress

The helm command is well explained in the official documentation

2.16 Affinities

Affinity and anti-affinity rules in Kubernetes allow you to control how Pods are scheduled onto nodes, based on labels. They can be used to ensure Pods are placed close to each other (affinity) or kept apart (anti-affinity), which is useful for performance, availability, or fault tolerance.

There are two main types: node affinity (rules about which nodes a Pod can run on) and pod affinity/anti-affinity (rules about how Pods relate to other Pods).

Affinities are defined in the Pod specification and give more expressive control than simple node selectors.

The concept is well explained in the official documentation: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/