Deploy a Next.js Container to EKS
Deploy a Next.js container to EKS, verify cluster health, and troubleshoot real deployment failures.
Introduction
30 Second Summary
Getting a container running on your laptop is one thing. Getting it running in the cloud where it can handle real traffic, scale automatically, and stay online is a completely different challenge.
In this project, you will deploy your containerized Next.js app to Amazon EKS (AWS's managed Kubernetes service), verify cluster health at the infrastructure level, diagnose real deployment failures, and expose your app to the internet through a public LoadBalancer URL.
What You'll Build
A production Kubernetes deployment on Amazon EKS that pulls your containerized Next.js app from ECR and serves it to the internet via an AWS LoadBalancer.
By the end of this project, you'll have:
- A running EKS cluster with managed nodes pulling your production container image directly from ECR.
- A verified cluster health check confirming the VPC CNI can assign pod IP addresses and your nodes have capacity for workloads.
- A diagnostic toolkit for troubleshooting common EKS deployment failures including IP exhaustion, image pull errors, and scheduling problems.
- A Kubernetes Deployment and Service exposing your Next.js app via a public LoadBalancer URL.
- Secret Mission: Harden your deployment with liveness/readiness probes, resource limits, and 3-replica scaling.
Are there any prerequisites?
Yes. You need to have completed Project #1 (Containerize a Next.js App for Production with Docker and Amazon ECR). You should have a Docker image already pushed to an ECR repository.
You also need the AWS CLI configured on your machine and Docker installed from that project.
Set Up Your Tools
Before you can deploy your containerized Next.js app to a Kubernetes cluster on AWS, you need two CLI tools that bridge the gap between your terminal and Amazon EKS.
In this step, you will install and verify eksctl (the official CLI for creating EKS clusters) and kubectl (the Kubernetes command-line tool). You will also confirm that your ECR image from Project #1 is accessible and set up environment variables for the rest of the project.
In this step, get ready to:
- Verify and install eksctl and kubectl.
- Confirm your ECR image from Project #1 exists and is pullable.
- Export environment variables for consistent naming across all steps.
Verify eksctl and kubectl are installed
Amazon EKS requires two CLI tools. eksctl creates and manages EKS clusters through CloudFormation stacks. kubectl communicates with the Kubernetes API to deploy and inspect workloads.
- Check whether eksctl is installed by running this command:
eksctl version
You need version 0.215.0 or later. Check your output and pick the matching tab below.
✔️ I see version 0.215.0 or higher
Your eksctl version meets the minimum requirement. Move on to the kubectl check below.
ⓧ I see an older version
Your installed version is outdated. EKS cluster creation requires version 0.215.0 or later.
- Upgrade eksctl by running these commands:
brew tap aws/tap
brew upgrade aws/tap/eksctl
- Verify the upgrade by running:
eksctl version
You should now see 0.215.0 or higher.
ⓧ Command not found
You do not have eksctl installed yet.
- Install eksctl from the AWS Homebrew tap by running these commands:
brew tap aws/tap
brew install aws/tap/eksctl
- Confirm the installation by running:
eksctl version
You should see 0.215.0 or higher printed to your terminal.
- Now check whether kubectl is installed by running this command:
kubectl version --client
You need a version within one minor release of the EKS cluster you will create. A version of 1.35 or higher works. Check your output and pick the matching tab below.
✔️ I see version 1.35 or higher
Your kubectl version is compatible with EKS. Move on to verifying your AWS identity below.
ⓧ I see an older version
Your kubectl version is too old. It must be within one minor version of the EKS cluster control plane.
- Upgrade kubectl by running:
brew upgrade kubernetes-cli
- Verify the upgrade by running:
kubectl version --client
You should now see version 1.35 or higher.
ⓧ Command not found
You do not have kubectl installed yet.
- Install kubectl via Homebrew by running:
brew install kubernetes-cli
- Confirm the installation by running:
kubectl version --client
You should see version 1.35 or higher printed to your terminal.
- Verify your AWS identity by running this command:
aws sts get-caller-identity
You should see a JSON response containing your Account, UserId, and Arn. This confirms the AWS CLI can authenticate to your account.
Seeing an error from aws sts?
If you see an "Unable to locate credentials" or "ExpiredToken" error, your AWS CLI session is not active. Re-run aws configure with your access key and secret key from Project #1.
Help me fix my AWS CLI credentials
Verify your ECR image from Project #1
Your EKS cluster will pull the Docker image you pushed to Amazon ECR in Project #1. Before creating the cluster, confirm the image is still there.
- List the images in your nextjs-production repository by running this command:
aws ecr describe-images --repository-name nextjs-production --query 'imageDetails[*].{Tag:imageTags[0],Pushed:imagePushedAt}' --output table
You should see a table with at least one row showing the latest tag and a recent push timestamp.
What does this command do?
The --query flag uses JMESPath to filter the response down to just the image tag and push date. The --output table flag formats it as a readable table instead of raw JSON.
🙋♀️ No images showing up?
If the table is empty or you get a RepositoryNotFoundException, your image from Project #1 is missing. Go back to Project #1 and complete the ECR push step before continuing.
Help me find my missing ECR image
- Note your full image URI. It follows this format:
# Your image URI looks like this:
# <account-id>.dkr.ecr.<region>.amazonaws.com/nextjs-production:latest
You will use this URI in the next substep when setting environment variables.
Set environment variables for the project
Every command in this project references the same cluster name, region, and image URI. Exporting these as environment variables keeps your commands consistent and avoids typos.
- Export your project environment variables by running these commands:
export CLUSTER_NAME=nextjs-eks-cluster
export REGION=[[REGION="your AWS region e.g. us-east-1"]]
export AWS_ACCOUNT_ID=[[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]]
export ECR_IMAGE=$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/nextjs-production:latest
What do these variables do?
CLUSTER_NAME sets the name for your EKS cluster. Every resource in this project uses the nextjs-eks- prefix so you can identify them easily.
REGION and AWS_ACCOUNT_ID come from your AWS configuration. ECR_IMAGE builds the full image URI that your Kubernetes Deployment manifest will reference later.
💡 Find your account ID and region
Your 12-digit account ID was printed in the aws sts get-caller-identity output above (the Account field). Your region is visible in the AWS Console header or by running aws configure get region.
- Verify your variables are set correctly by running:
echo $ECR_IMAGE
You should see your full image URI printed, like 123456789012.dkr.ecr.us-east-1.amazonaws.com/nextjs-production:latest.
Echo returns a blank line?
If echo $ECR_IMAGE prints nothing, one of the earlier variables was not exported. Re-run all four export commands in the same terminal session. Variables do not persist across terminal windows.
Help me troubleshoot my environment variables
Your tools are locked and loaded. Next up, you will write a cluster configuration file and provision a full EKS cluster with managed nodes.
Create the EKS Cluster
Your tools are verified, your ECR image is confirmed, and your environment variables are set. Now it is time to provision the actual EKS cluster that will run your containerized Next.js app.
You will write a declarative config file that describes your entire cluster infrastructure. eksctl reads this file and uses CloudFormation to create all the networking, compute, and IAM resources automatically.
In this step, get ready to:
- Write an eksctl ClusterConfig file defining your cluster and managed node group.
- Create the EKS cluster and wait for provisioning to complete.
- Verify that your nodes are Ready and system pods are Running.
Write the eksctl ClusterConfig file
Instead of passing dozens of CLI flags, eksctl supports a declarative YAML config file. This is the infrastructure-as-code approach you already used with Docker in Project #1.
- Create a file called cluster.yaml in your project folder and paste the following configuration:
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: nextjs-eks-cluster
region: [[REGION="your AWS region (e.g. us-east-1)"]]
managedNodeGroups:
- name: nextjs-eks-nodes
instanceType: t3.medium
desiredCapacity: 2
minSize: 1
maxSize: 3
ssh:
allow: false
What does this configuration do?
This file tells eksctl exactly what infrastructure to create:
- metadata names your cluster nextjs-eks-cluster and sets the AWS region.
- managedNodeGroups defines a group of EC2 instances (t3.medium with 2 vCPUs and 4 GiB RAM each) that run your pods.
- desiredCapacity: 2 starts with 2 nodes. The minSize and maxSize allow the group to scale between 1 and 3 nodes.
- ssh: allow: false disables SSH access to nodes. You manage everything through kubectl instead.
💡 Why a config file instead of CLI flags?
A config file is declarative and version-controllable. You can commit it to Git, review changes in pull requests, and reproduce the exact same cluster later.
This is the same infrastructure-as-code pattern you used with your Dockerfile in Project #1.
- Save cluster.yaml.
- Confirm the file exists by running:
cat cluster.yaml
You should see the full YAML contents printed in your terminal matching what you just saved.
File is empty or not found?
- Make sure you saved the file in the same directory where your terminal is running.
- Check that the file is named exactly cluster.yaml with no extra extensions like .txt.
Help me troubleshoot my cluster.yaml file
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: nextjs-eks-cluster
region: [[REGION="your AWS region (e.g. us-east-1)"]]
managedNodeGroups:
- name: nextjs-eks-nodes
instanceType: t3.medium
desiredCapacity: 2
minSize: 1
maxSize: 3
ssh:
allow: false
Create the cluster
This is the big moment. The next command provisions your entire EKS cluster. It typically takes 15-20 minutes because eksctl creates two CloudFormation stacks behind the scenes.
- Start the cluster creation by running this command:
eksctl create cluster -f cluster.yaml
What is eksctl creating right now?
eksctl is creating two CloudFormation stacks:
- eksctl-nextjs-eks-cluster-cluster creates the VPC (with public and private subnets), a NAT Gateway, an Internet Gateway, and the EKS control plane.
- eksctl-nextjs-eks-cluster-nodegroup-nextjs-eks-nodes creates your managed node group with 2 t3.medium EC2 instances in private subnets.
The managed node group's IAM role automatically includes the AmazonEC2ContainerRegistryReadOnly policy. This is why your nodes can pull images from ECR without any extra configuration.
💰 Cost warning
Your EKS control plane costs $0.10/hr. The two t3.medium nodes cost $0.0416/hr each ($0.0832/hr total). A NAT Gateway costs approximately $0.045/hr.
Complete the project and tear down within 2-3 hours to keep total costs under $3. You will tear down everything in Step 7.
The terminal shows progress as each resource is created. When you see a message confirming the cluster is ready and kubeconfig has been updated, the process is complete.
What happens to kubectl?
eksctl automatically updates your ~/.kube/config file with connection details for the new cluster. You do not need to run any extra commands to connect kubectl to your cluster.
🙋♀️ Cluster creation failed or timed out?
- Check that your AWS credentials have sufficient permissions (EKS, EC2, IAM, CloudFormation, VPC).
- Verify your region is correct in cluster.yaml and matches your $REGION environment variable.
- If you see a service quota error, your account may have hit its VPC or EIP limit. Check the CloudFormation console for the specific failure reason.
Help me debug my eksctl cluster creation failure
Verify the cluster is ready
Once eksctl finishes, confirm that your nodes and system pods are healthy.
- Check that your nodes are in Ready status by running:
kubectl get nodes -o wide
You should see 2 nodes listed with status Ready.
- Confirm the cluster details by running:
eksctl get cluster --name nextjs-eks-cluster --region $REGION
- Verify that system pods are running by checking all namespaces:
kubectl get pods -A
You should see system pods for coredns, aws-node, and kube-proxy all in Running status.
What are these system pods?
- coredns handles DNS resolution inside the cluster so pods can find each other by name.
- aws-node is the VPC CNI plugin that assigns IP addresses from your VPC to each pod.
- kube-proxy manages networking rules so traffic can reach the correct pods.
🙋♀️ Nodes not showing Ready?
- If kubectl get nodes returns an error about connecting to the server, your kubeconfig may not have updated. Run this to fix it:
- aws eks update-kubeconfig --name nextjs-eks-cluster --region $REGION
- If nodes show NotReady, wait 1-2 minutes. The nodes need time to register with the control plane after initial boot.
Help me troubleshoot my EKS cluster node status
Your EKS cluster is live with two healthy nodes ready to accept workloads. Next up, you'll verify that the cluster's networking layer is fully operational before deploying any application pods.
Verify Cluster Health
Your cluster is provisioned and your nodes report Ready status. Before deploying any workloads, production Kubernetes engineers verify that the cluster's networking layer can actually assign IP addresses to pods.
On EKS, every pod gets a real VPC IP address. The AWS VPC CNI plugin (running as the aws-node DaemonSet) is responsible for attaching Elastic Network Interfaces to your nodes and allocating IP addresses from your VPC subnets. If this plugin is unhealthy or your nodes have exhausted their IP capacity, new pods will get stuck in ContainerCreating forever.
In this step, get ready to:
- Verify the AWS VPC CNI DaemonSet is healthy on all nodes.
- Understand how EKS assigns pod IP addresses using ENIs.
- Check pod capacity and available IP addresses on your worker nodes.
Verify the VPC CNI DaemonSet
The aws-node DaemonSet runs one pod on every node in your cluster. It is the component that assigns VPC IP addresses to your pods. If aws-node is not running, no pod can receive an IP address.
- Check that all DaemonSets in the kube-system namespace are healthy by running this command:
kubectl get ds -n kube-system
You should see aws-node and kube-proxy listed. The DESIRED, CURRENT, and READY columns should all show 2 (matching your 2 worker nodes).
What is a DaemonSet?
A DaemonSet guarantees that one copy of a pod runs on every node in the cluster. When you add a new node, the DaemonSet automatically schedules a pod on it.
The aws-node DaemonSet runs the AWS VPC CNI plugin. It attaches Elastic Network Interfaces (ENIs) to your nodes and assigns secondary IP addresses from your VPC subnets to pods. This is why EKS pods get real, routable VPC IP addresses instead of virtual overlay addresses.
- Confirm the aws-node pods are running on each node by running:
kubectl get pods -n kube-system -l k8s-app=aws-node -o wide
You should see 2 pods in Running status, each scheduled on a different node (check the NODE column).
aws-node pods not running?
- If you see CrashLoopBackOff or 0/2 READY, the CNI plugin failed to start. Check its logs with kubectl logs -n kube-system -l k8s-app=aws-node --tail=30.
- Verify that your node IAM role has the AmazonEKS_CNI_Policy attached. Without it, the CNI cannot make EC2 API calls to attach ENIs.
- If DESIRED shows 0, your nodes may not have registered with the control plane yet. Wait 1-2 minutes and re-run the command.
Help me troubleshoot why my aws-node DaemonSet is not healthy
Check pod capacity on your nodes
Each EC2 instance type has a hard limit on how many pods it can run. This limit comes from how many IP addresses the VPC CNI can assign, which depends on the number of ENIs and IP addresses per ENI that the instance type supports.
- Inspect the allocatable pod capacity on your nodes by running this command:
kubectl describe nodes | grep -A 5 "Allocatable" | grep pods
You should see pods: 17 for each node. This is the maximum number of pods a t3.medium instance can run.
How does EKS calculate the 17-pod limit?
The AWS VPC CNI uses this formula: (number of ENIs) x (IPv4 addresses per ENI - 1) + 2. A t3.medium supports 3 ENIs with 6 IP addresses each.
The calculation: 3 x (6 - 1) + 2 = 17. The -1 accounts for the primary IP on each ENI (used by the interface itself). The +2 accounts for host-networking pods like aws-node and kube-proxy that do not consume a pod IP address.
- Check how many pods are already running on each node by running:
kubectl get pods -A -o wide --field-selector=status.phase=Running | awk '{print $8}' | sort | uniq -c
You should see each node running 3-4 system pods (aws-node, kube-proxy, and coredns). That leaves approximately 13-14 pod slots available per node for your application workloads.
Why does this matter?
If your nodes run out of allocatable IP addresses, new pods will be stuck in ContainerCreating with the error failed to assign an IP address to container. Understanding your cluster's IP capacity before deploying prevents this.
For this project with 2 application replicas, you have more than enough capacity. In production with dozens of pods, this check becomes critical.
Your cluster networking is verified and healthy. The VPC CNI is assigning IP addresses correctly, and you have confirmed there is sufficient pod capacity on your nodes. Next up, you'll deploy your containerized Next.js app onto these nodes.
Deploy the Application
Your cluster is verified healthy. The VPC CNI is assigning IP addresses, your nodes have pod capacity available, and all system pods are running. Now it's time to deploy your Next.js container from ECR onto those nodes.
To do this, you'll write two Kubernetes manifest files. A Deployment manifest tells Kubernetes how many copies of your container to run and where to pull the image from. A Service manifest tells Kubernetes how to expose those containers to the internet.
In this step, get ready to:
- Write and apply a Kubernetes Deployment manifest that pulls your ECR image.
- Write and apply a Kubernetes Service manifest that provisions a LoadBalancer.
- Verify both pods reach Running status with no image pull errors.
Write the Deployment manifest
The Deployment tells Kubernetes to run 2 replicas of your container. It references your ECR image directly, and because eksctl attached the AmazonEC2ContainerRegistryReadOnly policy to your node IAM role, no imagePullSecrets are needed.
- Create a file called deployment.yaml in your project folder by pasting the following:
# Kubernetes Deployment manifest for the Next.js app
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-eks-app
namespace: default
spec:
# Run 2 replicas for availability across both nodes
replicas: 2
selector:
matchLabels:
app: nextjs-eks-app
template:
metadata:
labels:
app: nextjs-eks-app
spec:
containers:
- name: nextjs
# Your ECR image URI - no imagePullSecrets needed
image: [[ECR_IMAGE="your-account-id.dkr.ecr.us-east-1.amazonaws.com/nextjs-production:latest"]]
ports:
- containerPort: 3000
What does this manifest do?
This Deployment creates a ReplicaSet that maintains 2 identical pods running your Next.js container.
- replicas: 2 tells Kubernetes to always keep 2 copies of the pod running.
- selector.matchLabels connects the Deployment to its pods via the label app: nextjs-eks-app.
- containerPort: 3000 declares the port your Next.js app listens on inside the container.
💡 Why no imagePullSecrets?
When eksctl creates a managed node group, it attaches the AmazonEC2ContainerRegistryReadOnly policy to the node IAM role. This allows the kubelet on each node to authenticate to ECR automatically using the ECR credential helper.
Because your ECR image and EKS cluster are in the same AWS account, no additional configuration is needed. The nodes can pull your image without any secrets in the manifest.
- Save deployment.yaml.
- Confirm the file exists by running:
ls deployment.yaml
You should see deployment.yaml listed in the output.
File not showing up?
- Make sure you saved the file in the same directory where you ran ls. Run pwd to check your current location.
- Check that your editor actually wrote the file to disk. Some editors require an explicit save action.
Help me find where my deployment.yaml was saved.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-eks-app
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nextjs-eks-app
template:
metadata:
labels:
app: nextjs-eks-app
spec:
containers:
- name: nextjs
image: [[ECR_IMAGE="your-account-id.dkr.ecr.us-east-1.amazonaws.com/nextjs-production:latest"]]
ports:
- containerPort: 3000
Write the Service manifest
A Kubernetes Service of type LoadBalancer tells AWS to provision a Classic Load Balancer that routes internet traffic to your pods. It maps external port 80 to your container's port 3000.
- Create a file called service.yaml in the same folder by pasting the following:
# Kubernetes Service to expose the app via an AWS Load Balancer
apiVersion: v1
kind: Service
metadata:
name: nextjs-eks-service
namespace: default
spec:
# LoadBalancer type provisions an AWS Classic Load Balancer
type: LoadBalancer
selector:
app: nextjs-eks-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
What does this Service do?
The Service acts as a stable network endpoint for your pods.
- type: LoadBalancer tells EKS to provision an AWS Classic Load Balancer with a public DNS name.
- selector: app: nextjs-eks-app routes traffic only to pods with that label (the ones your Deployment creates).
- port: 80 is the external port users hit. targetPort: 3000 is the port inside each container.
- Save service.yaml.
- Confirm both manifest files exist by running:
ls *.yaml
You should see cluster.yaml, deployment.yaml, and service.yaml listed.
Only seeing cluster.yaml?
- Verify you saved deployment.yaml and service.yaml to the same directory as cluster.yaml.
- If you used a different directory, move the files with mv or recreate them in the correct location.
Help me get my YAML files into the same directory.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
apiVersion: v1
kind: Service
metadata:
name: nextjs-eks-service
namespace: default
spec:
type: LoadBalancer
selector:
app: nextjs-eks-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
Apply both manifests and verify the pods
With both manifest files ready, you can tell Kubernetes to create the Deployment and Service. Kubernetes will schedule pods onto your nodes and begin pulling the container image from ECR.
- Apply the Deployment manifest by running:
kubectl apply -f deployment.yaml
- Apply the Service manifest by running:
kubectl apply -f service.yaml
What happens when you apply?
kubectl apply sends your manifest to the Kubernetes API server. The API server validates it, stores the desired state in etcd, and the scheduler assigns pods to nodes.
The kubelet on each node then pulls the image from ECR and starts the container. The Service also triggers AWS to provision a Classic Load Balancer in your VPC.
- Watch the pods come up in real time by running:
kubectl get pods -w
You'll see each pod transition through statuses like Pending, ContainerCreating, and finally Running with READY showing 1/1. Once both pods show 1/1 Running, press Ctrl+C to stop watching.
Seeing ImagePullBackOff or ErrImagePull?
- Double check your image URI in deployment.yaml. It must match your ECR repository URI exactly, including the account ID, region, repository name, and tag.
- Verify the image exists in ECR by running aws ecr describe-images --repository-name nextjs-production. If nothing is returned, go back to Project #1 and push the image.
- Confirm your cluster and ECR repository are in the same AWS region. Cross-region pulls require additional configuration.
Help me debug why my pods can't pull the image from ECR.
Your manifests are applied and Kubernetes is scheduling pods onto your nodes. Next up, you'll learn how to diagnose common deployment failures using kubectl's diagnostic commands.
Troubleshoot Deployment Failures
You applied your manifests and Kubernetes scheduled your pods. In production, pods do not always transition smoothly to Running status. Pods can get stuck in ContainerCreating, ImagePullBackOff, or Pending for infrastructure reasons that have nothing to do with your application code.
Successfully diagnosing and resolving these failures is a core skill for operating applications on Amazon EKS. In this step, you will learn the diagnostic workflow that production engineers use: observe the symptom, investigate with kubectl commands, and apply targeted fixes.
In this step, get ready to:
- Diagnose pods stuck in ContainerCreating using kubectl describe and events.
- Understand common EKS deployment failures and their root causes.
- Use kubectl logs, describe, and get events to investigate infrastructure issues.
Investigate pod status with kubectl describe
When a pod is not in Running status, the first command to reach for is kubectl describe pod. This shows you the full lifecycle of the pod including scheduling decisions, image pulls, and any errors the kubelet encountered.
- Check the current status of your pods by running:
kubectl get pods
- Pick one of your pods and describe it in detail by running this command:
kubectl describe pod $(kubectl get pods -o jsonpath='{.items[0].metadata.name}')
What should I look for in describe output?
Scroll to the Events section at the bottom. This is the diagnostic goldmine. Events tell you exactly what happened in chronological order.
- Scheduled confirms the pod was assigned to a node.
- Pulling and Pulled confirm the container image was downloaded from ECR.
- Created and Started confirm the container is running.
- If you see Warning events like FailedCreatePodSandBox or FailedScheduling, these tell you exactly what went wrong.
Diagnose common EKS deployment failures
The following failures are the most common issues EKS operators encounter. Understanding the symptoms and root causes for each helps you resolve problems quickly.
Pods stuck in ContainerCreating
Symptom: kubectl get pods shows status ContainerCreating for more than 30 seconds.
Root cause: The aws-node VPC CNI plugin cannot assign a VPC IP address to the pod. The Events section in kubectl describe pod will show failed to assign an IP address to container. This happens when the node has exhausted its ENI/IP capacity or the VPC subnet has run out of available addresses.
Fix: Check whether the aws-node DaemonSet is healthy with kubectl get ds -n kube-system. If it is healthy, the node may simply be at its 17-pod capacity limit. Scale the node group or use a larger instance type.
💡 Image pull failures from ECR
Symptom: pods show ImagePullBackOff or ErrImagePull status.
Root cause: The kubelet on the node cannot pull the container image from ECR. Common causes include an incorrect image URI, a missing or misspelled tag, the ECR repository being in a different region than the cluster, or the node IAM role missing the AmazonEC2ContainerRegistryReadOnly policy.
Fix: Verify your image URI matches exactly what ECR shows. Run aws ecr describe-images --repository-name nextjs-production --region $REGION to confirm the image and tag exist.
💡 Too many pods scheduling errors
Symptom: pods stay in Pending status and kubectl describe pod shows FailedScheduling with a message about insufficient resources.
Root cause: The scheduler cannot find a node with enough CPU, memory, or pod capacity to place the pod. On t3.medium nodes with a 17-pod limit, system pods already consume 3-4 slots. If you deploy more replicas than your nodes can handle, some pods will remain unscheduled.
Fix: Reduce replica count or add more nodes to the cluster. Check current usage with kubectl describe nodes | grep -A 5 "Allocated resources".
💡 LoadBalancer stuck in Pending
Symptom: kubectl get svc shows EXTERNAL-IP as <pending> for more than 5 minutes.
Root cause: The cloud controller cannot provision the AWS Classic Load Balancer. This is typically caused by the nodes running in private subnets without proper tags, or by EC2/ELB service quota limits in your account.
Fix: Check kubectl describe svc nextjs-eks-service for events. Look for messages about subnet tagging or quota errors. If you created the cluster with eksctl, subnet tags should already be correct.
Use events and logs for full-picture diagnostics
Individual pod events only show one piece of the puzzle. Cluster-wide events and container logs give you the full picture of what is happening across your deployment.
- View all recent cluster events sorted by time by running:
kubectl get events --sort-by='.lastTimestamp' -A
Look for any Warning type events. Normal events show successful operations. Warning events indicate problems that need investigation.
- Check your application container logs for startup errors by running:
kubectl logs -l app=nextjs-eks-app --tail=30
When to use each diagnostic command
- kubectl describe pod shows the full lifecycle and events for a single pod. Use it when you know which pod is failing.
- kubectl logs shows stdout/stderr from the application container. Use it when the pod started but your app is crashing or misbehaving.
- kubectl get events shows cluster-wide events across all resources. Use it for a bird's-eye view of what happened during and after deployment.
- kubectl describe node shows resource allocation and conditions on a specific node. Use it when you suspect the node itself is the problem (out of IPs, disk pressure, memory pressure).
- Inspect a node for capacity issues by running:
kubectl describe node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') | grep -A 10 "Conditions"
All conditions should show False for negative conditions (MemoryPressure, DiskPressure, PIDPressure) and True for Ready. Any other combination indicates a node-level problem.
Still seeing pods stuck in ContainerCreating?
- Run kubectl describe pod <pod-name> and look at the Events section. If you see failed to assign an IP address to container, the VPC CNI has exhausted available IPs on that node.
- Verify aws-node is running on all nodes with kubectl get ds -n kube-system. If READY does not match DESIRED, the CNI itself has a problem.
- Check whether the VPC subnet has available IP addresses. A subnet with no free IPs prevents the CNI from allocating addresses regardless of node capacity.
Help me diagnose why my EKS pods are stuck in ContainerCreating with IP assignment errors
You now have a diagnostic toolkit for the most common EKS deployment failures. Whether pods are stuck in ContainerCreating, failing to pull images, or unable to schedule, you know exactly which commands to run and what to look for. Next up, you'll verify that your app is accessible from the internet.
Verify and Access Your Live App
You have a diagnostic toolkit for troubleshooting deployment failures, and you know how to investigate pods stuck in ContainerCreating, image pull errors, and scheduling problems. Now it's time to confirm that your Next.js app is actually accessible from the internet.
The LoadBalancer Service you created in the previous step triggers AWS to provision a Classic Load Balancer that routes external traffic to your pods. In this step, you'll grab that public URL, test it, and inspect the deployment to confirm everything is healthy.
In this step, get ready to:
- Retrieve the LoadBalancer's public URL from Kubernetes.
- Test your app and health check endpoint with curl and a browser.
- Inspect the deployment using kubectl describe and logs.
Get the LoadBalancer external URL
When you created the Service of type LoadBalancer, Kubernetes asked AWS to provision a Classic Load Balancer. This can take 2-3 minutes to fully provision and receive a public hostname.
- Check the status of your Service by running this command:
kubectl get svc nextjs-eks-service
✔️ I see an EXTERNAL-IP hostname
The EXTERNAL-IP column shows a hostname like xxx.us-east-1.elb.amazonaws.com. Your Load Balancer is ready.
- Copy the EXTERNAL-IP hostname. You'll use it in the next substep.
ⓧ EXTERNAL-IP shows <pending>
The AWS Classic Load Balancer is still provisioning. This is normal and can take 2-3 minutes.
- Wait 30 seconds, then re-run the command:
kubectl get svc nextjs-eks-service
Repeat until you see a hostname in the EXTERNAL-IP column.
What is the EXTERNAL-IP?
The EXTERNAL-IP is the public DNS name of the AWS Classic Load Balancer that Kubernetes provisioned for your Service. It routes traffic from port 80 on the internet to port 3000 on your pods.
Test the application and health check
With the public hostname in hand, you can verify that your containerized app is serving real traffic from EKS.
- Test your app's root page by running this command:
curl http://[[EXTERNAL_IP="your LoadBalancer hostname (e.g. xxx.us-east-1.elb.amazonaws.com)"]]/
- You should see your Next.js app's HTML output in the terminal.
- Test the health check endpoint by running this command:
curl http://[[EXTERNAL_IP="your LoadBalancer hostname (e.g. xxx.us-east-1.elb.amazonaws.com)"]]/api/health
- You should see the health check JSON response you set up in Project #1.
- Open the same URL in your browser (paste http:// followed by your EXTERNAL-IP hostname into the address bar).
You should see your Next.js app rendered in the browser, served live from your EKS cluster.
Seeing 'Connection refused' or a timeout?
If the Load Balancer was recently provisioned, it may take an additional minute for health checks to pass and route traffic. Wait 60 seconds and try again.
If you still see errors, check that your pods are running with kubectl get pods. Both should show Running with READY 1/1.
help me troubleshoot why my EKS LoadBalancer is not routing traffic to my pods
Inspect the deployment from a Kubernetes perspective
Your app is live, but as a Kubernetes operator you should also know how to inspect what's happening inside the cluster. These commands help you confirm image versions, replica health, and recent activity.
- View the full details of your deployment by running this command:
kubectl describe deployment nextjs-eks-app
What should I look for?
Look for the Image field (confirms your ECR image URI), the Replicas line (should show 2 desired | 2 updated | 2 total | 2 available | 0 unavailable), and the Conditions section (should show Available True).
- Check the startup logs from your app containers by running:
kubectl logs -l app=nextjs-eks-app --tail=20
- You should see your Next.js server startup messages confirming the app is listening on port 3000.
- Review recent cluster events by running:
kubectl get events --sort-by='.lastTimestamp'
What do cluster events tell me?
Events show the lifecycle of your resources. You should see events like pod scheduling, image pulling from ECR, container creation, and the Load Balancer provisioning. These are your audit trail for what happened during deployment.
🙋♀️ Seeing ImagePullBackOff in events?
This means the node cannot pull your image from ECR. Verify your ECR image URI matches exactly what you pushed in Project #1. The format should be <account-id>.dkr.ecr.<region>.amazonaws.com/nextjs-production:latest.
help me fix an ImagePullBackOff error when my EKS managed nodes try to pull from ECR
Your Next.js app is live on the internet, served from a production Kubernetes cluster. Next up, you'll tear down all resources to avoid ongoing AWS charges.
Tear Down Everything
Your Next.js app is live on the internet, serving traffic through a Kubernetes LoadBalancer. That is a genuine production deployment.
Now it is time to tear everything down cleanly. The resources you provisioned (EKS control plane, EC2 nodes, NAT Gateway, Load Balancer) are actively billing your AWS account. A clean teardown in the right order prevents orphaned resources that continue to incur charges and block future VPC operations.
In this step, get ready to:
- Delete the Kubernetes LoadBalancer Service to release the AWS Load Balancer.
- Delete the EKS cluster and both CloudFormation stacks.
- Verify no orphaned resources remain in your AWS account.
Delete the Kubernetes service first
When you created a Service of type LoadBalancer, Kubernetes asked AWS to provision a Classic Load Balancer. That Load Balancer lives outside the EKS cluster in your VPC.
If you delete the cluster without removing the Service first, the Load Balancer becomes orphaned. It stays in your VPC, continues billing, and blocks VPC deletion.
Why delete the Service before the cluster?
The EKS cluster manages the lifecycle of the Load Balancer through a cloud controller. When you delete the Service, the controller tells AWS to deprovision the Load Balancer.
If the cluster is gone first, no controller exists to issue that cleanup call. The Load Balancer becomes an orphan that you must manually delete.
- Delete the LoadBalancer Service by running this command:
kubectl delete svc nextjs-eks-service
- Confirm the service is gone by running:
kubectl get svc
You should see only the default kubernetes ClusterIP service listed. The nextjs-eks-service with its EXTERNAL-IP is gone.
- Wait 1-2 minutes for the AWS Load Balancer to fully deprovision before continuing.
Still see the service listed?
Make sure you typed the service name exactly as nextjs-eks-service. Run kubectl get svc again to check. If the service still appears, wait a few more seconds and retry the delete command.
Why isn't my Service being deleted?
Delete the cluster and all associated resources
With the Load Balancer released, you can safely delete the entire EKS cluster. The eksctl delete cluster command tears down both CloudFormation stacks that were created during cluster provisioning.
- Delete the cluster by running this command:
eksctl delete cluster --name nextjs-eks-cluster --region $REGION --wait
What does this command do?
The --wait flag tells eksctl to wait until both CloudFormation stacks are fully deleted before returning. Without it, eksctl would issue the delete request and exit immediately without reporting errors.
This command deletes the nodegroup stack (eksctl-nextjs-eks-cluster-nodegroup-nextjs-eks-nodes) first, then the cluster stack (eksctl-nextjs-eks-cluster-cluster). It removes the VPC, subnets, NAT Gateway, EC2 instances, and the EKS control plane. This takes 5-10 minutes.
You should see output messages like will delete stack "eksctl-nextjs-eks-cluster-nodegroup-nextjs-eks-nodes" followed by will delete stack "eksctl-nextjs-eks-cluster-cluster". Wait for the command to complete fully before moving on.
Cluster deletion stuck or failing?
If deletion hangs due to Pod Disruption Budget (PDB) policies, try adding the --disable-nodegroup-eviction flag to bypass PDB checks:
eksctl delete cluster --name nextjs-eks-cluster --region $REGION --wait --disable-nodegroup-eviction
If you see a VPC deletion error, it usually means a Load Balancer was not released before cluster deletion. Go to the AWS EC2 console, find the orphaned Load Balancer under the Load Balancers section, delete it manually, then retry.
Help me resolve this deletion issue
Verify no orphaned resources remain
A successful eksctl delete does not guarantee every resource was cleaned up. Network resources like Load Balancers and NAT Gateways can linger if something went wrong.
These five commands confirm your account is clean and no resources are still billing.
- Check for orphaned Classic Load Balancers by running:
aws elb describe-load-balancers --query 'LoadBalancerDescriptions[*].LoadBalancerName' --output text
- Check for orphaned Application Load Balancers by running:
aws elbv2 describe-load-balancers --query 'LoadBalancers[*].LoadBalancerName' --output text
- Check for lingering NAT Gateways by running:
aws ec2 describe-nat-gateways --filter "Name=state,Values=available" --query 'NatGateways[*].NatGatewayId' --output text
- Check for remaining CloudFormation stacks by running:
aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE --query 'StackSummaries[?contains(StackName,`eksctl-nextjs-eks`)].StackName' --output text
- Confirm the EKS cluster no longer exists by running:
aws eks list-clusters --query 'clusters' --output text
What should I see?
Each command should return empty output or output that does not contain any resources from your deleted cluster. Here is what to look for:
- The aws elb command checks for orphaned Classic Load Balancers. Empty or no match means clean.
- The aws elbv2 command checks for Application Load Balancers. Empty or no match means clean.
- The aws ec2 describe-nat-gateways command checks for lingering NAT Gateways in an available state. Empty means clean.
- The aws cloudformation command checks for any remaining eksctl stacks. Empty means both stacks were fully deleted.
- The aws eks list-clusters command confirms the cluster itself no longer exists.
🙋♀️ Seeing leftover resources?
If any command returns resources related to your cluster, those are orphans actively billing your account. Delete them manually through the AWS console in the relevant service sections (EC2 for Load Balancers, VPC for NAT Gateways, CloudFormation for stacks).
How do I clean up orphaned AWS resources?
Secret mission
Production-Harden Your Deployment
Your deployment works, but it is fragile. There is no way for Kubernetes to know if your app is stuck or overloaded. Add health probes, resource constraints, and scale to 3 replicas to make this deployment production-grade.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running or delete them entirely. EKS does not support pausing, so your only real options are to keep running (and keep paying) or delete everything.
Cost warning
Your EKS cluster is actively billing at approximately $0.25/hr ($6/day). This includes the EKS control plane ($0.10/hr), two t3.medium EC2 instances ($0.0832/hr total), a NAT Gateway (~$0.045/hr), and a Classic Load Balancer (~$0.025/hr).
Delete your resources as soon as you finish the project. A 2-3 hour session should cost $1-3 total.
Resources you used:
- EKS cluster (nextjs-eks-cluster) including control plane, managed node group, VPC, and subnets.
- 2x t3.medium EC2 instances (managed nodes).
- NAT Gateway (created by eksctl inside the cluster VPC).
- Classic Load Balancer (created by the Kubernetes LoadBalancer Service).
- Local project files (cluster.yaml, deployment.yaml, service.yaml).
Keep everything running
No action needed. Choose this if you are still actively working on the project or plan to experiment further in the next hour.
- Your cluster, nodes, NAT Gateway, and Load Balancer will continue billing at approximately $0.25/hr.
- Your app remains accessible at the LoadBalancer EXTERNAL-IP.
- eksctl and kubectl remain installed on your machine (no ongoing cost).
Pause - I'll come back to this later
EKS does not support pausing. You cannot stop the control plane or managed nodes without deleting them.
If you want to stop costs but return later, delete the cluster now and re-create it from your saved YAML files when you come back. Your cluster.yaml, deployment.yaml, and service.yaml files make re-provisioning straightforward.
- Follow the Delete tab instructions to tear down all cloud resources.
- Keep your local YAML files so you can re-create the cluster later.
Delete - I don't want to use this again
Remove all project resources and return your AWS account to a zero-cost state. Follow these steps in order.
Delete the Kubernetes LoadBalancer Service first to release the Load Balancer:
- Run this command to delete the service:
kubectl delete svc nextjs-eks-service
- Confirm the service is gone by running:
kubectl get svc
- Wait 1-2 minutes for the AWS Load Balancer to deprovision before proceeding.
Why delete the service first?
If you delete the cluster without first removing LoadBalancer services, the ELB can become orphaned in your VPC. An orphaned ELB blocks VPC deletion and continues to incur charges.
Delete the cluster and all associated resources:
- Run this command to delete the cluster (this takes 5-10 minutes):
eksctl delete cluster --name nextjs-eks-cluster --region $REGION --wait
What does the --wait flag do?
The --wait flag ensures eksctl waits for both CloudFormation stacks (nodegroup and cluster) to fully delete. Without it, eksctl may exit before cleanup finishes.
Verify no orphaned resources remain:
- Check for orphaned Classic Load Balancers:
aws elb describe-load-balancers --query 'LoadBalancerDescriptions[*].LoadBalancerName' --output text
- Check for orphaned Application Load Balancers:
aws elbv2 describe-load-balancers --query 'LoadBalancers[*].LoadBalancerName' --output text
- Check for lingering NAT Gateways:
aws ec2 describe-nat-gateways --filter "Name=state,Values=available" --query 'NatGateways[*].NatGatewayId' --output text
- Check for remaining CloudFormation stacks:
aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE --query 'StackSummaries[?contains(StackName,`eksctl-nextjs-eks`)].StackName' --output text
- Confirm the cluster no longer exists:
aws eks list-clusters --query 'clusters' --output text
All five commands should return empty output (or output that does not contain your cluster's resources). If any command shows lingering resources, wait a few minutes and re-run.
Delete local project files:
- Remove your project files by running:
rm cluster.yaml deployment.yaml service.yaml
Cluster deletion stuck or failing?
If deletion hangs due to PodDisruptionBudget policies (common with aws-ebs-csi-driver), re-run with the eviction bypass flag:
eksctl delete cluster --name nextjs-eks-cluster --region $REGION --wait --disable-nodegroup-eviction
help me debug a stuck eksctl cluster deletion
That's a Wrap!
That's a Wrap!
Nice work! You just deployed a production Next.js container to Amazon EKS, verified cluster health at the infrastructure level, diagnosed deployment failures like a production engineer, and tore down every resource cleanly.
You've learned how to:
- Provisioned an EKS cluster and verified the AWS VPC CNI networking layer can assign pod IP addresses before deploying workloads.
- Diagnosed common deployment failures (ContainerCreating, image pull errors, IP exhaustion) using kubectl describe, kubectl logs, and kubectl get events.
- Deployed a production container from ECR to EKS and exposed it to the internet via a Kubernetes LoadBalancer Service.
- Secret Mission: Hardened the deployment with liveness/readiness probes, resource limits, and 3-replica scaling.
Ready to quiz yourself?