Tools: CI/CD for your Dockerized App with AWS CodeBuild, CodeDeploy and CodePipeline (Part 3/3) (2026)

Tools: CI/CD for your Dockerized App with AWS CodeBuild, CodeDeploy and CodePipeline (Part 3/3) (2026)

Prepare the codebase

Add buildspec.yml

Add deployment scripts

Add appspec.yml

AWS CodeBuild

Create an IAM role for CodeBuild

Create the CodeBuild project

AWS CodeDeploy

Create an IAM role for CodeDeploy

Create a CodeDeploy application

Create a deployment group

AWS CodePipeline

Test the pipeline

Wrapping up This is the final part of a three-part series where we build a production-ready, auto-scaled and continuously deployed Node.js application on AWS. In Part 1, we dockerized a Node.js app, pushed the image to AWS ECR and deployed it to an EC2 instance. In Part 2, we created an AMI, a launch template, an Application Load Balancer and an Auto Scaling Group so our app scales automatically. In this part, we wire up a full CI/CD pipeline. Every push to main on GitHub will: Make sure your code is hosted in a GitHub repository before continuing. Before touching AWS, we need to add three files to the repository: a build spec for CodeBuild and a set of deployment scripts for CodeDeploy. Create a buildspec.yml file at the root of your repository. CodeBuild reads this file to know how to build and push your Docker image. CodeDeploy needs scripts that run on each EC2 instance during a deployment. Create a folder called aws-scripts in your repository and add the following three files. aws-scripts/before_install.sh aws-scripts/application_stop.sh aws-scripts/application_start.sh This is the important one. It pulls the latest Docker image, refreshes the .env file from Parameter Store, restarts the container and reloads nginx. Create an appspec.yml file at the root of your repository. This tells CodeDeploy which scripts to run at each stage of the deployment. Commit and push all these changes to main before moving on to AWS. CodeBuild will run our buildspec.yml every time the pipeline is triggered. It will build the Docker image and push it to ECR. Search for IAM in the AWS panel and go to Roles. Click Create role. Search for CodeBuild in the AWS panel and click Create project. You can go to your CodeBuild project and click Start build to verify the build works before setting up the pipeline. CodeDeploy handles getting the latest image from ECR onto each EC2 instance in our Auto Scaling Group. Prerequisite: The AMI we created in Part 2 already has the CodeDeploy agent installed. If your instances are running, they should have it. You can verify with sudo service codedeploy-agent status on any running instance. Go to IAM → Roles and click Create role. After creating the role, we need to attach a few more permissions. Click on the role, then Add permissions → Attach policies, and add the following: Search for CodeDeploy in the AWS panel, go to Applications and click Create application. Click Create application. Inside the application we just created, click Create deployment group. Click Create deployment group. Now we connect everything together. The pipeline will watch for pushes to main, trigger CodeBuild to build and push a new image, then trigger CodeDeploy to roll it out to all instances. Search for CodePipeline in the AWS panel and click Create pipeline. Note: If you run into issues with No artifacts in either the build or deploy stage, switch them to SourceArtifact to resolve it. Inside your CodePipeline, click Release change to trigger a manual run and make sure everything is wired up correctly. Once that succeeds, test the full automated flow: Over this three-part series we went from a simple Node.js app to a fully production-grade deployment on AWS: You now have an auto-scaled, load-balanced, continuously deployed application running on AWS. 🎉 Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# Do not change version. This is the buildspec version, not your file version. version: 0.2 env: variables: AWS_REGION: "<aws_region>" # e.g. "ap-south-1" AWS_ACCOUNT_ID: "<aws_account_id>" # find this in your ECR repository URI IMAGE_REPO_NAME: "my-app" # your ECR repository name IMAGE_TAG: "latest" parameter-store: APP_NAME: "/my-app/APP_NAME" # add any other env variables here phases: pre_build: commands: - echo Logging in to Amazon ECR... - aws ecr get-login-password --region $AWS_REGION | -weight: 500;">docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com build: commands: - echo Build started on `date` - echo Building the Docker image... - -weight: 500;">docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . - -weight: 500;">docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG post_build: commands: - echo Build completed on `date` - echo Pushing the Docker image... - -weight: 500;">docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG # Do not change version. This is the buildspec version, not your file version. version: 0.2 env: variables: AWS_REGION: "<aws_region>" # e.g. "ap-south-1" AWS_ACCOUNT_ID: "<aws_account_id>" # find this in your ECR repository URI IMAGE_REPO_NAME: "my-app" # your ECR repository name IMAGE_TAG: "latest" parameter-store: APP_NAME: "/my-app/APP_NAME" # add any other env variables here phases: pre_build: commands: - echo Logging in to Amazon ECR... - aws ecr get-login-password --region $AWS_REGION | -weight: 500;">docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com build: commands: - echo Build started on `date` - echo Building the Docker image... - -weight: 500;">docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . - -weight: 500;">docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG post_build: commands: - echo Build completed on `date` - echo Pushing the Docker image... - -weight: 500;">docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG # Do not change version. This is the buildspec version, not your file version. version: 0.2 env: variables: AWS_REGION: "<aws_region>" # e.g. "ap-south-1" AWS_ACCOUNT_ID: "<aws_account_id>" # find this in your ECR repository URI IMAGE_REPO_NAME: "my-app" # your ECR repository name IMAGE_TAG: "latest" parameter-store: APP_NAME: "/my-app/APP_NAME" # add any other env variables here phases: pre_build: commands: - echo Logging in to Amazon ECR... - aws ecr get-login-password --region $AWS_REGION | -weight: 500;">docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com build: commands: - echo Build started on `date` - echo Building the Docker image... - -weight: 500;">docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . - -weight: 500;">docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG post_build: commands: - echo Build completed on `date` - echo Pushing the Docker image... - -weight: 500;">docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG #!/bin/bash echo "Before -weight: 500;">install" #!/bin/bash echo "Before -weight: 500;">install" #!/bin/bash echo "Before -weight: 500;">install" #!/bin/bash echo "Application -weight: 500;">stop" #!/bin/bash echo "Application -weight: 500;">stop" #!/bin/bash echo "Application -weight: 500;">stop" #!/bin/bash cd /home/ubuntu AWS_ACCOUNT_ID="<aws_account_id>" AWS_REGION="<aws_region>" IMAGE_TAG="latest" APP_NAME="my-app" PORT="8000" # Pull latest image from ECR aws ecr get-login-password --region $AWS_REGION | -weight: 500;">docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com -weight: 500;">docker pull $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$APP_NAME:$IMAGE_TAG # Refresh .env from Parameter Store aws ssm get-parameters-by-path \ --path "/${APP_NAME}" --recursive --with-decrypt \ | jq -r '.Parameters[] | (.Name | split("/")[-1]) + "=" + (.Value)' \ | tee /home/ubuntu/.env # Stop existing containers and clean up -weight: 500;">docker -weight: 500;">stop $(-weight: 500;">docker ps -q) -weight: 500;">docker system prune -f # Start the updated container -weight: 500;">docker run --env-file .env -p $PORT:$PORT -d $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$APP_NAME:$IMAGE_TAG # Restart nginx -weight: 600;">sudo nginx -t -weight: 600;">sudo -weight: 500;">service nginx -weight: 500;">restart #!/bin/bash cd /home/ubuntu AWS_ACCOUNT_ID="<aws_account_id>" AWS_REGION="<aws_region>" IMAGE_TAG="latest" APP_NAME="my-app" PORT="8000" # Pull latest image from ECR aws ecr get-login-password --region $AWS_REGION | -weight: 500;">docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com -weight: 500;">docker pull $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$APP_NAME:$IMAGE_TAG # Refresh .env from Parameter Store aws ssm get-parameters-by-path \ --path "/${APP_NAME}" --recursive --with-decrypt \ | jq -r '.Parameters[] | (.Name | split("/")[-1]) + "=" + (.Value)' \ | tee /home/ubuntu/.env # Stop existing containers and clean up -weight: 500;">docker -weight: 500;">stop $(-weight: 500;">docker ps -q) -weight: 500;">docker system prune -f # Start the updated container -weight: 500;">docker run --env-file .env -p $PORT:$PORT -d $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$APP_NAME:$IMAGE_TAG # Restart nginx -weight: 600;">sudo nginx -t -weight: 600;">sudo -weight: 500;">service nginx -weight: 500;">restart #!/bin/bash cd /home/ubuntu AWS_ACCOUNT_ID="<aws_account_id>" AWS_REGION="<aws_region>" IMAGE_TAG="latest" APP_NAME="my-app" PORT="8000" # Pull latest image from ECR aws ecr get-login-password --region $AWS_REGION | -weight: 500;">docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com -weight: 500;">docker pull $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$APP_NAME:$IMAGE_TAG # Refresh .env from Parameter Store aws ssm get-parameters-by-path \ --path "/${APP_NAME}" --recursive --with-decrypt \ | jq -r '.Parameters[] | (.Name | split("/")[-1]) + "=" + (.Value)' \ | tee /home/ubuntu/.env # Stop existing containers and clean up -weight: 500;">docker -weight: 500;">stop $(-weight: 500;">docker ps -q) -weight: 500;">docker system prune -f # Start the updated container -weight: 500;">docker run --env-file .env -p $PORT:$PORT -d $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$APP_NAME:$IMAGE_TAG # Restart nginx -weight: 600;">sudo nginx -t -weight: 600;">sudo -weight: 500;">service nginx -weight: 500;">restart version: 0.0 os: linux files: - source: / destination: /home/ubuntu/my-app overwrite: true hooks: ApplicationStart: - location: aws-scripts/application_start.sh timeout: 3000 runas: ubuntu file_exists_behavior: OVERWRITE version: 0.0 os: linux files: - source: / destination: /home/ubuntu/my-app overwrite: true hooks: ApplicationStart: - location: aws-scripts/application_start.sh timeout: 3000 runas: ubuntu file_exists_behavior: OVERWRITE version: 0.0 os: linux files: - source: / destination: /home/ubuntu/my-app overwrite: true hooks: ApplicationStart: - location: aws-scripts/application_start.sh timeout: 3000 runas: ubuntu file_exists_behavior: OVERWRITE - Part 1 - Dockerize and deploy your Node.js app with env on AWS EC2 - Part 2 - Auto scale and load balance your Dockerized app on AWS - Trigger AWS CodeBuild to build a new Docker image and push it to ECR - Trigger AWS CodeDeploy via AWS CodePipeline to deploy the new image to every running EC2 instance - Defines the AWS account variables and ECR repository details - Reads environment variables from Parameter Store (the same ones from Part 1) - Logs in to ECR, builds the Docker image and pushes the new image - Under Trusted entity type, select AWS -weight: 500;">service - Under Use case, select CodeBuild - Add these permission policies: AmazonEC2ContainerRegistryFullAccess, AmazonSSMFullAccess - Name the role my-app-code-build-role - Click Create role - Project name: my-app-code-build, Project type: Default - Source: Choose GitHub, connect your GitHub account and select your repository - Environment: On-demand, Managed image, EC2, Container - Operating system: Ubuntu, Standard runtime - Service role: Choose the my-app-code-build-role we just created - Buildspec: Choose Use a buildspec file — this picks up the buildspec.yml from the repository - Artifacts: No artifacts — we only need to push to ECR - Click Create project - Under Trusted entity type, select AWS -weight: 500;">service - Under Use case, select CodeDeploy - Keep the default AWSCodeDeployRole permission that is pre-selected - Name the role my-app-code-deploy-role - Click Create role - AmazonEC2ContainerRegistryFullAccess — to pull images from ECR - AmazonS3FullAccess — to access pipeline artifacts from S3 - AutoScalingFullAccess — to interact with the Auto Scaling Group - AWSCodePipeline_FullAccess — to work within the pipeline - Application name: my-app-code-deployment-application - Compute platform: EC2/On-premises - Deployment group name: my-app-deployment-group - Service role: Choose my-app-code-deploy-role - Environment configuration: Select Amazon EC2 Auto Scaling groups and choose my-app-asg (the Auto Scaling Group from Part 2) - Deployment settings: CodeDeployDefault.AllAtOnce - Load balancer: Uncheck Enable load balancing - Creation options: Build custom pipeline - Pipeline name: my-app-code-pipeline, Execution mode: Queued - Service role: Create a new -weight: 500;">service role — no need to reuse an existing one. Click next. - Source: Choose GitHub (via GitHub App), select your repository and main branch - Under Webhook events, select Start your pipeline on push or pull request event. Under Webhook event filters, set Event type to Push, Filter type to Branch and value to main. This ensures only pushes to main fire the pipeline. Click next. - Build stage: Under Other build providers, choose AWS CodeBuild and select my-app-code-build. No environment variables needed — they come from Parameter Store. Set artifacts to No artifacts. Click next. - Test stage: Skip this stage. - Deploy stage: Choose AWS CodeDeploy. Select my-app-code-deployment-application as the application and my-app-deployment-group as the deployment group. Click next. - Review everything and click Create pipeline. - Push a code change to the main branch on GitHub - Watch CodeBuild pick up the push, build the Docker image and push it to ECR - Watch CodeDeploy roll out the new image to your running EC2 instances - Visit your load balancer DNS (my-app-lb-xxxxxxxx.<aws_region>.elb.amazonaws.com) to see the updated application - Part 1: Dockerized the app, pushed it to ECR and deployed it to a single EC2 instance with environment variables managed in Parameter Store - Part 2: Created a base AMI, a launch template, an Application Load Balancer and an Auto Scaling Group so the app scales automatically - Part 3: Set up a full CI/CD pipeline — every push to main now automatically builds a new Docker image and deploys it to every running instance