Tutorial: Automatically Deploy Next.js App to AWS EKS with Docker, AWS ECS, and GitHub Actions

Asher Best Photo

Asher Best • March 22, 2025

In this DevOps tutorial, I am going to show you how to automatically deploy a Next.js app to AWS Elastic Kubernetes Service (EKS) with Docker, AWS Elastic Container Service (ECS), and GitHub Actions. This tutorial assumes you have already set up your AWS environment. Be sure to delete your resources at the end of this tutorial to avoid incurring costs. Let's jump right in!

1. Create Next.js Project

First, let's initialize a Next app:

1	npx create-next-app@latest

2. Create Dockerfile

Create a Docker file in the root directory:

./Dockerfile
1	# Build Stage
2	FROM node:22 AS builder
3	WORKDIR /app
4	COPY package.json ./
5	RUN npm install
6	COPY . .
7	RUN npm run build
8	
9	# Production Stage
10	FROM node:22
11	WORKDIR /app
12	COPY --from=builder /app ./
13	EXPOSE 3000
14	CMD [ "npm", "start" ]

We separate the Dockerfile into two parts: build stage and production stage. For the build stage, we use a node base image, install dependencies, and run the build. For the production stage, we copy the files from the build stage, expose port 3000, and set the npm start command.

Create a Docker ignore file in the root directory to reduce container size and prevent the exposure of keys and secrets:

./.dockerignore
1	node_modules
2	.git
3	.env

Build and run Docker locally replacing <image_name> with the name of your Docker image:

1	docker build -t <image_name> .
2	docker run -p 3000:3000 <image_name>

3. Create ECR Repository & Push Docker Image

Create an AWS policy for ECR with the follow permissions:

  • Allow authentication and repository creation

  • Push and pull container images

  • List images and view repository details

1	{
2	  "Version": "2012-10-17",
3	  "Statement": [
4	    {
5	      "Effect": "Allow",
6	      "Action": ["ecr:GetAuthorizationToken", "ecr:CreateRepository"],
7	      "Resource": "*"
8	    },
9	    {
10	      "Effect": "Allow",
11	      "Action": [
12	        "ecr:BatchCheckLayerAvailability",
13	        "ecr:GetDownloadUrlForLayer",
14	        "ecr:GetRepositoryPolicy",
15	        "ecr:DescribeRepositories",
16	        "ecr:ListImages",
17	        "ecr:DescribeImages",
18	        "ecr:BatchGetImage",
19	        "ecr:InitiateLayerUpload",
20	        "ecr:UploadLayerPart",
21	        "ecr:CompleteLayerUpload",
22	        "ecr:PutImage",
23	        "ecr:DeleteRepository"
24	      ],
25	      "Resource": "arn:aws:ecr:<region>:<account-id>:repository/<repo-name>"
26	    }
27	  ]
28	}

Next, we need to authenticate Docker with AWS ECR, create a repository in AWS ECR, tag the Docker image, and push the Docker image to AWS ECR:

1	aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com
2	
3	aws ecr create-repository --repository-name <repo-name> --region <aws-region>
4	
5	docker tag <repo-name>:latest <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/<repo-name>:latest
6	
7	docker push <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/<repo-name>:latest

4. Create EKS Cluster

Install eksctl:

1	# for ARM systems, set ARCH to: `arm64`, `armv6` or `armv7`
2	ARCH=amd64
3	PLATFORM=windows_$ARCH
4	
5	curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_$PLATFORM.zip"
6	
7	# (Optional) Verify checksum
8	curl -sL "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_checksums.txt" | grep $PLATFORM | sha256sum --check
9	
10	unzip eksctl_$PLATFORM.zip -d $HOME/bin
11	
12	rm eksctl_$PLATFORM.zip

Create an EKS Cluster with two nodes or however many you prefer:

1	eksctl create cluster --name <cluster-name> --region <aws-region> --nodegroup-name <nodegroup-name> --node-type t3.medium --nodes 2

Configure AWS IAM Policies:

1. Navigate to IAM in the AWS console and then to Policies

2. Create a new policy named EksAllAccess with the following JSON:

1	{
2	  "Version": "2012-10-17",
3	  "Statement": [
4	    {
5	      "Effect": "Allow",
6	      "Action": "eks:*",
7	      "Resource": "*"
8	    },
9	    {
10	      "Action": ["ssm:GetParameter", "ssm:GetParameters"],
11	      "Resource": ["arn:aws:ssm:*:<account_id>:parameter/aws/*", "arn:aws:ssm:*::parameter/aws/*"],
12	      "Effect": "Allow"
13	    },
14	    {
15	      "Action": ["kms:CreateGrant", "kms:DescribeKey"],
16	      "Resource": "*",
17	      "Effect": "Allow"
18	    },
19	    {
20	      "Action": ["logs:PutRetentionPolicy"],
21	      "Resource": "*",
22	      "Effect": "Allow"
23	    }
24	  ]
25	}

Create another policy named IAMLimitedAccess with the following JSON:

1	{
2	  "Version": "2012-10-17",
3	  "Statement": [
4	    {
5	      "Effect": "Allow",
6	      "Action": [
7	        "iam:CreateInstanceProfile",
8	        "iam:DeleteInstanceProfile",
9	        "iam:GetInstanceProfile",
10	        "iam:RemoveRoleFromInstanceProfile",
11	        "iam:GetRole",
12	        "iam:CreateRole",
13	        "iam:DeleteRole",
14	        "iam:AttachRolePolicy",
15	        "iam:PutRolePolicy",
16	        "iam:UpdateAssumeRolePolicy",
17	        "iam:AddRoleToInstanceProfile",
18	        "iam:ListInstanceProfilesForRole",
19	        "iam:PassRole",
20	        "iam:DetachRolePolicy",
21	        "iam:DeleteRolePolicy",
22	        "iam:GetRolePolicy",
23	        "iam:GetOpenIDConnectProvider",
24	        "iam:CreateOpenIDConnectProvider",
25	        "iam:DeleteOpenIDConnectProvider",
26	        "iam:TagOpenIDConnectProvider",
27	        "iam:ListAttachedRolePolicies",
28	        "iam:TagRole",
29	        "iam:UntagRole",
30	        "iam:GetPolicy",
31	        "iam:CreatePolicy",
32	        "iam:DeletePolicy",
33	        "iam:ListPolicyVersions"
34	      ],
35	      "Resource": [
36	        "arn:aws:iam::<account_id>:instance-profile/eksctl-*",
37	        "arn:aws:iam::<account_id>:role/eksctl-*",
38	        "arn:aws:iam::<account_id>:policy/eksctl-*",
39	        "arn:aws:iam::<account_id>:oidc-provider/*",
40	        "arn:aws:iam::<account_id>:role/aws-service-role/eks-nodegroup.amazonaws.com/AWSServiceRoleForAmazonEKSNodegroup",
41	        "arn:aws:iam::<account_id>:role/eksctl-managed-*"
42	      ]
43	    },
44	    {
45	      "Effect": "Allow",
46	      "Action": ["iam:GetRole", "iam:GetUser"],
47	      "Resource": ["arn:aws:iam::<account_id>:role/*", "arn:aws:iam::<account_id>:user/*"]
48	    },
49	    {
50	      "Effect": "Allow",
51	      "Action": ["iam:CreateServiceLinkedRole"],
52	      "Resource": "*",
53	      "Condition": {
54	        "StringEquals": {
55	          "iam:AWSServiceName": ["eks.amazonaws.com", "eks-nodegroup.amazonaws.com", "eks-fargate.amazonaws.com"]
56	        }
57	      }
58	    }
59	  ]
60	}

Navigate to the AWS IAM role you are authenticated with locally and add the following AWS managed IAM policies plus the custom policies you just created:

  • AmazonEC2FullAccess

  • AWSCloudFormationFullAccess

  • EksAllAccess

  • IAMLimitedAccess

5. Configure kubectl for EKS

Update EKS cluster configuration:

1	aws eks --region <region> update-kubeconfig --name <cluster-name>

Get cluster nodes:

1	kubectl get nodes

Create a Kubernetes deployment file:

./deployment.yaml
1	apiVersion: apps/v1
2	kind: Deployment
3	metadata:
4	  name: <app-name>-deployment
5	  labels:
6	    app: <app-name>
7	spec:
8	  replicas: 2
9	  selector:
10	    matchLabels:
11	      app: <app-name>
12	  template:
13	    metadata:
14	      labels:
15	        app: <app-name>
16	    spec:
17	      containers:
18	        - name: <app-name>
19	          image: <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/<app-name>-repo:latest
20	          ports:
21	            - containerPort: 80
22	          imagePullPolicy: Always
23	      imagePullSecrets:
24	        - name: ecr-secret

Create an image pull secret for AWS ECR, apply the deployment, and verify the deployment:

1	kubectl create secret docker-registry ecr-secret --docker-server=<your-aws-account-id>.dkr.ecr.<region>.amazonaws.com --docker-username=AWS --docker-password=$(aws ecr get-login-password --region <region>)
2	
3	kubectl apply -f deployment.yaml
4	
5	kubectl get deployments

Create service file to expose the app:

./service.yaml
1	apiVersion: v1
2	kind: Service
3	metadata:
4	  name: <app-name>-service
5	spec:
6	  type: LoadBalancer
7	  selector:
8	    app: <app-name>
9	  ports:
10	    - protocol: TCP
11	      port: 80
12	      targetPort: 80

Apply the service and get the external IP address:

1	kubectl apply -f service.yaml
2	
3	kubectl get svc <app-name>-service

Once the LoadBalancer is ready, the EXTERNAL-IP will show up, and you can access your app in the browser!

6. Automate EKS Deployment with GitHub Actions

Create AWS IAM User & Attach Permissions

  1. Go to AWS IAM Console → Users → Create User

  2. Name it GitHubActionsEKS or whatever you prefer

  3. Attach the following managed policies:

    • AmazonEKSClusterPolicy

    • AmazonEC2ContainerRegistryFullAccess

    • AmazonEKSWorkerNodePolicy

    • AmazonEKSServicePolicy

    • IAMFullAccess (Needed to interact with IAM roles)

    • AmazonS3FullAccess (Optional for storing artifacts)

  4. Generate & save the AWS Access Key and Secret Key.

Store AWS Credentials in GitHub Secrets

Go to your GitHub Repository → Settings → Secrets and Variables → Actions → New Repository Secret Add the following secrets:

  • AWS_ACCESS_KEY_ID → Your IAM Access Key

  • AWS_SECRET_ACCESS_KEY → Your IAM Secret Key

  • AWS_REGION → e.g., us-east-1

  • ECR_REGISTRY → <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com

  • ECR_REPOSITORY → Your ECR repository name

  • EKS_CLUSTER_NAME → Your EKS cluster name

Inside your repository, create the GitHub Actions workflow file:

.github/workflows/deploy.yml
1	name: Deploy to Amazon EKS with Rollback
2	
3	on:
4	  push:
5	    branches:
6	      - main
7	
8	jobs:
9	  deploy:
10	    name: Deploy to EKS
11	    runs-on: ubuntu-latest
12	
13	    steps:
14	      - name: Checkout Code
15	        uses: actions/checkout@v4
16	
17	      - name: Configure AWS Credentials
18	        uses: aws-actions/configure-aws-credentials@v2
19	        with:
20	          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
21	          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
22	          aws-region: ${{ secrets.AWS_REGION }}
23	
24	      - name: Authenticate with AWS ECR
25	        run: |
26	          aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | \
27	          docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }}
28	
29	      - name: Build and Push Docker Image
30	        run: |
31	          IMAGE_TAG=$(echo $GITHUB_SHA | cut -c1-7)
32	          docker build -t ${{ secrets.ECR_REGISTRY }}/${{ secrets.ECR_REPOSITORY }}:$IMAGE_TAG .
33	          docker push ${{ secrets.ECR_REGISTRY }}/${{ secrets.ECR_REPOSITORY }}:$IMAGE_TAG
34	          echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
35	
36	      - name: Update kubeconfig for EKS and create ECR secret
37	        run: |
38	          aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name ${{ secrets.EKS_CLUSTER_NAME }}
39						kubectl create secret docker-registry ecr-secret --docker-server=${{ secrets.ECR_REPOSITORY }} --docker-username=AWS --docker-password=$(aws ecr get-login-password --region ${{ secrets.AWS_REGION }})
40	
41	      - name: Get Previous Image Tag (For Rollback)
42	        id: get-prev-image
43	        run: |
44	          PREV_IMAGE=$(kubectl get deployment <deployment-name> -o=jsonpath='{.spec.template.spec.containers[0].image}')
45	          echo "PREV_IMAGE=$PREV_IMAGE" >> $GITHUB_ENV
46	
47	      - name: Deploy to Kubernetes
48	        id: deploy
49	        run: |
50	          kubectl set image deployment/<deployment-name> <app-name>=${{ secrets.ECR_REGISTRY }}/${{ secrets.ECR_REPOSITORY }}:$IMAGE_TAG
51	          kubectl rollout status deployment/<deployment-name> || exit 1
52	
53	      - name: Rollback on Failure
54	        if: failure()
55	        run: |
56	          echo "Deployment failed! Rolling back to previous stable image..."
57	          kubectl set image deployment/<deployment-name> <app-name>=$PREV_IMAGE
58	          kubectl rollout status deployment/<deployment-name>

(Optional) Map GitHubActionsEKS IAM Identity to EKS Cluster

This step is only necessary if the IAM profile used to create the cluster is different than the GitHubActionsEKS profile.

Check the cluster IAM identity mapping, create the IAM identity mapping, and update the kubeconfig file:

1	eksctl get iamidentitymapping --cluster <cluster-name>
2	
3	eksctl create iamidentitymapping --cluster <cluster-name> --region <region> --arn arn:aws:iam::<aws-account-id>:user/<aws-iam-username> --group system:masters --no-duplicate-arns --username <aws-iam-username>
4	
5	aws eks update-kubeconfig --name <eks-cluster-name> --region <aws-region>

7. Clean Up AWS Resources to Avoid Incurred Costs

Delete the EKS cluster and ECR repository:

1	eksctl delete cluster --region=<region> --name=<cluster-name>
2	
3	aws ecr delete-repository --repository-name <repo-name> --region <region> --force

And that is it! You successfully deployed a Next.js app to AWS Elastic Kubernetes Service (EKS) with Docker, AWS Elastic Container Service (ECS), and GitHub Actions.

© 2025 — Website designed & developed by Asher Best