Sitemap
Mustafa Bora

Mustafa Bora

Multi-container Pod Structure

16 min readApr 6, 2026

--

Hello everyone! In this document, we will cover a topic that often causes confusion in the Kubernetes world: the multi-container Pod structure. We know that a single Pod can host more than one container, but what does that really mean? Why would we need such a structure?

Many people who are new to Kubernetes assume that creating a separate Pod for each application is always enough. However, in some cases, complementary processes need to share the same lifecycle. This is exactly where the multi-container Pod structure comes into play.

This structure allows us to run multiple containers within the same Pod. These containers:

  • share the same network interface, including IP address and ports,
  • can use the same filesystem, for example through an emptyDir volume,
  • start together and terminate together.

This approach is generally preferred in scenarios where the main application and supporting processes need to work together. For example:

  • a log forwarder (sidecar) that collects logs from the main application,
  • a proxy or adapter running in front of the application,
  • supporting components such as a cache container that manages temporary data.

However, there is an important point here: the multi-container structure is not meant to pack multiple microservices into a single Pod. Its purpose is to manage different parts of the same workload together. If two applications need to scale differently or be updated independently, they should run in separate Pods.

A Simple Example

In the example below, one application container writes its logs to the /var/log/app directory. A second container in the same Pod, for example fluent-bit, then reads those logs and forwards them to a central logging system.

This time, however, we want to keep the log directory persistent. In other words, even if the Pod restarts, the logs should not be lost. For this reason, we will use a Persistent Volume and a Persistent Volume Claim.

apiVersion: v1
kind: Pod
metadata:
name: multi-container-pod
spec:
containers:
- name: app
image: busybox
command: ['sh', '-c', 'while true; do echo "log record $(date)" >> /var/log/app/app.log; sleep 2; done']
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: log-collector
image: busybox
command: ['sh', '-c', 'tail -f /var/log/app/app.log']
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
persistentVolumeClaim:
claimName: my-pvc

If we examine the YAML file here piece by piece:

containers:
This section defines all the containers that will run inside the Pod.
Normally, a Pod contains only one container, but in this example two containers share the same lifecycle.

app container
This container represents the main application.
It uses the busybox image, and we define the command to be executed directly in the command field.

If we examine the command used in this container:
sh -c: means run a shell command.

while true: creates an infinite loop.

echo "log record$(date)" >> /var/log/app/app.log: writes the current timestamp into the log file.

sleep 2: waits two seconds in each loop iteration before writing the timestamp again.
In other words, this container behaves like a simple application that continuously generates logs.

log-collector container
This is the helper container. It reads the logs produced by the main application in real time.

If we look at the command used in this container:

tail -f: continuously watches the file and prints new lines to the terminal as they are added.
This allows the logs to be monitored live, much like a log forwarder such as Fluent Bit or Vector.

volumeMounts
Both containers are mounted to the same volume named logs. This allows the containers to share the same filesystem.
In other words, while the app container writes logs, the log-collector container can read the exact same file.

volumes
Here, we use a Persistent Volume Claim named my-pvc.
This ensures that the log files are not lost even if the Pod is deleted, because the PVC is backed by an underlying Persistent Volume.

As we can see in this definition, both containers access the same volume named logs. In other words, in this example the output is stored persistently by using a Persistent Volume.

What Are Persistent Volume and Persistent Volume Claim?

At this point, we encounter two concepts for the first time:

  • Persistent Volume (PV)
  • Persistent Volume Claim (PVC)

Simply put:

A Persistent Volume represents a physical or virtual storage resource within Kubernetes. In other words, it is a disk or file storage area that has been provisioned in advance by the system administrator.

A Persistent Volume Claim is a request made by an application for a certain amount of that storage. The application basically says, “I need 1 GB of persistent storage,” and Kubernetes finds a suitable PV and binds it to the claim.

How does this work in the real world?

In a real-world scenario, a developer wants to keep the application’s data in a persistent storage area. However, they do not need to deal with details such as where the disk is physically located, which storage driver is being used, or which node it belongs to. They simply create a Persistent Volume Claim (PVC) saying, “I need 1 GiB of storage.” Kubernetes then finds a suitable Persistent Volume (PV) for that request and binds it to the PVC.

This provides several advantages:

  • Even if the Pod is deleted, the data remains on the PV.
  • If a new Pod is created using the same PVC definition, Kubernetes can bind it to the same PV and make the previous data available again.

This is especially valuable for stateful applications, such as databases, caches, or log storage.

However, instead of leaving this binding process as “background magic,” we will perform the matching manually here so that we can see exactly how the mechanism works step by step.

Persistent Volume and Persistent Volume Claim objects are not directly “created and run” resources like a Pod or a Deployment.
They are infrastructure components that define how Kubernetes should use storage resources.

In other words, when creating a PV or PVC, we need to clearly specify:

  • which disk or storage area will be used,
  • which access mode it will use,
  • and how much capacity it will provide.

Since Kubernetes cannot automatically infer these details, these resources are typically created through YAML definitions rather than short commands such as kubectl create deployment.

This approach gives us full control over the configuration and also makes it much easier to version these definitions, for example through Git.

Now, let’s start by creating a Persistent Volume.

apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv
spec:
claimRef:
namespace: default
name: my-pvc
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
storageClassName: manual
hostPath:
path: /mnt/data
persistentVolumeReclaimPolicy: Retain

If we examine the Persistent Volume structure here:

capacity.storage: 1Gi
This is the total capacity of the PV. In this case, it provides 1 GiB of storage.
If needed, different values such as 10Gi or 100Mi can also be specified.

→ This value must match the PVC’s resources.requests.storage value; otherwise, the binding will not happen.

accessModes: - ReadWriteOnce
This defines the access mode of the volume.

There are three main access types:

ReadWriteOnce (RWO): the volume can be read from and written to by only one node.

ReadOnlyMany (ROX): the volume can be read by multiple nodes, but not written to.

ReadWriteMany (RWX): the volume can be both read from and written to by multiple nodes.

→ RWO is the safest and most commonly used mode.
→ RWX is generally used with shared storage systems such as NFS or CephFS.

storageClassName: manual
This is the class that the PV belongs to.

When a PVC is created with the same storageClassName, Kubernetes can match it with this PV.
If automatic binding is not preferred, as in our example, a value such as manual can be used.

→ In real-world environments, this field is often defined with names such as standard, gp2, ssd, or nfs.

hostPath.path: /mnt/data
This shows where the volume is physically mounted on the node.

In this example, we are using the /mnt/data directory on the node.

→ Advantage: it is very easy to test and uses the local disk.
→ Disadvantage: if the Pod is scheduled on another node, this path will not exist there, because it relies on local storage.

For this reason, in production environments, remote storage solutions such as NFS, AWS EBS, GCE Persistent Disk, Ceph, or Azure Disk are usually preferred instead of hostPath.

persistentVolumeReclaimPolicy: Retain
This defines what happens to the volume when the Pod or PVC is deleted.

There are three main reclaim policies:

Retain: the volume is not deleted and the data is preserved. Cleanup must be done manually.

Delete: when the PVC is deleted, the volume is deleted as well. This is common in cloud-based storage systems.

Recycle: this used to simply clean the volume and make it reusable again, but it is now deprecated and is no longer used.

claimRef
This field means that “this PV is now being used by my-pvc.”

It can be added manually, but in most cases Kubernetes populates it automatically.

After creating the Persistent Volume, we can start creating the Persistent Volume Claim.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: manual
resources:
requests:
storage: 1Gi
volumeName: my-pv

If we examine the Persistent Volume Claim structure here:

resources.requests.storage
This is where we specify how much storage we need.

As in this example, we can request the full amount defined for the PV, or we can specify a different value depending on our needs.

volumeName: my-pv
Here, we bind our PVC to the Persistent Volume named my-pv.

storageClassName: manual
This value must be the same as the one defined on the PV we want to bind to. If the values are different, the binding will not succeed.

accessModes
The value here must match the access mode of the PV we want to bind to exactly.

After this stage, the Persistent Volume, Persistent Volume Claim, and the sample multi-container Pod we created can be created using the kubectl apply -f command. If the Pod is created before the PV and PVC, it will fail and will not be able to start.

We can verify that the PV, PVC, and multi-container Pod structure we created is working correctly by using the following commands and outputs.
Through these checks, we can confirm the volume binding, see that the Pod is running, and verify that both containers are using the same log file.

Get Mustafa Bora’s stories in your inbox

Join Medium for free to get updates from this writer.

We can use the kubectl get pv command to view that the Persistent Volume resource has been created, along with its capacity, access mode, and which PVC has claimed it.

kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
my-pv 1Gi RWO Retain Bound default/my-pvc manual <unset> 147d

We can check if the PersistentVolumeClaim resource has been successfully created and which PV it is connected to with the kubectl get pvc command. kubectl get pvc

kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
my-pvc Bound my-pv 1Gi RWO manual <unset> 147d

We can see that the Pod is actively running and both containers inside are ready using the `kubectl get pod` command. The `READY 2/2` expression indicates that both containers in the Pod have successfully started up.

kubectl get pod multi-container-pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
multi-container-pod 2/2 Running 2 (44m ago) 147d 192.168.230.6 k8s-worker-1 <none> <none>

We can verify that the logs generated by the app container are actually written to the shared volume by accessing the pod.

kubectl exec -it multi-container-pod -c app -- sh (Entry into the pod is made)
/ # tail -n 5 /var/log/app/app.log
log record Tue Mar 17 08:11:45 UTC 2026
log record Tue Mar 17 08:11:47 UTC 2026
log record Tue Mar 17 08:11:49 UTC 2026
log record Tue Mar 17 08:11:51 UTC 2026
log record Tue Mar 17 08:11:53 UTC 2026

We can verify that the same log file is visible to the second container using the following command. This indicates that the two containers are sharing data through the same volume.

kubectl exec -it multi-container-pod -c log-collector -- sh (Entry into the pod is made)
/ # tail -n 5 /var/log/app/app.log
log record Tue Mar 17 08:12:58 UTC 2026
log record Tue Mar 17 08:13:00 UTC 2026
log record Tue Mar 17 08:13:02 UTC 2026
log record Tue Mar 17 08:13:04 UTC 2026
log record Tue Mar 17 08:13:06 UTC 2026

We now understand the fundamentals of the multi-container Pod structure, volume relationships, and the persistent storage mechanism provided by Persistent Volume and Persistent Volume Claim.

Next, we can move on to the two most powerful complementary components of this structure: Init Containers and Sidecar Containers.
These two concepts are essential in real production environments, especially for controlling Pod behavior, managing startup order, and sharing common responsibilities such as log handling.

Init Container

An Init Container is a special container that runs before the main containers inside a Pod. It is generally used to check dependencies, prepare the environment, or create required files. The main application does not start until the Init Container has completed successfully.

For example, in a real-world scenario:

  • if an application needs to connect to a database before it starts,
  • or if some configuration files need to be copied from an external source,

these tasks can be handled by an Init Container.

To reinforce the concept with an example:

In this example, we want our nginx application to wait until MySQL becomes reachable before starting. For this, we first create the Pod and Service structure that provides the MySQL side. Then, the Init Container performs a reachability check against the MySQL service; once access is available, it completes, and the main application container starts.

For this example, we first create the init-demo Pod.
The role of this Pod is:

  • to check MySQL connectivity first,
  • to wait if MySQL is not ready yet,
  • to start the main application once MySQL becomes reachable.

This prevents the application from starting too early before its dependency is ready. This approach is especially useful in applications that depend on a database.

The YAML definition for the init-demo Pod will be as follows:

spec:
initContainers:
- name: wait-for-db
image: mysql:5.7
command:
- sh
- -c
- |
until mysqladmin ping -h 10.99.66.235 -uroot -proot123 --silent; do
echo "Waiting fot MySQL service..."
sleep 2
done
echo "MySQL service is accessible. Nginx is starting.."
containers:
- name: web
image: nginx:1.25
ports:
- containerPort: 80

To explain this YAML file here a little further:

initContainers:
This field defines the init containers that will run before the main containers.
The containers listed here run in sequence, and Kubernetes does not move on to the regular containers until they complete successfully.

until mysqladmin ping -h 10.99.66.235 -uroot -proot123 --silent; do
This is the line where the MySQL connectivity check begins.

The purpose of this command is:
to attempt a connection to the MySQL server using mysqladmin ping,

to use the MySQL Service’s ClusterIP address as the target with -h 10.99.66.235,

to specify the username with -uroot,

to specify the password with -proot123,

to use --silent so that the result is returned in a simple success/failure form.

The until structure here means:
keep repeating this command until it succeeds.

In other words, if MySQL is not yet reachable, the init container will continue waiting.

In the next step, after creating the init-demo Pod, we will create the MySQL server that this Pod depends on.

The role of this MySQL Pod is:

  • to run the MySQL database,
  • to accept connections on port 3306,
  • to act as the target that the init container will check.

In other words, this Pod serves as the dependent service in the init container scenario.

The YAML definition for the MySQL server will be as follow:

spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD
value: "root123"
ports:
- containerPort: 3306

When the Pods are created in sequence, if we create init-demo first, we can see that the init container enters a waiting state because it cannot reach its dependent service yet. During this time, the Pod remains in the Init:0/1 state.

kubectl get pods
NAME READY STATUS RESTARTS AGE
init-demo 0/1 Init:0/1 0 10s
multi-container-pod 2/2 Running 2 147d

kubectl logs init-demo -c wait-for-db
Waiting fot MySQL service...
mysqladmin: [Warning] Using a password on the command line interface can be insecure.
Waiting fot MySQL service...
mysqladmin: [Warning] Using a password on the command line interface can be insecure.

Later, when we bring up the MySQL Pod, we can see that the init container is eventually able to reach MySQL through its periodic checks and exits the waiting state.

kubectl get pods
NAME READY STATUS RESTARTS AGE
init-demo 1/1 Running 0 80s
multi-container-pod 2/2 Running 2 147d
mysql 1/1 Running 0 31s

kubectl logs init-demo -c wait-for-db
Waiting fot MySQL service...
mysqladmin: [Warning] Using a password on the command line interface can be insecure.
Waiting fot MySQL service...
mysqladmin: [Warning] Using a password on the command line interface can be insecure.
mysqld is alive
MySQL service is accessible. Nginx is starting...

Sidecar Container

We have now covered the pre-start checks performed before a Pod begins running by using an Init Container. Next, we move on to another structure that supports the main application while the Pod is already running: the Sidecar Container.

A Sidecar Container is a helper container that runs in the same Pod as the main application container. It is commonly used to process data produced by the main application, expose that data externally, or perform a supporting task. It runs alongside the main application and, when needed, shares the same volume, network space, and lifecycle.

If we need to:

  • expose content produced by the main application,
  • support the main application with a helper proxy or web layer,
  • serve data produced inside the same Pod through another container,

then these tasks can be handled through the Sidecar Container pattern.

To reinforce the concept with an example:

In this example, the app container will create an index.html file on a shared volume. A web sidecar container running in the same Pod will then publish that file using nginx.

For this example, we first create the sidecar-demo Pod.
The role of this Pod is:

  • to generate content in a shared area through the app container,
  • to publish that content through the web sidecar container,
  • to demonstrate how two containers can work together within the same Pod.

In this way, we can see that the Sidecar Container structure is a helper component that runs alongside the main application and uses the output produced by it.

The YAML definition for the sidecar-demo Pod will be as follows:

apiVersion: v1
kind: Pod
metadata:
name: sidecar-demo
labels:
app: sidecar-demo
spec:
volumes:
- name: shared-data
emptyDir: {}

containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- |
echo '<h1>kubestack v1</h1>' > /shared/index.html
echo 'The app container has been started, and index.html has been created.'
sleep 36000
volumeMounts:
- name: shared-data
mountPath: /shared

- name: web
image: nginx:1.25
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
ports:
- containerPort: 80

To explain this YAML file here a little further:

volumes
This field defines the shared volumes that will be used inside the Pod.

name: shared-data
This is the name we give to the shared volume.

emptyDir: {}
This line creates an empty directory when the Pod is created. The containers inside the Pod can use this area as a shared space.

In this example, our goal is not to keep data persistent, but to demonstrate how two containers can share the same file area. That is why we use emptyDir.

web container

containerPort: 80
This indicates that Nginx serves traffic on port 80 inside the Pod.
Later, this port can be exposed externally through a Service.

After this stage, we need to define a Service so that we can access the sidecar-demo Pod from outside.

Our goal here is not to explain the Service structure in detail, but simply to make the nginx output running inside the sidecar container accessible from outside the Pod.

For this reason, in this example we will define a simple Service of type NodePort. This will allow us to access the web sidecar container running inside the Pod through a browser.

We will cover the working logic, types, and usage scenarios of Service structures in much more detail in the following sections.
Here, we are only setting up the connection required to test the sidecar example from outside.

The sidecar-demo-service.yaml file will be as follows:

apiVersion: v1
kind: Service
metadata:
name: sidecar-demo-service
spec:
type: NodePort
selector:
app: sidecar-demo
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080

The key points to consider in this yaml file are:

With type: NodePort, we enable access from outside the Pod.
With selector, we define which Pod this Service should route traffic to.
With nodePort: 30080, we specify the port that will be used for external access.

To apply the Service definition shared above and make it active, we use:
kubectl apply -f sidecar-demo-service.yaml

With this structure, we can test the nginx service running inside the sidecar container through a browser.

After creating and bringing the Pod and Service resources defined in the YAML files above into a running state, we can use the external IP address of the node where our sidecar-demo Pod is running, together with the port value we defined through the NodePort configuration, to view the content we wrote inside the container.

As a continuation of our example, we can update the HTML file inside the app container and test how the same structure changes instantly.
To make this change, we first need to enter the app container inside our Pod.

kubectl exec -it sidecar-demo -c app -- sh

In the next step, we update the text with the following command.

echo '<h1>kubestack v2</h1>' > /shared/index.html

After this change, we can view the updated text when we refresh the page in our browser.

In this example, the role of the sidecar container is to expose the content produced by the main container to the outside world. While the app container creates or updates the index.html file on the shared volume, the web sidecar container uses the same volume to publish that file through nginx. In this way, the sidecar container acts as a helper component that makes the output produced by the main application accessible.

In short, in this structure the main container produces the content, while the sidecar container serves that content. This clearly demonstrates the sidecar approach as a helper container pattern that runs alongside the main application and supports its function.

In this section, we did more than simply understand the basic logic of the multi-container Pod structure. We also covered, step by step, in which scenarios containers running inside the same Pod should be positioned together, how shared volumes are used, how Persistent Volume and Persistent Volume Claim structures come into play when persistent storage is needed, and how these resources are associated with a Pod. We then examined, in practice, how dependency checks can be performed before a Pod starts by using an Init Container, why a Pod remains in a waiting state when its dependent service is not yet ready, and how the main container starts once that service becomes reachable. Finally, by exploring the Sidecar Container structure, we reinforced with practical examples how the output produced by the main application can be served by a helper container, and how containers within the same Pod can share responsibilities. In this sense, this section allowed us to evaluate the multi-container Pod approach not only theoretically, but also as a complete concept together with its critical components such as storage, startup order, and helper container usage.

Thank you for taking the time to read this document. I hope it contributes to your Kubernetes learning journey. See you in the next module!

Press enter or click to view image in full size

--

--