A Pod should ideally be created as part of a Deployment, which allows it to be managed as one or more replicas based on a specific template. This template mirrors what we will see later in the Deployment manifest. For now, let's look closely at the Pod manifest, focusing on options like volumes, probes, and environment variables. Understanding these components is essential for grasping how Pods work in Kubernetes and for troubleshooting potential issues.
First steps
The first part of your Pod manifest defines the apiVersion, kind, and any associated metadata:
apiVersion: v1
kind: Pod
metadata:
name: hyperskill-clone
namespace: hyper
labels:
app: hyper-app
annotations:
environment: "Production"
description: "A simple web application"
owner: "hyper-user"The apiVersion field specifies which version of the Kubernetes API you want to use to create the object (for a Pod, this is simply v1). The kind field declares the type of object you're defining — in this case, Pod. Together, these two fields tell the API server how to interpret the rest of the file and which schema to validate it against. Without them, Kubernetes cannot process the manifest.
The metadata section is where you give your Pod its identity. You start by providing a name for your Pod, which must be unique within its namespace. You can also attach labels and annotations here. This metadata does not describe the workload itself; it provides the information that Kubernetes, other tools, and your teammates use to find, group, and understand the Pod later.
While both labels and annotations look similar at first glance, they serve different purposes. Labels are short identifying tags that Kubernetes uses to select and group objects (for example, in a Deployment). You can also use them with kubectl to retrieve specific Pods:
$ kubectl get pods -l app=hyper-appAnnotations hold arbitrary information for tools or humans, such as build IDs, versions, etc. A good example is the kubectl.kubernetes.io/last-applied-configuration annotation that Kubernetes automatically adds when you apply a manifest and uses it to track the previously applied manifest.
Containers and initContainers
The spec section is where you define the desired state of the Pod. It includes various subfields, such as containers, initContainers, and volumes:
spec:
initContainers:
...
containers:
...
volumes:
...The initContainers field allows you to define one or more containers that run before the main containers start:
spec:
initContainers:
- name: init
image: busybox:latest
imagePullPolicy: Always
command: ["sh", "-c", "echo Initializing... && sleep 5"] If you'd like to define a sidecar container, just set the restartPolicy of an init container to Always:
spec:
initContainers:
- name: log-shipper
image: fluent/fluent-bit:latest
restartPolicy: Always # this makes it a sidecarThe containers field defines the main application container running inside the Pod:
spec:
containers:
- image: nginx:latest
imagePullPolicy: Always
name: frontend
ports:
- containerPort: 80
name: http
protocol: TCP
- image: initram/hyperskill-clone:latest
imagePullPolicy: IfNotPresent
name: hyperskill-backend
ports:
- containerPort: 8000
protocol: TCP
name: hyperskill
env:
- name: log-level
value: "info"
- name: db_password
valueFrom:
secretKeyRef:
- name: hyperskill-secrets
key: database_passwordEach entry in this list is a complete container specification. At a minimum, you provide a name (used to identify the container within the Pod) and an image (the container image to run). You can also set an imagePullPolicy to control when Kubernetes pulls the image from the registry (the default registry is Docker Hub). Common values are Always, IfNotPresent, and Never. You can also run sidecars in this way, but the modern approach is to use an entry in initContainers.
Another important field is the env field, which injects environment variables into the container at startup. You can provide values inline with value, or pull them from external sources using valueFrom. This latter method references a ConfigMap for plain configuration or a Secret for sensitive data like passwords or tokens. The referenced Secret or ConfigMap must exist as a separate object in the same namespace; the Pod only refers to it by name.
Volumes and volumeMounts
Storage in a Pod is defined in two places that work together. volumes, at the Pod level, declares what storage is available, while volumeMounts inside each container declares where that storage appears in the container's filesystem. You must declare a volume in spec.volumes before any container can mount it:
spec:
volumes:
- name: cache
emptyDir: {}
- name: config
configMap:
name: app-config
- name: data
persistentVolumeClaim:
claimName: app-dataIn this snippet, three volumes are defined. The cache volume uses emptyDir, a temporary directory that Kubernetes creates when the Pod starts and deletes when it removes the Pod. This is a useful scratch space shared between containers. The config volume is backed by a ConfigMap named app-config, which exposes its keys as files. The data volume is backed by a PersistentVolumeClaim named app-data, which binds the Pod to durable storage that survives restarts and rescheduling. Other common types include secret, hostPath, and downwardAPI.
PersistentVolumeClaim and ConfigMap are standalone Kubernetes objects that you create separately (usually in their own manifests) and then reference here by name.
Once you declare the volumes, each container that needs them attaches them with volumeMounts:
spec:
containers:
- name: hyperskill-backend
image: initram/hyperskill-clone:latest
volumeMounts:
- name: data
mountPath: /var/lib/app
- name: cache
mountPath: /tmp/shared
- name: app-config
mountPath: /etc/my_app/conf.d
readOnly: trueEach entry in volumeMounts references a volume by name and specifies a mountPath — the absolute path inside that container where the volume's contents will appear. You can also set readOnly: true to prevent the container from writing to it. You can also use subPath to mount only a single file or subdirectory from the volume rather than its entire contents.
The name in a volumeMount must match a name under spec.volumes.
Probes
By default, the Kubelet considers a container "running" as long as its process has not exited. This is not enough because a process can be up but unable to serve requests. Probes fill this gap by actively checking the container's actual state. You define them inside a container entry, and they come in three flavors:
startupProbe: for slow-starting applications. It runs first, and other probes are disabled until it succeeds.readinessProbe: decides whether the container should receive traffic. On failure, Kubernetes removes the Pod from available endpoints but does not restart it.livenessProbe: decides whether the container is still healthy. On repeated failure, the Kubelet restarts the container.
spec:
containers:
- name: hyperskill-backend
image: initram/hyperskill-clone:latest
ports:
- containerPort: 8000
protocol: TCP
startupProbe:
httpGet:
path: /healthy
port: 8000
initialDelaySeconds: 30
failureThreshold: 5
periodSeconds: 5
readinessProbe:
httpGet:
path: /ready
port: 8000
timeoutSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthy
port: 8000
periodSeconds: 10
timeoutSeconds: 3Each probe describes how to check the container and how often. The action can be an httpGet (a successful HTTP response), a tcpSocket (the port accepts a connection), or an exec (a command inside the container exits with code 0). You control timing with fields such as initialDelaySeconds, periodSeconds, and failureThreshold.
Keep in mind that the developer implements the endpoints and logic behind these checks (routes like /healthy) inside the application itself. Kubernetes only calls these endpoints, and the application must return a meaningful response.
Exposing and accessing ports
Once you apply this YAML file, you can reach it from your local machine using the kubectl port-forward command:
$ kubectl port-forward pod/hyperskill-backend 8000:8000This command opens a tunnel from a port on your machine to a port inside the pod. In this case, we're forwarding local port 8000 to port 8000 inside the hyperskill-backend Pod. While the command is running, you can open http://localhost:8000/tracks in your browser or use curl from your terminal, and the traffic will tunnel into the Pod.
Conclusion
Congratulations! You successfully ran a simple application inside a Kubernetes Pod. By understanding the necessary commands and syntax for creating Pod manifests, you can now deploy your applications in a Kubernetes cluster. Remember, this is only the starting point; later, we'll see how you can design self-healing applications with Deployments.