Deploying an application to managed Kubernetes requires a ready-to-use container image, kubectl access to the cluster, Helm, an Ingress Controller, a domain, and TLS.
The basic flow is: Container Registry → Deployment → Pod → Service → Ingress → domain → HTTPS.
Helm simplifies release installation, upgrades, and rollbacks, while Kubernetes maintains the required number of replicas and handles rolling updates. A private Registry requires an imagePullSecret, while automated TLS requires cert-manager.
Before deploying to production, verify probes, resource requests and limits, service account permissions, Secret storage, pinned image versions, and rollback compatibility with database migrations.
What We Will Deploy and Configure in This Article
Welcome!
Deploying an application in Kubernetes differs significantly from the familiar single-VM approach. Simply copying files to a server, starting a process, and opening a port is not enough.
The application runs inside a Pod, is managed by a Deployment, receives a stable internal address through a Service, and is exposed externally through an Ingress. HTTPS is typically configured using a separate mechanism for issuing TLS certificates. If the image is stored in a private Container Registry, the cluster also needs credentials to pull it.
To make all these components easier to manage, Helm is often used on top of Kubernetes. It lets you package a Deployment, Service, Ingress, and other resources into a single chart, move configurable parameters to values.yaml, and then update or roll back the application as a separate release.
In this article, we will go through the entire process: connecting to a managed cluster using kubectl, configuring access to a private Container Registry, examining Deployments and Services, creating a Helm chart, installing an Ingress Controller, and configuring a domain and TLS. We will then update the release, perform a rollback, change the number of replicas, and cover basic Pod troubleshooting.
We will not scatter commands and YAML throughout the theoretical subsections. First, we will examine what exactly is happening and why each component is needed, and at the end of each chapter, we will present the practical steps separately.
How the Application Will Be Deployed
Before connecting to the cluster, it is helpful to review the overall architecture.
In Kubernetes, an external request passes through several layers. Each layer serves a specific purpose, so an issue with one component does not necessarily mean that the entire application is down.
In simplified terms, a user request follows this path:

Helm and the Container Registry operate separately from this request flow.
The Container Registry stores the application image that Kubernetes uses to create containers. Helm, in turn, helps define and manage all Kubernetes resources as a single release.
Components Involved in the Architecture
Let’s start with the application itself.
In Kubernetes, the application does not run directly on a virtual machine. Instead, it runs in a container. This container is part of a Pod—the smallest unit that Kubernetes deploys to the cluster’s worker nodes.
However, you generally do not need to create individual Pods manually.
A Deployment handles this task. It defines the desired state of the application: which image to run, how many instances should be running, and how they should be updated.
If a Pod crashes, the Deployment creates a replacement through the associated Kubernetes mechanisms.
The next component is a Service.
A Pod should not be treated as a permanent server. It may be recreated, assigned a different IP address, or moved to another node. This makes it impractical for other application components to access a specific Pod directly.
A Service provides a stable access point for a group of matching Pods and routes traffic to running application instances.
However, a Service alone does not provide a convenient way to expose a website to the internet.
This is where Ingress comes in.
Ingress defines routing rules. For example, requests to app.example.com should be routed to a specific Service.
However, the Ingress object itself does not accept traffic from the internet. Its rules must be enforced by a separate Ingress Controller.
The Ingress Controller is the component that actually accepts external HTTP and HTTPS traffic and routes it within the cluster.
TLS is added on top of this architecture. You can upload a certificate manually, but for ongoing operation, it is much more convenient to use cert-manager, which can automatically obtain and renew certificates through ACME.
Finally, Helm brings all these resources together into a single managed release.
The resulting flow is as follows:

Helm manages the definition of this architecture and its versions.
How Managed Kubernetes Differs from a Standard Virtual Machine
With a standard VPS, users are typically responsible for almost everything themselves.
They need to install the operating system and packages, configure network services, launch the application, monitor its process, and plan for recovery from failures.
Kubernetes takes a different approach.
The user specifies the desired state, and the cluster itself strives to maintain it.
For example, instead of specifying “start the application process,” you define the desired state: “three instances of this container must be running at all times.”
If one instance disappears, Kubernetes will attempt to create a replacement.
With managed Kubernetes, the cloud provider also handles some of the infrastructure work. The provider typically maintains the cluster’s management layer—the control plane, the Kubernetes API, and related system components.
Users do not need to manually set up their own API Server, etcd, or other control plane components.
However, this does not mean that managed Kubernetes fully manages the application itself.
Users remain responsible for:
- Deployment;
- Service;
- Ingress;
- Container images;
- Secrets;
- Pod resources;
- Application updates;
- Access permissions;
- Workload diagnostics.
In other words, the provider maintains Kubernetes as a platform, while we manage what is deployed on top of it.
Prerequisites
The first requirement is an existing Kubernetes cluster.
Your provider must supply a kubeconfig file or another supported method for accessing the Kubernetes API.
kubectl and Helm must be installed on your workstation.
kubectl will be used to interact directly with Kubernetes, including viewing Pods, Services, events, and other resources.
Helm will be used to install the application and manage it as a release.
You will also need a ready-to-use container image of the application.
This article assumes that the application has already been built into a Docker image and pushed to a Container Registry.
If the Registry is private, you will need credentials that Kubernetes can use to pull the image.
You will need a domain or subdomain to expose the application. Its DNS record will later point to the Ingress Controller’s external address.
Finally, enabling HTTPS requires the ability to verify domain ownership so that cert-manager can obtain a certificate through ACME.
What to Prepare Before Deployment
Before starting the hands-on portion, it is helpful to gather all the required information in one place.
This way, you will not have to pause the setup later because of a missing Registry endpoint, an unknown image name, or a domain that is not ready.
| Component | Purpose | What must be ready |
| Managed Kubernetes | Application runtime environment | A provisioned cluster and access to the Kubernetes API |
| kubeconfig | Connecting kubectl to the cluster | A file or data provided by the cloud provider |
kubectl | Managing Kubernetes resources | Installed on the workstation |
Helm | Installing and managing releases | Installed and available in the terminal |
| Container Registry | Storing the application image | An uploaded image with a meaningful tag |
| Registry credentials | Pulling a private image | Credentials with permission to pull the image |
| Domain | Public address of the application | Access to DNS management |
| Ingress Controller | Handling incoming external traffic | We will install it later in this article |
| cert-manager | Automated TLS management | We will install it before issuing the certificate |
| Application | What we will deploy | The container must start and listen on a known port |
At this stage, we are not making any changes to the cluster. We have only defined the architecture and confirmed that all the required information is available.
Connecting to a Kubernetes Cluster with kubectl
Now that we have covered the deployment architecture, we can move on to the cluster itself.
The first practical step is to configure kubectl to connect to the correct Kubernetes API and run commands in the appropriate environment.
This is especially important in managed Kubernetes, where the provider operates the control plane and users are typically provided with the necessary connection details.
What Is kubeconfig
kubectl does not connect to a cluster on its own.
It requires a kubeconfig—a configuration file that specifies where to send requests and which credentials to use to authenticate access.
A kubeconfig file typically contains several types of data:
- Kubernetes API address;
- Cluster information;
- User credentials;
- Certificates or tokens;
- Context;
- Selected namespace.
In managed Kubernetes, this file can usually be downloaded from the provider’s control panel or obtained through its CLI.
By default, kubectl looks for the configuration in the user’s home directory:
~/.kube/config.
However, you can use a different file if necessary.
It is important to treat kubeconfig as sensitive data. Depending on the authentication method, it may contain certificates, tokens, or other data that provides access to the cluster.
Contexts and Why Checking the Active Cluster Matters
A single kubeconfig can contain multiple clusters.
For example, a developer may have all of the following:
- A test cluster;
- Staging;
- Production;
- A separate local Kubernetes cluster.
kubectl uses a context to determine which cluster to work with.
A context links together:
- A cluster;
- A user;
- Optionally, a namespace.
Conceptually, it can be represented as follows:
context = cluster + credentials + namespace.
This is very useful, but it also introduces a risk.
If someone is accustomed to working in a test cluster but the active context unexpectedly points to production, a routine command that makes changes may be applied to the wrong cluster.
For this reason, it is good practice to check the active context before performing any significant operation.
This is especially important before:
- Installing a Helm release;
- Deleting resources;
- Updating a Deployment;
- Scaling;
- Performing a rollback.
The more clusters you work with, the more useful it is to explicitly keep track of the current context.
How to Verify That the Cluster Is Accessible
Simply having a kubeconfig file does not guarantee access.
The configuration may be outdated, the credentials may have been revoked, or the API endpoint may be inaccessible from the current network.
It is therefore best to verify connectivity in several steps.
First, make sure that kubectl knows which context is active.
Next, verify access to the Kubernetes API.
Then, retrieve the list of cluster nodes. If the control plane responds and the user has the required permissions, kubectl will display the worker nodes.
For managed Kubernetes, this is a particularly useful check: it shows not only that the API is accessible, but also the worker nodes on which Pods will later run.
It is also worth checking the namespaces.
Namespaces provide logical separation of resources within a single cluster. For example, applications may reside in the default, staging, or production namespace, or in a dedicated project namespace.
If no namespace is specified explicitly, kubectl will use the namespace configured in the context, or default.
Before deployment, it is therefore helpful to know exactly where the resources will be created.
Connection and Verification Commands
If the provider supplied a separate kubeconfig file, you can use it via an environment variable or move it to the default path.
You can then perform the basic checks as follows:
| What to Check | Command | What It Shows |
| Current context | kubectl config current-context | Which cluster is currently selected |
| All available contexts | kubectl config get-contexts | Which environments are available |
| Kubernetes API information | kubectl cluster-info | The addresses of the cluster’s core components |
| Worker nodes | kubectl get nodes | Available nodes and their status |
| Namespaces | kubectl get namespaces | Logical scopes within the cluster |
| Resources in the current namespace | kubectl get all | The main objects that already exist |
To switch to a different context:
kubectl config use-context <context-name>
When using a separate kubeconfig file, it is convenient to specify it explicitly first:
export KUBECONFIG=/path/to/kubeconfig
Then run the remaining checks.
At this stage, the expected result is as follows: kubectl shows the correct context, cluster-info responds without errors, the worker nodes are in the Ready state, and the list of namespaces loads successfully.
We now have confirmed access to the cluster and know which context will be used for the next steps.
Next, we will prepare the second key part of the setup: the application’s container image and Kubernetes access to the private Container Registry.
Preparing the Application Image and a Private Container Registry
Now that connectivity to the cluster has been verified, you can move on to what Kubernetes will actually run.
On a conventional virtual machine, you can deploy an application from source code, install its dependencies, and start the process manually. Kubernetes uses a different model: the application is first packaged as a container image, and the cluster then creates containers from that image inside a Pod.
Therefore, before creating a Deployment, you need to ensure that the image exists, is accessible from the cluster, and, if necessary, is secured with dedicated credentials.
Why Kubernetes Runs a Container Image Instead of Source Code
Kubernetes does not build applications from source code.
Its role is to run a prebuilt container with the required number of replicas and maintain the desired state.
The workflow is typically as follows:

This separates two distinct processes.
The build process produces a deployable version of the application with the required dependencies.
Kubernetes is responsible for running that version: scheduling Pods, restarting them after failures, scaling, and performing updates.
This approach makes deployments more predictable. The same image can run in both test and production clusters without rebuilding dependencies on each node.
Why You Need a Container Registry
A container registry is a repository for container images.
Once an image has been built, it is pushed to the registry. When Kubernetes creates a Pod, it pulls the image from the registry to the appropriate worker node.
An image is typically specified using the registry address, project name, and version tag.
For example, an image reference might look like this:
registry.example.com/myproject/webapp:1.0.0.
Here, 1.0.0 is the tag for a specific version of the application.
Using fixed tags is especially important in Kubernetes because they make it possible to determine exactly which version is currently deployed.
If you always use latest, it becomes more difficult to determine exactly which code is running in a particular Pod, and updates and rollbacks become less predictable.
For production environments, it is therefore better to tie releases to specific image versions.
How a Private Registry Differs from a Public Registry
An image can be pulled from a public registry without additional authentication.
For example, public images for Nginx, Redis, and other popular services often work this way.
A private registry requires authentication.
The reason is simple: it may contain private application images that cannot be made available to just any client on the internet.
For developers, this usually means authenticating before pushing or pulling an image.
The situation is similar for Kubernetes, except that the cluster itself must obtain the credentials.
If a Deployment references a private image and Kubernetes does not have the registry credentials, the Pod will be unable to pull the image.
In this case, it will often enter a state such as ImagePullBackOff or ErrImagePull.
In other words, the problem occurs before the application even starts: there is no image from which to create the container.
How Kubernetes Accesses a Private Registry
Registry credentials in Kubernetes are typically stored in a special type of Secret for Docker registries.
This Secret contains the data required for authentication:
- Registry address;
- Username;
- Password or token;
- Email address, if required.
The Deployment can then reference the Secret through imagePullSecrets.
Here is a simplified diagram:

It is important to understand that the Secret must be in the same namespace in which the Pod is created.
If the Secret is created in default while the Deployment is in production, Kubernetes cannot simply use it across namespaces.
This is a common reason why an apparently correctly configured imagePullSecret does not work.
Another important point is that a Secret does not automatically secure registry credentials.
Kubernetes stores a Secret as a dedicated object, but it should not be considered a full-fledged external secrets manager. Access to Secrets must be restricted through RBAC, and the credentials themselves should be granted only the minimum permissions required.
Registry and imagePullSecret Commands
Now we can move on to the practical steps.
If the image has already been pushed to a private registry, first make sure you know its full image reference.
For example:
registry.example.com/myproject/webapp:1.0.0
To test access to the registry locally, log in with Docker:
docker login registry.example.com
Now create a Secret for Kubernetes:
kubectl create secret docker-registry registry-secret \
--docker-server=registry.example.com \
--docker-username=<username> \
--docker-password=<password> \
--docker-email=<email> \
--namespace=default If the registry uses a token instead of a password, pass the token to —docker-password.
Verify that the Secret was created:
kubectl get secret registry-secret --namespace=default
You can also check its type:
kubectl get secret registry-secret \
--namespace=default \
-o jsonpath='{.type}' Expected type:
kubernetes.io/dockerconfigjson
Quick Command Reference
| Task | Command | What It Verifies |
| Log in to the private registry locally | docker login registry.example.com | The credentials work correctly |
| Create an imagePullSecret | kubectl create secret docker-registry registry-secret ... | Kubernetes receives the registry credentials |
| Check the Secret | kubectl get secret registry-secret -n default | The object was created in the correct namespace |
| Check the Secret type | kubectl get secret registry-secret -n default -o jsonpath='{.type}' | The kubernetes.io/dockerconfigjson type is used |
| View Secrets in the namespace | kubectl get secrets -n default | The Secret is available in the same namespace as the future Deployment |
Kubernetes now knows where to pull the application image from and which credentials to use to access the private registry.
The next step is to define the application within the cluster: create a Deployment to manage the Pods and a Service to provide those Pods with a stable access point.
Understanding Deployments and Services
Two key objects come into play here: Deployment and Service. The former manages the lifecycle of Pods, while the latter provides stable access to them within the cluster.
They are often used together because working directly with individual Pods is impractical: Pods can be recreated, change IP addresses, and disappear during updates.
Why You Need a Deployment
A Deployment describes the desired state of an application.
It typically specifies:
- Container image;
- Number of replicas;
- Container ports;
- Environment variables;
- Resource constraints;
- Update strategy;
- ImagePullSecrets, if the image is stored in a private registry.
A Deployment does not run the application directly. It manages a ReplicaSet, which in turn maintains the required number of Pods.
It works like this:

For example, if three replicas are specified, Kubernetes will continuously attempt to maintain three matching Pods.
If one of them fails or a node becomes unavailable, the cluster will attempt to create a replacement.
This is why using a Deployment is more convenient than creating Pods manually: instead of defining a specific instance, we define the state that Kubernetes must maintain.
Why You Should Not Access a Pod Directly
Each Pod receives its own IP address within the cluster.
At first glance, it may seem reasonable to access the Pod directly. However, its IP address cannot be considered permanent.
A Pod may be recreated because of:
- A Deployment update;
- An application failure;
- A node restart;
- A change in the number of replicas;
- The workload being moved to another node.
When a Pod is recreated, the new Pod may receive a different IP address.
Therefore, tying other application components to a specific Pod IP address is unreliable.
The problem is especially apparent when multiple replicas are running. If three Pods are running simultaneously, the client must somehow choose among them and account for the fact that the set of instances may change at any time.
Kubernetes uses a Service to solve this problem.
What a Service Does
A Service provides a stable network endpoint for a group of Pods.
It identifies matching instances by their labels and routes traffic to them.
For example, a Deployment can assign the following label to its Pods:
app: webapp.
The Service then selects all Pods with the same label and routes requests to them.
This results in the following setup:

If a Pod disappears, the Service stops routing traffic to it. If the Deployment creates a new instance with the matching label, it is automatically added to the group.
A Service of type ClusterIP is sufficient for our application.
It creates an internal endpoint that is accessible within the Kubernetes cluster.
There is no need to expose the Service directly to the internet at this stage. Later, the Ingress Controller will receive external traffic and forward it to this Service.
How containerPort, port, and targetPort are related
At this stage, it is easy to get confused by the different port numbers.
You will encounter containerPort, port, and targetPort, but they apply at different levels.
containerPort specifies the port on which the application listens inside the container.
For example, if a web application runs on port 8080, you can specify this value in the Deployment.
containerPort does not expose the application externally by itself. Its primary purpose is to indicate which port the container uses.
targetPort is defined in the Service and specifies where traffic should be sent within the Pod.
If the application listens on port 8080, targetPort will usually also be set to 8080.
port, in turn, is the Service’s own port.
For example, you can configure it as follows:

Other Kubernetes components will then access the Service on port 80, and the Service will forward the requests to the container on port 8080.
This is useful because the application’s external-facing port does not have to match the internal port used by the process.
The key is to ensure that targetPort matches the port on which the application actually listens inside the container.
Hands-on: Creating a Deployment and Service
We can now create the minimal manifests.
For this example, we will use:
- The webapp application;
- The registry.example.com/myproject/webapp:1.0.0 image;
- One Pod;
- Application port 8080;
- The previously created Secret named registry-secret.
Create a Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
imagePullSecrets:
- name: registry-secret
containers:
- name: webapp
image: registry.example.com/myproject/webapp:1.0.0
ports:
- containerPort: 8080 It is especially important that selector.matchLabels matches the labels in the Pod template.
Now add a Service:
apiVersion: v1
kind: Service
metadata:
name: webapp
spec:
type: ClusterIP
selector:
app: webapp
ports:
- port: 80
targetPort: 8080 The Service selects the Pod with the app: webapp label and forwards incoming traffic from its port 80 to port 8080 in the container.
After saving the manifests, for example as deployment.yaml and service.yaml, apply them to the cluster.
| Task | Command | What to verify |
| Create the Deployment | kubectl apply -f deployment.yaml | Kubernetes has accepted the application definition |
| Create the Service | kubectl apply -f service.yaml | An internal endpoint has been created |
| Check the Deployment | kubectl get deployment webapp | The required number of replicas is available |
| Check the Pod | kubectl get pods | The Pod is in the Running state |
| Check the Service | kubectl get service webapp | The Service has been created and has a ClusterIP |
| Check the related resources | kubectl get pods -l app=webapp | The selector finds the Pod as expected |
If the Pod does not transition to Running, do not proceed to Ingress and TLS yet. First, make sure that the application itself starts successfully in Kubernetes.
We now have a running application in the cluster and a stable Service in front of it.
Packaging the Application as a Helm Chart
The Deployment and Service are already running, but they currently exist as separate YAML manifests.
This is sufficient for a single test application. However, once you introduce multiple environments, new image versions, scaling, or rollback requirements, manually editing several files becomes cumbersome.
This is where Helm comes in.
Helm does not replace Kubernetes. Instead, it adds a more convenient management layer on top of standard manifests, providing templates, configuration parameters, release versioning, and a unified installation process.
Why Use Helm When You Already Have YAML?
A standard Kubernetes manifest is a complete definition of a specific resource.
For example, a Deployment directly specifies:
- Application name;
- Container image;
- Tag;
- Number of replicas;
- Port;
- Secret name;
- Other parameters.
The problem arises when some of these values need to change.
For example, a test environment might use one image tag and one replica, while production uses a different tag and three replicas.
Without Helm, you have to either edit the YAML manually or maintain several nearly identical files.
Helm lets you separate the values that may change while keeping the common resource structure in templates.
As a result, the same chart can be used for different environments by changing only the parameters.
Helm also keeps track of installed releases. This will be useful later when we update the application and perform a rollback.
What a Helm Chart Consists Of
A Helm chart is a directory that describes an application.
In a simple case, it contains several key components.
Chart.yaml contains information about the chart itself, including its name, version, and internal metadata.
values.yaml stores the values that are substituted into the templates.
The templates directory contains Kubernetes manifests in template form.
The structure looks like this:

We will add Ingress a little later; for now, we are interested in Deployment and Service.
The basic idea behind Helm is simple: the structure of the Kubernetes resources remains in templates, while parameters that may change are moved to values.yaml.
What to Put in values.yaml
The values.yaml file should contain values that may vary between versions or environments.
For example:
- Container image name;
- Image tag;
- Number of replicas;
- Service port;
- Application port;
- imagePullSecret name;
- Domain;
- Ingress settings;
- Resource requests and limits;
- TLS settings.
Do not turn values.yaml into a dumping ground for every possible value.
If a parameter never changes and is part of the chart’s logic, it can remain in the template.
However, values that an administrator will actually change during deployment are better defined separately.
For example, updating the application version then becomes a matter of changing a single image tag rather than searching for the relevant line in the Deployment.
How Helm Generates the Final Kubernetes Manifests
Helm does not create a separate resource type.
Ultimately, Kubernetes still receives standard Deployment, Service, Ingress, Secret, and other objects.
The only difference is how these manifests are prepared.
Helm takes templates from the templates directory, substitutes values from values.yaml, and generates the final YAML.
The process works as follows:

For example, instead of a hardcoded image, a Deployment template can use a value from values.yaml.
The same applies to the number of replicas and to ports.
This keeps the chart reusable, while the specific configuration is defined separately.
The final manifests can be checked before installation. This is useful because it allows you to see exactly what Helm is going to send to the Kubernetes API.
Hands-on: Creating and Validating a Helm Chart
Create a chart scaffold:
helm create webapp
Helm will create a standard directory structure with several ready-to-use templates.
You can remove unnecessary files or adapt them to the application.
To get started, you can use the following values.yaml snippet:
replicaCount: 1
image:
repository: registry.example.com/myproject/webapp
tag: "1.0.0"
pullPolicy: IfNotPresent
imagePullSecrets:
- name: registry-secret
service:
type: ClusterIP
port: 80
targetPort: 8080 The Deployment and Service templates should then use these values instead of hard-coded parameters.
Before installing the chart, first validate its structure and syntax.
As usual, here is a table to help you navigate the commands:
| Task | Command | What to verify |
| Create a chart scaffold | helm create webapp | A standard Helm structure has been created |
| Validate the chart | helm lint ./webapp | Structural and template errors |
| View the rendered YAML | helm template webapp ./webapp | Which manifests will be sent to Kubernetes |
| Install the release | helm install webapp ./webapp | Application resources have been created |
| View installed releases | helm list | Helm recognizes the installed release |
| Verify the created resources | kubectl get pods,svc | The Deployment and Service are actually working |
If the chart is already installed and you need to apply changes, we will later upgrade the release instead of running install again.
The Deployment and Service are now managed as a single Helm release rather than as a set of independent YAML files.
The next step is to expose the application externally. To do this, we will install an Ingress Controller and examine how it differs from the Ingress object itself.
Installing the Ingress Controller
Why Ingress Is Needed
Before proceeding with the installation, it is worth clarifying the terminology and distinguishing between two concepts.
A ClusterIP Service works only within the cluster.
It is well suited for communication between components, but does not provide public access from the internet on its own.
Ingress is used to define routing rules for external HTTP and HTTPS traffic.
For example, you can specify that requests to app.example.com should be routed to the webapp Service, while requests to api.example.com should be routed to a different Service.
In other words, Ingress connects an application’s external address to internal Kubernetes resources.
However, it is not a standalone web server and does not accept traffic on its own.
How Ingress Differs from an Ingress Controller
Ingress is a Kubernetes object that defines routing rules.
An Ingress Controller is the component that actually enforces these rules.
In summary:
- Ingress → defines where requests should be routed
- Ingress Controller → receives requests and applies these rules
If you create an Ingress without installing an Ingress Controller, there will be no component to enforce the rules.
The Ingress Controller:
- listens for incoming external HTTP and HTTPS traffic;
- monitors Ingress objects in the Kubernetes API;
- determines which Service corresponds to each domain and path;
- routes requests into the cluster.
NGINX Ingress Controller remains one of the most widely used options.
How Traffic Reaches an Application from an External Address
Once the Controller is installed, the request path is nearly complete.
A user accesses the domain, and DNS directs the request to an external address assigned to the Ingress Controller or an associated cloud load balancer.
The Controller then checks the Ingress rules and determines which Service to route the request to.
The Service then selects an appropriate Pod.
The resulting request flow is as follows:

This is one of the core principles of Kubernetes networking: each layer has a specific responsibility.
DNS handles name resolution, Ingress defines the routing rules, a Service provides stable access to a group of Pods, and a Deployment ensures that the required number of Pods exist.
Key Considerations for Managed Kubernetes
In managed Kubernetes, the external address for an Ingress Controller is often provisioned through the cloud provider’s infrastructure.
For example, the controller’s Service can be configured with the LoadBalancer type, after which the provider automatically provisions an external load balancer and attaches it to the cluster.
Its IP address or hostname will then need to be specified in DNS.
However, there is an important caveat: the mechanism depends on the specific managed Kubernetes service.
With one provider, an external IP address may appear automatically within a few seconds.
With another, you may need to create a Load Balancer separately.
A third may use special annotations or its own cloud load balancer controller.
The Kubernetes logic remains consistent, but the specific method for obtaining an external address should be checked against the provider’s documentation.
There is another practical consideration: after a Service of type LoadBalancer is created, its external address may remain in the pending state for some time.
This does not necessarily indicate an error. The cloud platform needs time to provision the external resource and associate it with Kubernetes.
Commands for Installing and Verifying the Ingress Controller
As an example, we will install the NGINX Ingress Controller using Helm.
First, add the official chart repository and update its index:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update Now install the controller in a separate namespace:
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace After installation, check the Pod:
kubectl get pods -n ingress-nginx
Then check the Service:
kubectl get svc -n ingress-nginx
We are particularly interested in the controller Service of type LoadBalancer.
Once an external address is assigned, it will appear in the corresponding field.
For a more detailed check, you can view the resources of the Helm release itself:
helm list -n ingress-nginx
Command Quick Reference
| Task | Command | What to Verify |
| Add the Helm Repository | helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx | The chart repository is accessible |
| Update the Helm Repository Index | helm repo update | The latest chart versions have been retrieved |
| Install the Controller | helm install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginx --create-namespace | The Ingress Controller resources have been created |
| Check the Pod | kubectl get pods -n ingress-nginx | The Controller is in the Running state |
| Check the Service | kubectl get svc -n ingress-nginx | An external address has been assigned |
| Check the Helm Release | helm list -n ingress-nginx | The release has been installed successfully |
The cluster now has an endpoint that can accept external HTTP and HTTPS traffic. The next step is to associate this address with a domain so that users do not have to access the application via an IP address or a technical hostname.
Connecting a Domain to the Application
The Ingress Controller has already been assigned an external address, so you can now associate it with a proper domain name.
Technically, the application can be accessed via the load balancer’s IP address or hostname, but using your own domain is more convenient for a full production deployment. The domain will be needed not only by users, but also in the next step to issue a TLS certificate.
What the DNS Record Should Point To
The DNS record must point to the external endpoint through which the Ingress Controller receives traffic.
In managed Kubernetes, this is typically one of two options:
- An external IP address;
- The DNS name of the cloud load balancer.
If the provider assigns an external IP address, an A record is typically created.
For example, the app.example.com domain should point to the load balancer’s external address.
If a DNS name is provided instead of an IP address, a CNAME record can be used.
Do not point the domain to the IP address of an individual Pod or a ClusterIP Service. These addresses are intended for the internal Kubernetes network and do not provide a stable public entry point.
Why DNS Must Be Configured Before TLS
In the next step, cert-manager will obtain a TLS certificate for our domain.
To verify that the domain belongs to us, the certificate authority must be able to access it from outside the cluster.
For an HTTP-01 challenge, the request is sent to the same domain for which the certificate is being issued. The Ingress Controller and cert-manager must process this request correctly and return a specific response.
If DNS still points to the wrong destination, the validation request will never reach Kubernetes.
As a result, cert-manager may be configured correctly and the Ingress may exist, but the Certificate will remain pending or fail.
This is why the order matters:

First, make sure the domain consistently resolves to the cluster address. Only then should you proceed with the certificate.
Why DNS Changes Are Not Visible Immediately
After a DNS record is changed, the new address may not become visible to all users immediately.
DNS relies heavily on caching.
Each record has a TTL—the length of time a DNS resolver may cache the response it received.
For example, if the previous value was cached for an hour, some clients may continue using it until the TTL expires.
The following may also maintain their own caches:
- Operating system;
- Browser;
- Home router;
- ISP’s DNS server;
- Public DNS resolver.
Therefore, it is perfectly normal for a domain to resolve to the new address for one user while still resolving to the old address for another.
For verification, it is best to query DNS directly using multiple tools rather than relying solely on the browser.
Domain verification commands
First, let’s check the external address of the Ingress Controller.
| Task | Command | What it checks |
| View the controller’s Service | kubectl get svc -n ingress-nginx | External IP address or hostname |
| Check the A record | dig app.example.com | The IP address returned by DNS |
| Get a concise DNS response | dig +short app.example.com | The resulting address without unnecessary output |
| Check the domain with nslookup | nslookup app.example.com | The DNS response from another client |
| Check HTTP | curl -I http://app.example.com | Whether the request reaches the external entry point |
If DNS is configured correctly, dig or nslookup should return an address associated with the Ingress Controller or its Load Balancer.
At this stage, curl does not yet need to return our application. For now, we are only checking that the domain points to the correct cluster. We will create the routing rule in the next chapter.
The domain is now associated with the external Kubernetes entry point. The remaining step is to tell the Ingress Controller which Service should receive requests for this domain name.
Exposing the Application Through Ingress
DNS already directs users to the Ingress Controller, but the Controller does not yet know where to route requests for app.example.com.
For this purpose, an Ingress object is created.
It defines the relationship between the external domain and the application’s internal Service.
How Ingress Maps a Domain to a Service
Ingress uses rules.
A rule typically includes:
- Hostname;
- Path;
- Target Service;
- Service port.
For our application, the setup will look like this:

When the Ingress Controller receives a request for app.example.com, it finds the matching rule and forwards the traffic to the webapp Service.
The Service already knows which Pods match its selector.
This creates the following end-to-end flow:

This is why Ingress does not need to connect directly to Pods. It works through a stable Service.
What Is an IngressClass?
In theory, multiple Ingress controllers can run in a single Kubernetes cluster.
For example, one can handle public traffic, while another handles internal services.
An IngressClass tells Kubernetes which controller should handle a specific Ingress.
If ingress-nginx is installed, the corresponding class is typically named nginx.
An Ingress with this class will be handled by the NGINX Ingress Controller.
This is particularly important in managed Kubernetes, where the provider may already have its own controller installed or offer multiple options for exposing applications.
If the wrong IngressClass is specified, the resource may exist in Kubernetes, but the intended controller will simply not handle it.
How to Route Multiple Applications
One of the main advantages of Ingress is its ability to serve multiple applications through a single external address.
You can use different domains.
For example:
- app.example.com → Service webapp
- api.example.com → Service api
- admin.example.com → Service admin
You can also route requests based on the path.
For example:
- example.com/ → frontend
- example.com/api/ → api-service
This allows you to use a single Ingress Controller and a single external Load Balancer for multiple applications.
However, the more complex the rules, the more important it is to carefully monitor the host, path, and routing order.
For our example, we will use the most straightforward option: one domain and one Service.
Hands-on: Creating an Ingress
Create an ingress.yaml manifest:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webapp
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: webapp
port:
number: 80 Here, ingressClassName associates the rule with the NGINX Ingress Controller, host specifies the domain, and backend points to the webapp Service.
Now apply the manifest and verify the result.
| Task | Command | What to verify |
| Create the Ingress | kubectl apply -f ingress.yaml | The resource is accepted by Kubernetes |
| View the Ingress | kubectl get ingress | Host, class, and external address |
| View details | kubectl describe ingress webapp | Backend, rules, and events |
| Check the Service | kubectl get svc webapp | The Service exists and uses port 80 |
| Test HTTP | curl -I http://app.example.com | The request reaches the application through the Ingress |
If everything is configured correctly, a request to http://app.example.com should now reach the application.
The public HTTP request path is now fully operational: DNS directs the request to the Ingress Controller, the Ingress selects the appropriate Service, and the Service forwards the traffic to a Pod.
The final key step in exposing the application externally is to add TLS and switch the application to HTTPS.
Configuring TLS for HTTPS
How TLS Works with Ingress
When a user opens https://app.example.com, the TLS connection is terminated at the Ingress Controller.
In other words, the Controller presents the certificate to the browser and establishes a secure connection.
The request is then routed within the cluster to the Service and subsequently to the Pod.
A simplified flow looks like this:

The Ingress resource specifies which TLS Secret to use for a particular domain.
This Secret contains the certificate and private key.
It is important to understand that TLS protects the external segment of the connection up to the Ingress Controller. Within the cluster, traffic to the Service and Pod may use HTTP unless internal encryption is configured separately.
Why You Need cert-manager
You can create a certificate manually and upload it to a Kubernetes Secret yourself.
However, you would then need to monitor its validity period, renew the certificate, and replace the Secret before the certificate expires.
This is feasible for a single test application, but inconvenient in production.
cert-manager automates this process.
It can:
- Create a certificate request;
- Verify domain ownership;
- Obtain a certificate from a certificate authority;
- Store it in a Kubernetes Secret;
- Monitor its validity period;
- Renew it automatically.
In other words, instead of managing certificates manually, Kubernetes gets a dedicated controller that maintains their state, much like a Deployment maintains the state of Pods.
How Domain Validation Works with ACME
The ACME protocol is commonly used to automate certificate issuance.
One common validation method is the HTTP-01 challenge.
The process is straightforward.
The certificate authority must verify that we actually control the app.example.com domain.
To do this, it sends a request to a specific HTTP path on the domain and expects a valid response.
cert-manager temporarily creates the necessary Kubernetes resources to route this request to the cluster and return the expected content.
This is why we configured DNS before TLS.
If the domain does not point to the Ingress Controller, the validation request will not reach the cluster, and certificate issuance will not complete.
The flow is as follows:

Once validation succeeds, cert-manager obtains the certificate and stores it in a Secret.
What Are ClusterIssuer and Certificate?
cert-manager provides several Kubernetes custom resources.
One of the main resources is Issuer or ClusterIssuer.
It specifies where to obtain certificates and how to validate domains.
They differ in scope.
Issuer works only within a single namespace.
ClusterIssuer is available cluster-wide.
ClusterIssuer is generally more convenient for an infrastructure setup with multiple applications because it can be configured once and then used across different namespaces.
A Certificate is a request for a specific certificate.
It specifies:
- Domain names;
- The name of the Secret for the certificate;
- The Issuer or ClusterIssuer to use.
When cert-manager is integrated with Ingress, a separate Certificate is often created automatically based on the Ingress annotations and TLS configuration.
This means that we can configure HTTPS directly in the Ingress, and cert-manager will automatically create and manage the required certificate.
Hands-on: Installing cert-manager and issuing a certificate
First, install cert-manager using Helm.
Add the repository:
helm repo add jetstack https://charts.jetstack.io
helm repo update Now install cert-manager in a separate namespace:
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true After installation, create a ClusterIssuer.
For example:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: [email protected]
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx Save it as clusterissuer.yaml, for example.
Now add TLS to the Ingress:
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: webapp-tls
rules:
- host: app.example.com After you apply the Ingress, cert-manager will detect the certificate request, complete the ACME challenge, and create the webapp-tls Secret.
Installation and Verification Commands
| Task | Command | What to Verify |
| Add the cert-manager repository | helm repo add jetstack https://charts.jetstack.io | Helm recognizes the chart source |
| Update the repository index | helm repo update | The latest versions have been retrieved |
| Install cert-manager | helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true | The controller and CRDs are installed |
| Check the cert-manager Pods | kubectl get pods -n cert-manager | All components are in the Running state |
| Create a ClusterIssuer | kubectl apply -f clusterissuer.yaml | The certificate issuer is configured |
| Check the ClusterIssuer | kubectl get clusterissuer | The issuer is in the Ready state |
| Check the Certificate | kubectl get certificate | The certificate has been created |
| Check the TLS Secret | kubectl get secret webapp-tls | The certificate is stored in Kubernetes |
| Check HTTPS | curl -I https://app.example.com | The application is accessible over TLS |
If the Certificate remains not ready for a long time, also check the status of the ACME resources and the cert-manager events.
The external request path is now fully configured: the domain points to the Ingress Controller, the Ingress routes requests to the Service, and TLS is managed automatically by cert-manager.
Updating the Application with Helm
The application is already available over HTTPS, so we can now move on to something that happens regularly in production: releasing a new version.
This is where Helm proves particularly useful. Instead of manually editing multiple Kubernetes resources, we change the release parameters and apply the update in a single operation.
What Happens When a Helm Release Is Upgraded
A Helm release is an installed instance of a chart with a specific set of values.
During an upgrade, Helm regenerates the final Kubernetes manifests based on the chart and the current values, compares them with the existing state, and submits the changes to the Kubernetes API.
Kubernetes then determines which objects need to be updated.
For example, if only the container image version has changed, the Service and Ingress may remain unchanged, while the Deployment is updated with a new Pod configuration.
In other words, Helm defines the new desired state, while Kubernetes applies that state within the cluster.
This distinction is important: Helm does not directly “restart containers.” It updates Kubernetes resources, after which the cluster controllers perform the required work.
Why Image Tags Should Be Updated Explicitly
Updating an application typically involves changing the image tag.
For example, a release starts using version 1.1.0 instead of 1.0.0.
This approach is much more predictable than always using latest.
If all releases reference the same latest tag, it becomes harder to answer a simple question: exactly which image is currently running in the cluster?
In addition, Kubernetes does not always have to pull an image again when its name and tag remain unchanged, especially if the image pull policy does not require it.
Pinned versions provide a clear history: 1.0.0 → 1.1.0 → 1.2.0
This aligns well with the Helm revision history.
If the update to 1.2.0 is unsuccessful, it is much easier to determine which version to roll back to.
For even stricter reproducibility, you can use an image digest, but for a typical release process, pinned tags already offer a significant advantage over latest.
How Kubernetes Performs a Rolling Update
When a Deployment receives a new Pod template, Kubernetes does not have to stop all existing instances before starting new ones.
By default, a Deployment uses the RollingUpdate strategy.
New Pods are created gradually, while old ones are removed as the new Pods become ready.
In simplified terms, the process may look like this:

This allows the application to remain available during the update.
However, there is an important requirement: Kubernetes must be able to determine when a new Pod is actually ready to receive traffic.
Readiness probes are particularly useful for this purpose; we will discuss them separately in the production section.
Without a properly configured readiness check, a Pod may be considered operational before the application has actually finished starting up.
Update and Verification Commands
Suppose the new image is tagged 1.1.0.
You can update the release by passing the new value to Helm:
helm upgrade webapp ./webapp \
--set image.tag=1.1.0 If the values are stored in a separate file, you can first update values.yaml and then run a standard upgrade.
After the update, it is useful to check not only Helm but also the Deployment.
| Task | Command | What to verify |
| Update the release | helm upgrade webapp ./webapp --set image.tag=1.1.0 | The new chart configuration has been applied |
| View the history | helm history webapp | A new revision has been created |
| Check the Deployment rollout | kubectl rollout status deployment/webapp | The new Pods have successfully replaced the old ones |
| View the Pods | kubectl get pods | The new instances are in the Running state |
| Check the Deployment image | kubectl get deployment webapp -o jsonpath='{.spec.template.spec.containers[0].image}' | The required image version is in use |
| Check the Helm release | helm status webapp | The release is in the correct state |
Application updates are now performed as a managed operation: Helm creates a new revision, while Kubernetes gradually replaces the Pods.
However, any release can potentially fail. The next step is therefore to examine the reverse operation: rolling back to the previous state.
Rolling Back a Failed Release
Suppose version 1.1.0 was installed successfully and the Pods transitioned to the Running state, but after the release, the application was found to be working incorrectly.
Instead of manually restoring the previous YAML manifests, Helm lets you roll back to an earlier release revision.
Why Helm Stores Release History
Each successful change to a Helm release is assigned its own revision number.
For example:
- REVISION 1 → initial installation
- REVISION 2 → application version 1.1.0
- REVISION 3 → the next configuration
This allows Helm to track the Kubernetes manifests and parameters associated with each stage of the release history.
This is useful for more than just rollbacks.
The history helps you determine:
- How many times the release has been changed;
- When updates occurred;
- What the state was before the current one;
- Which revision you can roll back to.
A Helm release is therefore more than just a way to install YAML—it also provides a history of application changes within Kubernetes.
What Happens During a Rollback
A rollback takes the state of a selected previous revision and reapplies it as the current state of the release.
For example, if the current revision, revision 2, uses image version 1.1.0, while revision 1 used version 1.0.0, rolling back to revision 1 restores the Deployment to its previous configuration.
Kubernetes then performs a new rollout and gradually replaces the Pods.
It is important to understand that a rollback does not “turn back time.”
Helm creates a new entry in the release history based on the previous configuration.
In other words, the history is not erased after a rollback. Instead, a new revision is created to record the rollback.
This is useful for auditing because it shows not only that the state was rolled back, but also exactly when the rollback occurred.
Why Rolling Back an Application Does Not Always Roll Back External Dependencies
This is where one of the most important limitations comes into play.
Helm effectively manages the resources included in a release, but applications often depend on external systems.
For example:
- Databases;
- Queues;
- External APIs;
- Object storage;
- Schema migrations;
- User data.
Suppose version 1.1.0 performed a database migration at startup.
A subsequent Helm rollback restored the application container to version 1.0.0.
However, the database does not necessarily revert to its previous schema.
As a result, the older version of the application may be incompatible with the new data structure.
For this reason, production rollbacks must be planned in conjunction with migrations.
A sound release process requires either reversible migrations, a schema compatible with multiple application versions, or a separate database recovery plan.
Helm rollback addresses Kubernetes resources, but it does not replace a comprehensive rollback strategy for the entire application.
Rollback and Verification Commands
First, view the release history:
helm history webapp
Suppose we need to revert to revision 1.
Run the rollback:
helm rollback webapp 1
After that, check the Deployment rollout and the status of the Pods.
| Task | Command | What to Verify |
| View revisions | helm history webapp | Available release states |
| Revert to a revision | helm rollback webapp 1 | The previous configuration has been reapplied |
| Check the Helm release | helm status webapp | The rollback completed successfully |
| Wait for the Deployment to complete | kubectl rollout status deployment/webapp | The old Pods have been replaced by Pods running the required version |
| Check the Pods | kubectl get pods | All instances are running |
| Check the current image | kubectl get deployment webapp -o jsonpath='{.spec.template.spec.containers[0].image}' | The Deployment is using the required tag again |
| View the history after the rollback | helm history webapp | The rollback appears as a new revision |
After the rollback, check not only the Kubernetes state but also the application externally: open its primary URL, run a health check, and ensure that it works correctly with the database and other dependencies.
We now have a complete release cycle: install the application, update it, and restore a working version if necessary. The next step is to change the number of Pods and see how Kubernetes scales the application without changing its network endpoint.
Scaling the Application
What the Number of Replicas Means
replicas specifies the number of Pods that a Deployment must maintain simultaneously.
If set to 1, Kubernetes attempts to keep one instance of the application running.
If set to 3, the cluster will maintain three identical Pods with the same container configuration.
In simple terms:

If one of them disappears, Kubernetes creates a replacement to bring the actual state back in line with the desired state.
This is useful for two reasons.
First, multiple replicas allow the load to be distributed across application instances.
Second, the failure of a single Pod does not necessarily make the entire service unavailable: the remaining instances continue to run.
However, it is important to understand that simply increasing the number of replicas does not address every aspect of high availability. If all Pods depend on a single external database or a single bottleneck, scaling only the application will not eliminate that dependency.
How a Service Distributes Requests Among Pods
A Service is not tied to a specific Pod.
It selects all instances that match its selector.
For example, if a Service selects Pods with the label app: webapp and a Deployment creates three such instances, they all become backends for the same Service.
The diagram looks like this:

Meanwhile, the client continues to use a single domain and a single Service.
It does not need to know:
- How many Pods are currently running;
- What their IP addresses are;
- Which nodes they are running on;
- Which instance was recreated.
Kubernetes automatically maintains an up-to-date set of matching endpoints.
This is why scaling an application typically does not require changes to Ingress or DNS.
How Manual Scaling Differs from HPA
The number of replicas can be changed manually.
For example, if an administrator knows in advance that the load will increase, they can increase the number of Pods from two to five.
This approach is simple and straightforward, but the scaling decision is made by a person.
HPA—Horizontal Pod Autoscaler—automates this process.
It can adjust the number of replicas based on metrics such as CPU or memory utilization, provided that the required metrics infrastructure is configured.
The logic then works as follows:

When the load decreases, the number of Pods can be scaled back down.
Manual scaling is sufficient for a basic article. HPA requires a separate discussion of Metrics Server, resource requests, thresholds, and autoscaler behavior.
There is another important consideration specific to Helm.
If the replicaCount value is stored in values.yaml, it is best to change it using Helm. Otherwise, you can manually scale the Deployment with kubectl, but the next helm upgrade will cause Helm to reapply the value from the chart.
In other words, a temporary manual adjustment can diverge from the declarative Helm configuration.
Scaling and Verification Commands
If the application is managed with Helm, the preferred approach is to change replicaCount.
For example:
helm upgrade webapp ./webapp \
--set replicaCount=3 The Deployment should then gradually create additional Pods.
For a temporary manual change, you can use kubectl:
kubectl scale deployment webapp --replicas=3
However, before the next Helm upgrade, keep in mind the value configured in the chart.
| Task | Command | What to Verify |
| Change the replica count through Helm | helm upgrade webapp ./webapp --set replicaCount=3 | The Helm release stores the new value |
| Scale the Deployment manually | kubectl scale deployment webapp --replicas=3 | Kubernetes changes the number of Pods directly |
| Check the Deployment | kubectl get deployment webapp | The desired and available replica counts match |
| View the Pods | kubectl get pods -l app=webapp | All application instances have been created |
| Monitor changes | kubectl get pods -l app=webapp -w | Pods appear and transition to Running |
The application can now not only be updated and rolled back, but also run in multiple instances. The next step is to learn how to quickly determine why a specific Pod fails to start or why an externally accessible application suddenly stops responding.
Troubleshooting Pod Issues
Kubernetes automates a vast number of operations, but that does not mean an application will always become operational on the first attempt.
A Pod may fail to pull an image, a container may exit immediately after startup, an application may listen on the wrong port, or a Service may fail to find any matching backends.
For this reason, basic Kubernetes troubleshooting does not rely on a single “universal command,” but instead involves systematically checking several layers.
What Pod States Mean
A Pod’s state provides an initial indication of what is happening.
Running means that the Pod has started, but this alone does not guarantee that the application inside it is fully ready to serve requests.
Pending means that the Pod has been created but has not yet been able to start successfully. Possible causes include insufficient resources, an inability to schedule the Pod on a node, or waiting for a volume.
ImagePullBackOff usually indicates a problem pulling the container image.
For example:
- Incorrect image name;
- Incorrect tag;
- The private registry is unavailable;
- The imagePullSecret is not working.
CrashLoopBackOff indicates a different situation: the container starts, exits with an error, Kubernetes attempts to restart it, and the cycle repeats.
In other words, the state alone can indicate at which stage to look for the problem.
Why a Pod May Fail to Start
There are many possible causes, but they can be conveniently grouped into categories.
The first is the container image.
If Kubernetes cannot pull the image, check the registry, tag, and credentials.
The second is the application itself.
The container image may be pulled successfully, but the container may exit immediately because of a missing environment variable, incorrect configuration, or a startup error.
The third is resources.
If a Pod requests more CPU or memory than the cluster can provide, the scheduler may leave it in the Pending state.
The fourth is dependencies.
The application may start but fail to become ready because a database, API, or other service is unavailable.
Finally, the problem may not be with the Pod at all. The Pod itself may be working correctly, but the Service may use the wrong selector, or the Ingress may route traffic to the wrong port.
Troubleshooting should therefore proceed gradually outward, starting with the container itself.
How logs Differs from describe
logs and describe answer different questions.
Logs show the output generated by the application running inside the container.
For example, they may contain:
- A stack trace;
- A database connection error;
- An incorrect configuration;
- A message indicating that a port is already in use;
- A process crash.
describe shows the resource from Kubernetes’ perspective.
The Events section is particularly important, as it may contain messages about:
- An image pull failure;
- A volume mount error;
- A scheduling issue;
- A failed readiness probe;
- Insufficient resources.
Therefore, if the container does not get a chance to start properly, describe is often more useful than logs.
If the Pod starts but the application crashes inside the container, check logs first.
Where to troubleshoot when an application is unavailable
If an external domain is not responding, do not immediately assume that the Ingress is the problem.
It is best to work up the stack from the bottom.
First, check the Pods.
If they are not running properly, there is no point in troubleshooting further yet.
Next, check the Service and make sure it is actually selecting the correct Pods.
Then check the Ingress to ensure that the rules specify the correct host, Service, and port.
Only then should you proceed to the external layer:

For example, if the application works when accessed through the Service from within the cluster but is unavailable via the domain, the problem is further up the stack—in the Ingress, DNS, or TLS.
This approach saves time: instead of checking everything at once in a haphazard manner, we gradually narrow down the search area.
Basic Troubleshooting Commands
| What to Check | Command | What to Look For |
| Pod status | kubectl get pods | Running, Pending, CrashLoopBackOff, ImagePullBackOff |
| Pod details | kubectl describe pod <pod-name> | Events, scheduling, image pulls, probes |
| Container logs | kubectl logs <pod-name> | Errors from the application itself |
| Logs from the previous run | kubectl logs <pod-name> --previous | Why the restarted container crashed |
| Deployment | kubectl get deployment webapp | Available and desired replicas |
| Service | kubectl get svc webapp | Service port and type |
| Endpoints | kubectl get endpoints webapp | Whether the Service has a backing Pod |
| Ingress | kubectl get ingress | Host, class, and external address |
| Ingress details | kubectl describe ingress webapp | Backend and controller events |
Helm release | helm status webapp | Status of the installed release |
| Release history | helm history webapp | Recent upgrades and rollbacks |
If a Service has no endpoints, check the Pod labels and the Service selector. If an endpoint exists but the domain does not work, check the Ingress and DNS next.
This turns Kubernetes troubleshooting from trying to determine “why everything is broken” into a clear sequence of checks, from the Pod through to external HTTPS access.
Verifying the Entire Deployment
We have already verified each component individually, so now it is useful to test the entire flow, from connecting to the cluster to external HTTPS access.
This final verification is particularly useful before going to production: it confirms that Kubernetes, Helm, Ingress, and TLS work together as an integrated system rather than as isolated components.
First, verify the cluster itself and make sure that kubectl is accessing the correct environment:
| What to Check | Command | Expected Result |
| Active context | kubectl config current-context | The correct cluster is selected |
| Worker nodes | kubectl get nodes | The nodes are in the Ready state |
| Namespaces | kubectl get namespaces | The required namespace exists |
If the context is incorrect, do not proceed. All subsequent commands must be run in the environment where the application is deployed.
Now verify the application itself:
| What to Check | Command | Expected Result |
| Deployment | kubectl get deployment webapp | The desired and available replica counts match |
| Pod | kubectl get pods -l app=webapp | All Pods are in the Running state |
| Service | kubectl get svc webapp | The Service exists and uses the correct port |
| Endpoints | kubectl get endpoints webapp | The Service can reach the running Pods |
Helm release | helm status webapp | The release is operational |
| Release history | helm history webapp | Installations, upgrades, and rollbacks are listed |
If the number of ready Pods is lower than expected, stop here and troubleshoot the Deployment first. There is no point in checking DNS or TLS while the application is not running.
After verifying the internal components, proceed to external access:
| What to Check | Command | Expected Result |
| Ingress | kubectl get ingress | The correct host, class, and external address are specified |
| Ingress details | kubectl describe ingress webapp | The backend points to the correct Service |
| TLS certificate | kubectl get certificate | The Certificate is in the Ready state |
| TLS Secret | kubectl get secret webapp-tls | The Secret exists |
| DNS | dig +short app.example.com | The domain points to the cluster’s external address |
| HTTP/HTTPS | curl -I https://app.example.com | The application responds over HTTPS |
Ultimately, the entire setup should look like this:

Helm and cert-manager operate separately on top of this setup: Helm manages release versions, while cert-manager maintains the TLS certificate.
If all checks pass, the deployment is fully integrated: the cluster is accessible, the required number of Pods are running, the Service can reach them, the Ingress accepts external traffic, the domain resolves correctly, TLS is active, and Helm maintains the release history and supports upgrade management.
The final layer is production configuration: restricting permissions, storing secrets, configuring probes, requests, and limits, pinning image versions, and ensuring safe rollback.
How to Use This Approach Safely in Production
Do Not Store Registry Credentials or Kubernetes Secrets in Git
A private container registry requires credentials, while the application itself often needs database passwords, API tokens, and other secrets.
Such values should not be stored directly in a Helm chart or an unprotected values.yaml file.
A particularly risky scenario is when a password or registry token is committed to Git along with the source code. Even if it is later removed from the current version of the file, the secret may remain in the repository history.
Kubernetes Secrets provide a convenient mechanism for passing sensitive values to Pods, but they are not a substitute for a full-fledged external secrets manager.
Access to Secrets should therefore be restricted using RBAC. For more complex production infrastructure, a dedicated secrets manager can be used, with values passed to Kubernetes at deployment time.
If a credential is exposed in a public repository, log, or other untrusted location, it is safer to consider it compromised and replace it.
Limit service account and kubeconfig permissions
Kubernetes provides fine-grained access control through RBAC.
The same principle of least privilege that we applied to the private registry also applies here: each component should have only the permissions it needs to operate.
For example, an application typically does not need permission to create new Deployments or read Secrets belonging to other services.
The same applies to CI/CD. If a pipeline only needs to update a single Helm release in a specific namespace, it does not need administrative access to the entire cluster.
The kubeconfig file requires particular attention.
Depending on the credentials it contains, such a file may grant extensive permissions, so it should not be stored in plaintext in Git, sent through regular chats, or used as a single administrative kubeconfig for all automated processes.
The narrower the scope of a given set of credentials, the less damage their leakage can cause.
Use readiness and liveness probes
A Running status does not necessarily mean that the application is ready to accept user requests.
The container may already be running while the application inside it is still, for example, initializing or establishing a connection to its dependencies.
Kubernetes uses a readiness probe for this purpose.
It answers the question: Can traffic be routed to this Pod yet?
Until the readiness check succeeds, the Service should not treat the Pod as a ready application instance.
A liveness probe serves a different purpose. It helps determine when the process still exists but the application itself is no longer functioning properly and should be restarted.
This is especially important during a Rolling Update.
Without a properly configured readiness probe, a new Pod may begin receiving user traffic too early while the old Pod is already being terminated.
Probes are therefore not merely an additional diagnostic tool, but an essential part of safely updating an application.
Set requests and limits for a Pod
If no resource requirements are specified, Kubernetes has more difficulty scheduling Pods across nodes.
requests specify the amount of resources a container needs for normal operation and that the scheduler takes into account when scheduling it.
limits define the maximum amount of a resource that a container can use.
This is especially important in a shared cluster where multiple applications run simultaneously.
Without limits, a single container may consume excessive CPU or memory and affect other workloads.
However, overly restrictive limits can also cause problems. For example, the system may terminate a container that exceeds its memory limit.
Therefore, it is best to choose values based on the application’s actual resource usage and gradually adjust them after observing the workload.
Use pinned versions of images and Helm charts
Production deployments must be reproducible.
If one version is deployed today and different code suddenly appears under the same name tomorrow, investigating incidents and performing rollbacks becomes significantly more difficult.
It is therefore better to use pinned image tags or, for even stricter pinning, image digests.
The same applies to Helm charts.
If the chart version changes, it is important to know exactly which templates were used for a specific release.
As a result, any release can be described by a combination of:
- Application version;
- Container image version;
- Chart version;
- Set of configuration values.
The more precisely this combination is known, the easier it is to reproduce a previous state and understand the differences between two deployments.
Plan rollbacks alongside database migrations
Helm rollback is excellent at restoring Kubernetes resources to a previous state, but external data follows its own rules.
This is especially true for databases.
If the new application version has already modified the schema, simply reverting the container to the previous image may not be enough.
For example, the old version may not understand new tables, data types, or required fields.
The migration strategy should therefore be developed alongside the release strategy.
A good approach is to make schema changes in a way that allows the old and new application versions to work with the schema concurrently for a limited time whenever possible.
This is particularly important during a Rolling Update, when Pods running two different versions may briefly coexist in the cluster.
Irreversible changes require a separate recovery plan rather than relying solely on helm rollback.
What to check before going into production
Before releasing the application, review this short checklist:
| What to check | Why it matters |
| The kubeconfig file is secured and has not been exposed | It may provide access to the Kubernetes API |
| Registry credentials are not stored in Git | A leak could allow a private image to be downloaded |
| Secrets are accessible only to the components that need them | This reduces the impact of a compromise |
| The service account has only the minimum required permissions | This limits what the application and automation can do |
| Image versions are pinned | This makes releases reproducible |
| The Helm chart is versioned | This makes it possible to identify the configuration of each release |
| A readiness probe is configured | Traffic is routed only to ready Pods |
| A liveness probe is configured | An unresponsive container can be restarted automatically |
| Resource requests and limits are defined | This makes resource allocation across the cluster more predictable |
| Multiple replicas are running if high availability is required | The failure of a single Pod does not stop the application |
| The TLS Certificate is in the Ready state | HTTPS is being served correctly |
| The rollback strategy has been tested | A failed release can be rolled back quickly |
| Migrations are compatible with the upgrade strategy | The database does not prevent the application from being rolled back |
| Kubernetes logs and events are accessible | Issues can be diagnosed after deployment |
| External HTTPS access has been tested | The entire chain from DNS to the Pod works end to end |
At this point, the production deployment can be regarded not merely as a collection of functioning Kubernetes objects, but as a managed system designed in advance to handle updates, failures, and recovery.
Conclusion

Managed Kubernetes allows users to offload much of the cluster management to the cloud provider, but they remain responsible for the application deployment workflow. Publishing a service requires linking the container image, Deployment, Service, Ingress, domain, and TLS into a single coherent process.
In this article, we covered the entire process: connecting via kubectl, configuring access to a private Container Registry, packaging resources into a Helm chart, installing an Ingress Controller and cert-manager, and then examining updates, rollback, scaling, and Pod diagnostics.
Helm makes releases manageable, while Kubernetes maintains the required number of replicas and rolls out changes gradually. However, production reliability depends on the details: probes, resource limits, least-privilege access, secure storage of secrets, and a well-designed approach to migrations.
When these layers are configured consistently, managed Kubernetes transforms from a collection of individual objects into a predictable platform where applications can be safely published, updated, scaled, and recovered without manually managing each container.
FAQ
Can I deploy an application to Kubernetes without Helm?
Yes. Kubernetes can work directly with standard YAML manifests, which may be sufficient for a small application.
Helm becomes particularly useful when you have multiple environments, repeated installations, frequent updates, several configuration parameters, and the need for rollbacks.
Why is the Pod in the Running state, but the application is still inaccessible?
The Running state only means that the Pod is running. The issue may be further up the request path: the Service is not selecting the Pod, the Ingress points to the wrong port, DNS has not yet propagated, or the TLS certificate is not ready.
Troubleshooting should therefore proceed sequentially: Pod → Service → Ingress → DNS → TLS.
Why can’t a private image be pulled from the Container Registry?
The issue is most often related to the imagePullSecret, an incorrect image name, or an incorrect tag.
The Secret must also be in the same namespace as the Pod. If the credentials were created in a different namespace, Kubernetes cannot use them directly.
Is it necessary to install an Ingress Controller?
If you want to use an Ingress object, yes—you need a component to process it.
The Ingress itself contains only routing rules. The Ingress Controller receives the actual external traffic.
How does Helm rollback differ from Kubernetes rollout undo?
kubectl rollout undo uses the revision history of a specific Deployment.
Helm rollback restores the state of an entire Helm release, so it may affect multiple related resources at once, including Deployments, Services, Ingresses, and other objects defined in the chart.
However, neither mechanism automatically rolls back an external database or any migrations that have already been applied.
Do all production applications need multiple replicas?
Not necessarily, but for services where availability is important, multiple replicas are generally preferable to a single Pod.
However, the application itself must support horizontal scaling. For example, user sessions and critical state should not be stored solely in the memory of a single Pod.
Which is better for production: an image tag or a digest?
A fixed tag such as 1.4.2 is much better than latest because it explicitly identifies the application version.
A digest provides even stricter pinning: it points to specific image content and does not change even if the same tag is republished.
What happens if cert-manager cannot renew a certificate?
The existing certificate will continue to work until it expires, but a new one will not be issued in time.
Therefore, in production, you should monitor the Certificate status, cert-manager events, and ACME errors rather than relying solely on automation.
