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

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
Go to AWS IAM Console → Users → Create User
Name it
GitHubActionsEKS
Attach the following managed policies:
AmazonEKSClusterPolicy
AmazonEC2ContainerRegistryFullAccess
AmazonEKSWorkerNodePolicy
AmazonEKSServicePolicy
IAMFullAccess
(Needed to interact with IAM roles)AmazonS3FullAccess
(Optional for storing artifacts)
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 KeyAWS_SECRET_ACCESS_KEY
→ Your IAM Secret KeyAWS_REGION
→ e.g., us-east-1ECR_REGISTRY
→ <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.comECR_REPOSITORY
→ Your ECR repository nameEKS_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.