From Git Push to AWS Deployment: My Docker CI/CD Pipeline for the 2048 Game

Tech stack: Docker, AWS CodePipeline, AWS CodeBuild, Amazon ECR, Amazon ECS Fargate, IAM, Amazon S3, GitHub

Project repository: aws-ecs-ci-cd-2048-game

Introduction

For a while, my Docker experience stopped at my laptop. I could build an image and run a container locally, but I wanted to understand what happens when a team needs to ship changes automatically.

So I built a CI/CD pipeline on AWS where a GitHub push triggers a build, publishes a Docker image, and deploys the updated application to Amazon ECS Fargate.

I used the open-source 2048 game because the application itself is relatively simple. That let me focus on the infrastructure, the deployment process, and the problems that came up along the way.

The goal: push code, let the pipeline do the work, and verify that the change is live without manually rebuilding or deploying the application.

Credit: I built this project while taking project-based cloud course by Lucy Wang. The course supplied the project brief and starter files. This post isn't a walkthrough. It's my field notes on what went wrong, what I changed, and what I learned. If you want the guided version, I recommend the course.

The Shape of the System

GitHub Push | v AWS CodePipeline | v AWS CodeBuild | | Build and tag Docker image v Amazon ECR | | Pull image and deploy v Amazon ECS Fargate | v Browser 

AWS CI/CD pipeline architecture

Here's what each service does:

Service Responsibility
GitHub Stores the application source code and triggers the workflow when changes are pushed.
AWS CodePipeline Orchestrates the source, build, and deployment stages.
AWS CodeBuild Builds the Docker image and prepares the deployment configuration.
Amazon ECR Stores the versioned Docker images in a private container registry.
Amazon ECS Fargate Runs the application container without requiring me to manage the underlying servers.
IAM Controls which AWS resources and actions the pipeline can access.
Amazon S3 Supports storage of pipeline artifacts.

I built and tested the project on Windows using PowerShell, with the AWS resources deployed in eu-north-1.

The architecture is straightforward, but getting all the pieces to work together taught me quite a bit.

What Was Given vs. What I Added

The course provided the project brief, starter application, and sample build configuration. I made several changes to improve the configuration and understand how the deployment process worked.

1. Removed hardcoded AWS account details

The original build configuration included my AWS account ID and region directly in the file.

That wasn't something I wanted to leave in a public repository. I changed the configuration to retrieve the account ID during the build and construct the ECR registry URI dynamically.

For example, these commands can be used in a CodeBuild pre_build phase:

pre_build: commands: - ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) - REGISTRY=$ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com 

The account ID now comes from the AWS identity used by the build environment rather than being hardcoded in the repository.

This also made the configuration easier to reuse across environments.

2. Added commit-based Docker image tags

Initially, the latest tag seemed sufficient. But there's a problem with relying on it alone: the tag can point to a different image after every build.

If something breaks after deployment, I want to know exactly which version of the source code produced the image.

I added a tag derived from the Git commit hash:

- IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION:-local} | cut -c 1-7) 

For Git sources where CodeBuild exposes the commit hash, this produces a short identifier that connects the image to a specific revision.

For example, an image might be tagged with a value such as a1b2c3d.

Now, instead of relying only on a moving latest tag, I can identify a build by its source commit. With the corresponding image retained in ECR and the deployment configuration referencing that tag, I can redeploy an earlier image if necessary.

3. Simplified the image URI used for deployment

I also changed how the deployment configuration was assembled.

The sample configuration combined parts of the image address in multiple places, making it easier to accidentally duplicate the repository name.

I used a single variable for the full image URI and reused it when generating the deployment file.

This made the build configuration easier to follow and reduced the chance of deploying an incorrectly constructed image address.

4. Kept a troubleshooting log

I documented the errors I encountered instead of treating them as isolated annoyances.

That turned out to be one of the most useful parts of the project. Several failures had nothing to do with the application itself.

The Errors That Taught Me the Most

1. Docker couldn't reach its engine

The first error looked like this:

ERROR: failed to connect to the docker API at npipe:////./pipe/dockerDesktopLinuxEngine 

My first instinct was that something was wrong with Docker.

It turned out that Docker Desktop wasn't running.

The docker command is a client that communicates with the Docker engine. Having the CLI available doesn't mean the engine is running.

I started Docker Desktop and verified the connection:

docker info 

Once the engine was available, I could continue building and running containers.

Lesson: Check whether the underlying service is running before troubleshooting the commands that depend on it.

2. PowerShell didn't recognize aws

Next, PowerShell returned:

aws : The term 'aws' is not recognized as the name of a cmdlet... 

The AWS CLI was either not installed correctly or wasn't available in the terminal's current environment.

After installing AWS CLI v2, I reopened PowerShell and checked the installation:

aws --version 

Reopening the terminal ensured it picked up the updated environment.

Lesson: A tool can be installed on your machine without being available in a terminal session that was already open.

3. AWS didn't know who I was

The next error was:

NoCredentials: Unable to locate credentials 

The AWS CLI was installed, but the local environment didn't have credentials configured for the commands I was running.

For this learning project, I configured an IAM user's access key using:

aws configure 

Then I verified which AWS identity the CLI was using:

aws sts get-caller-identity 

That command became one of my first checks whenever I started working on a new AWS project.

For a local learning environment, credentials need to be configured securely. Access keys should never be committed to Git or exposed in screenshots, and the identity should have only the permissions it needs. For longer-term development, temporary credentials through an appropriate role or IAM Identity Center are preferable where available.

Lesson: Before troubleshooting an AWS permission error, confirm which identity is actually making the request.

4. The wall inside the pipeline

This was the most frustrating error because it happened after I had connected the AWS services and configured the pipeline.

When the pipeline reached the build stage, it failed before any of my build commands ran:

Error calling startBuild: Cannot have more than 0 builds in queue for the account (Error Code: AccountLimitExceededException) 

At first, I wondered whether my build configuration or IAM permissions were wrong.

But the error pointed to something different: an account-level CodeBuild limit.

My account's applicable build quota was zero, so CodeBuild couldn't accept the build request. This wasn't an application bug or a Dockerfile problem.

How to approach this error: Check the relevant AWS CodeBuild quotas in Service Quotas and determine whether the account needs an eligible quota increase. Once the account limit has been addressed, retry the pipeline and inspect the next failure, if any.

This was a useful reminder that an AWS account can have service limits that prevent a technically correct configuration from running.

Lesson: Read the error type before changing your code. An AccountLimitExceededException points you toward an account or service quota, not automatically toward your build file.

Decisions I Can Explain

These are the choices that helped me understand the architecture rather than simply reproduce a set of instructions.

Deploy manually before automating

I deployed the application before relying on the pipeline.

That established a working baseline: the ECS service, task definition, networking, and container configuration needed to be functional before automation could update them.

When a pipeline fails, knowing that the underlying application already works makes debugging much easier.

Keep Nginx in the foreground

The 2048 application is served as a web application through Nginx.

A container needs its main process to remain running. If Nginx starts in daemon mode and exits from the container's perspective, the container can stop even though the application files are present.

For this setup, the Nginx process needs to remain in the foreground, commonly using:

CMD ["nginx", "-g", "daemon off;"] 

This allows the container runtime to monitor the main process properly.

Enable privileged mode for Docker builds in CodeBuild

My CodeBuild environment needed to build Docker images.

Running Docker builds inside CodeBuild can require privileged mode so the build environment can access the Docker daemon, depending on the selected build environment and configuration.

My initial symptom was another Docker daemon connection error.

This taught me that a Dockerfile can be perfectly valid while the environment attempting to build it is not configured to support Docker.

Privileged mode also has security implications, so it should be enabled only when needed and combined with appropriate IAM permissions and build isolation.

Match the container names in the deployment configuration

The container name referenced by the deployment configuration needs to match the container name in the ECS task definition.

If the names differ, the deployment action may be unable to identify the container image that needs updating.

It's a small detail, but it illustrates how deployment automation depends on consistent configuration across services.

Choose Fargate over servers I manage

I used ECS Fargate because I wanted to focus on container deployment without managing EC2 instances.

The trade-off is cost: running tasks and associated infrastructure can continue to incur charges. Serverless container management doesn't mean the workload is free.

Proof It Worked

The most satisfying moment was seeing the entire process work without manually intervening in the deployment.

I changed the page title to include my name, committed the change, and pushed it to GitHub.

CodePipeline picked up the change, CodeBuild built a new Docker image, and the deployment process updated the ECS service to run the new version.

When I opened the address for the running task, the updated title was there.

Pipeline execution

AWS CodePipeline showing successful pipeline stages

Updated application

2048 game displaying the updated page title

That was the point where the architecture clicked for me. The change started in GitHub and made its way to the running application through the pipeline.

The screenshots show the successful pipeline execution and the updated page. Together, they provide evidence that the automated deployment worked.

Cleanup: The Part You Shouldn't Skip

After testing the pipeline, I removed the AWS resources to avoid unnecessary ongoing charges.

My cleanup covered the pipeline, ECS service and cluster, CodeBuild project, ECR repository, artifact storage, log groups, IAM roles, GitHub connection, and the local IAM user's access key.

The exact deletion order depends on resource dependencies. Generally, remove or disconnect resources that depend on other resources before deleting those dependencies. For example, stop the ECS service and its tasks before deleting the cluster, and ensure S3 buckets and ECR repositories are empty if deletion requires it.

I also verified the resources I intended to remove rather than assuming that deleting the pipeline automatically removed everything it used.

A pipeline can be temporary. Its costs don't necessarily stop when you stop working on it.

Cleanup is part of building responsibly in the cloud.

What I'd Improve Next

The project works as a learning exercise, but there are several ways I'd make it more production-ready.

  • Add an Application Load Balancer. This would provide a stable entry point instead of relying on a task's public address.
  • Configure HTTPS and a custom domain. This would provide a more appropriate endpoint for a publicly accessible application.
  • Introduce Terraform. Infrastructure as code would make the AWS resources reproducible and simplify setup and teardown.
  • Add an automated test stage. Tests should run before an image is promoted to deployment.
  • Apply least-privilege IAM. The learning setup used broader managed policies than I'd want for production. I'd restrict each role to the actions and resources it actually needs.
  • Improve deployment and rollback controls. I'd make the deployed image version explicit and define a reliable way to return to a previously working version.

These changes would make the project more secure, repeatable, and maintainable.

What I Learned

This project taught me that building a CI/CD pipeline is about more than connecting AWS services.

My biggest takeaways were:

  • Verify the toolchain first. My initial errors were setup problems, not application bugs.
  • Read the error before changing the code. Account limits, missing credentials, and configuration mismatches can look like application failures.
  • Avoid hardcoding account-specific details. Keep configuration reusable and secrets out of public repositories.
  • Test components before connecting them. A working Docker build, a valid ECS deployment, and a functioning pipeline are separate things to verify.
  • Understand what happens after a Git push. Knowing how images are built, stored, tagged, and deployed makes troubleshooting much easier.
  • Treat cleanup as part of the project. AWS resources can continue to cost money after the application is no longer needed.

The biggest shift for me was moving from running containers locally to understanding how a source-code change can become a deployed application through an automated workflow.

That's the kind of practical cloud engineering experience I wanted to gain from this project.

Explore the Project

Skills demonstrated: Docker, Amazon ECR, Amazon ECS Fargate, AWS CodeBuild, AWS CodePipeline, IAM, Amazon S3, VPCs, security groups, Git, and GitHub.