Introduction
Kubernetes offers multiple types of workloads to manage containerized applications. In this post, we’ll explore how to run an NGINX container using different Kubernetes objects like Pods, ReplicaSets, Deployments, StatefulSets, DaemonSets, Helm, and more — all with steps to expose them for access.
Prerequisites
- A running Kubernetes cluster (Minikube, KIND, EKS, GKE, or AKS) in our case we will use minikube cluster created locally
-
kubectl
CLI configured -
Helm
installed (for Helm-based setup)
1) Pod
A Pod is the most basic unit of deployment in Kubernetes. It represents a single instance of a running process, and in this case, it can be used to run a single NGINX container. Pods are ideal for quick testing or experimentation but aren’t suitable for production workloads since they don’t support replication, self-healing, or rolling updates. You can deploy a Pod using a simple YAML manifest and access it via port-forwarding or by creating a NodePort service temporarily.
A) Imperative way (via kubectl commands)
You tell Kubernetes exactly what to do right now.
kubectl run nginx-pod --image=nginx --port=80 -n default
Check if the pod is running:
$ kubectl get pods NAME READY STATUS RESTARTS AGE nginx-pod 1/1 Running 0 23s
Expose it:
kubectl expose pod nginx-pod --type=NodePort --port=80 --name=nginx-service
This will create a service of type NodePort and expose
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 9m22s nginx-service NodePort 10.96.83.111 <none> 80:32282/TCP 6s
Now, let’s try to access the Nginx application through the browser.
$ minikube service nginx-service --url http://127.0.0.1:51986 ! Because you are using a Docker driver on windows, the terminal needs to be open to run it.
Accessing the Nginx application is possible by visiting the provided URL, which is http://127.0.0.1:51986.
Cleanup:
# Delete the service kubectl delete service nginx-service # Delete the pod kubectl delete pod nginx-pod
b) Declarative way (via YAML manifests)
You describe the desired state in a YAML file and apply it.
nginx-pod.yaml
apiVersion: v1 kind: Pod metadata: name: nginx-pod labels: app: nginx spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 80
Apply it:
kubectl apply -f nginx-pod.yaml
Check if the pod is running:
$ kubectl get pods NAME READY STATUS RESTARTS AGE nginx-pod 1/1 Running 0 6s
Create a YAML file for nginx service:
nginx-service.yaml
apiVersion: v1 kind: Service metadata: name: nginx-service labels: app: nginx spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80
apply it:
kubectl apply -f nginx-service.yaml
This will create a service of type NodePort and expose
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 17m nginx-svc NodePort 10.108.129.175 <none> 80:30288/TCP 8s
And you can access the pod in same way as earlier
Cleanup:
# Delete the service using the YAML file kubectl delete -f nginx-service.yaml # Delete the pod using the YAML file kubectl delete -f nginx-pod.yaml
2) ReplicationController
ReplicationController (RC) is an older Kubernetes controller used to maintain a fixed number of identical Pods running at any given time. Though largely replaced by ReplicaSets and Deployments, RCs still exist and can be used to demonstrate how Kubernetes ensures availability by replacing failed Pods. To deploy NGINX using an RC, you define the number of replicas and a Pod template. Once deployed, you can expose the RC using a NodePort service to access NGINX from outside the cluster. It's mainly used today for legacy compatibility.
Create a YAML file:
nginx-replication-controller.yaml
apiVersion: v1 kind: ReplicationController metadata: name: nginx-rc spec: replicas: 2 selector: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 80
apply it:
kubectl apply -f nginx-replication-controller.yaml -n default
check if its running required number of pods:
$ kubectl get rc NAME DESIRED CURRENT READY AGE nginx-rc 2 2 2 32s $ kubectl get pods NAME READY STATUS RESTARTS AGE nginx-rc-j7fw2 1/1 Running 0 39s nginx-rc-x9gdk 1/1 Running 0 39s
Expose the it using service (we can use earlier nginx-service.yaml). The service selects the pods running with the label selector. The pods can be accessed using similar way mentioned earlier.
Cleanup:
kubectl delete -f nginx-replication-controller.yaml
3) ReplicaSet
ReplicaSet is the next evolution of ReplicationController, providing more flexible label selectors and tighter integration with modern workloads. While you can use a ReplicaSet directly to manage NGINX replicas, it’s commonly used under the hood by Deployments. This method ensures the desired number of NGINX Pods are always running. You can expose the ReplicaSet using a NodePort service to access it externally. Though you can use ReplicaSets directly, it's best practice to use them via Deployments for better lifecycle management and upgrade strategies.
Create a YAML filefor replica set:
apiVersion: apps/v1 kind: ReplicaSet metadata: name: nginx-replicaset labels: app: nginx spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx-container image: nginx:latest
and nginx-service.yaml
apiVersion: v1 kind: Service metadata: name: nginx-service labels: app: nginx spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80
Apply them:
kubectl apply -f nginx-replicaset.yaml -n default kubectl apply -f nginx-service.yaml -n default
Check if its running:
$ kubectl get all NAME READY STATUS RESTARTS AGE pod/nginx-replicaset-9w775 1/1 Running 0 35s pod/nginx-replicaset-q7kw4 1/1 Running 0 35s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 50m service/nginx-service NodePort 10.103.27.19 <none> 80:31199/TCP 19s NAME DESIRED CURRENT READY AGE replicaset.apps/nginx-replicaset 2 2 2 35s
You can access the pod as earlier.
Cleanup:
kubectl delete -f nginx-replicaset.yaml -n default kubectl delete -f nginx-service.yaml -n default
4) Deployment
Deployment is the most widely used workload controller in Kubernetes. It manages ReplicaSets and provides declarative updates for Pods and ReplicaSets, making it perfect for production scenarios. Deploying NGINX via a Deployment allows you to scale easily, perform rolling updates, and roll back if something goes wrong. This setup is ideal for running web servers in a highly available manner. After deploying, you can expose it using a Service of type NodePort, LoadBalancer, or via an Ingress to access NGINX from outside the cluster.
Create nginx-deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx-container image: nginx:latest
and nginx-service.yaml :
apiVersion: v1 kind: Service metadata: name: nginx-service labels: app: nginx spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80
Apply them:
kubectl apply -f nginx-deployment.yaml -n default kubectl apply -f nginx-service.yaml -n default
Check if everythins is running:
$ kubectl get all NAME READY STATUS RESTARTS AGE pod/nginx-deployment-58cdc7b878-bf7gh 1/1 Running 0 81s pod/nginx-deployment-58cdc7b878-w82sr 1/1 Running 0 81s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 55m service/nginx-service NodePort 10.99.103.182 <none> 80:30357/TCP 65s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/nginx-deployment 2/2 2 2 81s NAME DESIRED CURRENT READY AGE replicaset.apps/nginx-deployment-58cdc7b878 2 2 2 81s
Nginx can be accessed as mentioned earlier.
Cleanup:
kubectl delete -f nginx-deployment.yaml -n default kubectl delete -f nginx-service.yaml -n default
5) StatefulSet
StatefulSet is designed for managing stateful applications that require persistent storage and stable network identities. While NGINX is typically stateless, deploying it via a StatefulSet helps demonstrate scenarios where you need unique pod hostnames, such as with caches or databases. Each pod gets a sticky identity (e.g., nginx-0, nginx-1) and can be accessed via stable DNS addresses within the cluster. For external access, you need to configure an Ingress or LoadBalancer. Though not typical for NGINX, it's a good learning exercise for understanding StatefulSet behavior.
Create nginx-statefulset.yaml
apiVersion: apps/v1 kind: StatefulSet metadata: name: nginx-sts labels: app: nginx spec: replicas: 2 serviceName: nginx selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx-container image: nginx:latest
Create nginx-service.yaml
apiVersion: v1 kind: Service metadata: name: nginx-service labels: app: nginx spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80
This StatefulSet maintains two replicas and provides each Pod with a stable, unique network identity — a key requirement for stateful applications.
The serviceName field in a StatefulSet refers to a headless service that facilitates DNS-based discovery and direct communication with each individual Pod. It defines the service responsible for managing the network identity of the Pods within the StatefulSet.
Apply them:
kubectl apply -f nginx-statefulset.yaml -n default kubectl apply -f nginx-service.yaml -n default
Check if everything is running:
$ kubectl get all NAME READY STATUS RESTARTS AGE pod/nginx-sts-0 1/1 Running 0 13s pod/nginx-sts-1 1/1 Running 0 9s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 75m service/nginx-service NodePort 10.96.136.227 <none> 80:30171/TCP 6s NAME READY AGE statefulset.apps/nginx-sts 2/2 13s
Nginx can be accessed as mentioned earlier.
CLeanup:
kubectl delete -f nginx-statefulset.yaml -n default kubectl delete -f nginx-service.yaml -n default
6) DaemonSet
A DaemonSet ensures that a copy of a Pod (in this case, running NGINX) runs on every node in your cluster. This is useful when you want a service like NGINX to be available on all nodes, perhaps for node-local routing, logging, or monitoring purposes. You can configure each NGINX container with a hostPort to make it accessible via the host's network interface. Alternatively, use a NodePort service to reach any of the NGINX instances. This method is not typical for web servers in production, but it’s highly useful for infrastructure-level components.
Create nginx-daemonset.yaml
apiVersion: apps/v1 kind: DaemonSet metadata: name: nginx-daemonset labels: app: nginx spec: selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx-container image: nginx:latest
Create nginx-service.yaml
apiVersion: v1 kind: Service metadata: name: nginx-service labels: app: nginx spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80
apply them:
kubectl apply -f nginx-daemonset.yaml -n default kubectl apply -f nginx-service.yaml -n default
Check if everything is running:
$ kubectl get all NAME READY STATUS RESTARTS AGE pod/nginx-daemonset-nbf2h 1/1 Running 0 11s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 83m service/nginx-service NodePort 10.111.183.20 <none> 80:32707/TCP 4s NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE daemonset.apps/nginx-daemonset 1 1 1 1 1 <none> 11s
and you can access nginx as mentioned above.
Cleanup:
kubectl delete -f nginx-daemonset.yaml -n default kubectl delete -f nginx-service.yaml -n default
7) Helm
Helm is the package manager for Kubernetes that simplifies deployment through reusable, templatized YAML charts. Instead of manually creating manifests for each component, you can deploy NGINX using a Helm chart from a public repository like Bitnami. This method is ideal for teams managing complex applications or infrastructure, as Helm supports values overrides, upgrades, and rollbacks. Deploying NGINX via Helm is fast and efficient, and it also makes version control and customization easier. Services created by the chart often include a LoadBalancer or Ingress to expose NGINX automatically.
A) Official HELM chart
helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update helm install my-nginx bitnami/nginx
$ helm install my-nginx bitnami/nginx NAME: my-nginx LAST DEPLOYED: Thu Jul 31 12:49:13 2025 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: CHART NAME: nginx CHART VERSION: 21.0.8 APP VERSION: 1.29.0
Check if everything is running:
$ kubectl get all NAME READY STATUS RESTARTS AGE pod/my-nginx-864d6c4cfb-xmsl9 1/1 Running 0 99s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 107m service/my-nginx LoadBalancer 10.109.216.60 <pending> 80:32573/TCP,443:31138/TCP 99s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/my-nginx 1/1 1 1 99s NAME DESIRED CURRENT READY AGE replicaset.apps/my-nginx-864d6c4cfb 1 1 1 99s
Minikube doesn’t support cloud LoadBalancer services, so you’ll need to use Minikube’s tunnel or service exposure.
$ minikube service my-nginx --url http://127.0.0.1:58205 http://127.0.0.1:58206 ! Because you are using a Docker driver on windows, the terminal needs to be open to run it.
Cleanup:
bash
$ helm uninstall my-nginx
B) Create your own HELM chart
Create helm chart using:
helm create nginx-chart
This will create a new directory in your project called nginx-chart with the follwing structure:
\NGINX-CHART │ .helmignore │ Chart.yaml │ values.yaml ├───charts └───templates │ deployment.yaml │ hpa.yaml │ ingress.yaml │ NOTES.txt │ service.yaml │ serviceaccount.yaml │ _helpers.tpl └───tests test-connection.yaml
Here remove all files under templates/ and update with the below files.
\NGINX-CHART │ .helmignore │ Chart.yaml │ values.yaml │ ├───charts └───templates deployment.yaml NOTES.txt service.yaml _helpers.tpl
deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }} labels: {{- include "nginx-chart.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "nginx-chart.selectorLabels" . | nindent 6 }} template: metadata: labels: {{- include "nginx-chart.selectorLabels" . | nindent 8 }} spec: containers: - name: nginx image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: {{ .Values.service.port }} protocol: TCP
service.yaml
apiVersion: v1 kind: Service metadata: name: {{ .Release.Name }}-service labels: {{- include "nginx-chart.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: 80 protocol: TCP name: http selector: {{- include "nginx-chart.selectorLabels" . | nindent 4 }}
_helpers.tpl
{{/* Expand the name of the chart. */}} {{- define "nginx-chart.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "nginx-chart.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "nginx-chart.labels" -}} helm.sh/chart: {{ include "nginx-chart.chart" . }} {{ include "nginx-chart.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "nginx-chart.selectorLabels" -}} app.kubernetes.io/name: {{ include "nginx-chart.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }}
NOTES.txt
1. Get the application URL by running this commands: minikube service {{ .Release.Name }}-service --url
values.yaml
# Default values for nginx-chart. # This is a YAML-formatted file. # Declare variables to be passed into your templates. # This will set the replicaset count replicaCount: 2 # This sets the container image image: repository: nginx # This sets the pull policy for images. pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" # This is for setting up a service service: # This sets the service type type: NodePort # This sets the ports port: 80
Chart.yaml
# nginx-chart/Chart.yaml apiVersion: v2 name: nginx-chart description: A simple Helm chart for NGINX version: 0.1.0 appVersion: "latest"
Install the Chart
$ helm install nginx nginx-chart -n default NAME: nginx LAST DEPLOYED: Thu Jul 31 15:27:24 2025 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: 1. Get the application URL by running this commands: minikube service nginx-service --url
Check if everything is running
$ kubectl get all NAME READY STATUS RESTARTS AGE pod/nginx-549fdb7f67-s7xhz 1/1 Running 0 43s pod/nginx-549fdb7f67-zb8ff 1/1 Running 0 43s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 4h24m service/nginx-service NodePort 10.107.150.254 <none> 80:32339/TCP 43s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/nginx 2/2 2 2 43s NAME DESIRED CURRENT READY AGE replicaset.apps/nginx-549fdb7f67 2 2 2 43s
and you can access nginx as earlier
Cleanup:
helm uninstall nginx
8) Job and CronJob
Running Nginx using a Job or CronJob in Kubernetes is technically possible, but it's not typical, because Nginx is a long-running web server, and Jobs/CronJobs are designed for short-lived, one-time or periodic tasks (like batch processing, backups, etc.).
Still, for demo purposes or a special use case, here’s how you can run it:
A Job runs to completion, so you need to override the Nginx container to exit after a while — otherwise, the Job will hang.
apiVersion: batch/v1 kind: Job metadata: name: nginx-job spec: ttlSecondsAfterFinished: 30 # auto-delete 30s after success template: spec: containers: - name: nginx image: nginx:latest command: ["sh", "-c", "nginx & sleep 10"] restartPolicy: Never backoffLimit: 1
When a Job completes, its Pod moves to the Completed state, but still exists unless manually cleaned up.
Kubernetes has a feature called TTLAfterFinished that lets Jobs self-delete after a specified duration.
ttlSecondsAfterFinished: 30 # auto-delete 30s after success
If you want to run Nginx periodically, you can use a CronJob, again making sure it doesn't run forever.
apiVersion: batch/v1 kind: CronJob metadata: name: nginx-cronjob spec: schedule: "*/5 * * * *" successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 jobTemplate: spec: template: spec: containers: - name: nginx image: nginx command: ["sh", "-c", "nginx & sleep 10"] restartPolicy: OnFailure
When a CronJob completes, it creates a Job each time it runs. These jobs and their pods accumulate over time.
successfulJobsHistoryLimit: 3 # keeps only the most recent 3 successful jobs failedJobsHistoryLimit: 1 # keeps only the most recent 1 failed job
Conclusion
Running an NGINX container on a Kubernetes cluster can be achieved through various workload types — from basic Pods to sophisticated tools like Helm charts. Each method serves a unique purpose: Pods and ReplicationControllers help you learn the fundamentals, Deployments and ReplicaSets offer scalability and rollback features, while StatefulSets and DaemonSets cater to specialized workloads. Helm, on the other hand, enables modular, reusable, and version-controlled deployments that scale well in real-world environments.
Although Jobs and CronJobs are not typical choices for running long-lived services like NGINX, they are useful for short-term tasks or demo purposes — provided you handle their cleanup properly using TTLs or history limits.
Understanding when and why to use each method gives you flexibility and control over your Kubernetes workloads, making your architecture both robust and maintainable. Whether you're experimenting locally with Minikube or deploying on a cloud-based EKS cluster, these deployment strategies will enhance your confidence and proficiency in Kubernetes operations.
References:
Medium Blogs by Anvesh Muppeda: https://medium.com/@muppedaanvesh
Top comments (0)