Skip to main content

From Git Push to Production: Building a Real CI/CD Pipeline with GitHub Actions, Docker & AWS

Welcome back to the KpolitX tech blog! In the previous articles, we looked at building applications and working with backend technologies. This time, we are moving one step further into the world of DevOps and cloud deployment.

Building an application is only one part of software development. The more interesting question is: What happens after you push your code to GitHub?

In this project, I built a small Flask application called VaultMart and used it as a practical way to understand how a complete CI/CD pipeline works with GitHub Actions, Docker and AWS.



💡 The goal: Build a workflow where a code change can automatically move through testing, Docker image creation, AWS ECR and deployment to an EC2 server.

The final workflow looks like this:

Code Change
     ↓
Git Push
     ↓
GitHub Actions
     ↓
Pytest
     ↓
Docker Build
     ↓
Amazon ECR
     ↓
AWS Systems Manager
     ↓
Amazon EC2
     ↓
🎬 Live VaultMart

1. What We Are Building

Instead of looking at GitHub Actions, Docker and AWS as completely separate technologies, this project connects them into one workflow.

The application starts as a simple Flask project running locally. From there, it becomes a Docker image, gets pushed to Amazon ECR, and is eventually deployed to an EC2 server.

Developer
    │
    ▼
GitHub Repository
    │
    ▼
GitHub Actions
    │
    ├── Install dependencies
    ├── Run tests
    └── Build Docker image
              │
              ▼
          Amazon ECR
              │
              ▼
       AWS Systems Manager
              │
              ▼
           Amazon EC2
              │
              ▼
       Docker Container
              │
              ▼
       VaultMart Application

2. Get the VaultMart Project

If you want to follow the project yourself, start with the project repository and clone it locally.

bash
git clone <your-github-repository-url>

Move into the project directory:

bash
cd VaultMart

You can use the project as a starting point and follow the remaining steps to understand how the CI/CD pipeline is assembled.

3. Understand the Flask Project

The application is intentionally kept simple. The goal is not to build a huge Flask system but to have a working application that can travel through the complete deployment lifecycle.

text
VaultMart/
│
├── app.py
├── requirements.txt
├── templates/
│   └── index.html
└── tests/
    └── test_app.py

The main files have different responsibilities:

File Purpose
app.py Flask application
requirements.txt Python dependencies
templates/index.html Application frontend
tests/test_app.py Automated tests

4. Install the Python Dependencies

Install the packages defined in the project's dependency file:

bash
pip install -r requirements.txt

One important detail here is that the same requirements.txt file will later be used by both the CI environment and the Docker image.

5. Run VaultMart Locally

Before introducing automation, the application should work correctly on the development machine.

bash
python app.py

The application should be available at:

url
http://localhost:5000

A typical Flask development configuration uses:

python
app.run(host="0.0.0.0", port=5000, debug=True)

Here, 0.0.0.0 allows Flask to listen on available network interfaces, while port 5000 is used by the application.

6. Add Automated Testing with Pytest

Manual testing becomes inefficient as a project grows. One of the first CI/CD principles is therefore simple: test the application automatically.

The test file is located at:

text
tests/
└── test_app.py

Run the test suite locally:

bash
pytest
✓ Why this matters: The same test command will later execute automatically inside GitHub Actions. If the tests fail, the pipeline can stop before an invalid application reaches deployment.

7. Initialize Git

Once the application works locally, initialize a Git repository:

bash
git init

Check the repository:

bash
git status

8. Create a .gitignore File

Local environments contain files that should not be committed to the repository.

A basic Python .gitignore could contain:

gitignore
venv/
__pycache__/
*.pyc
.env

This prevents virtual environments, Python cache files and local environment configuration from accidentally being committed.

9. Create the First Commit

bash
git add .

git commit -m "Initial VaultMart application"

10. Connect the GitHub Repository

Connect the local repository to GitHub:

bash
git remote add origin <repository-url>

git branch -M main

git push -u origin main

The source code is now available on GitHub and ready for automation.

11. Why Docker?

The next challenge is making sure the application can run consistently outside the development machine.

Docker packages the application together with its runtime environment and dependencies:

Application
    +
Python
    +
Dependencies
    +
Configuration
    │
    ▼
Docker Image

This image can then be moved between environments without rebuilding the application from scratch.

12. Create the Dockerfile

Create a file named:

text
Dockerfile

Add the following:

dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

Understanding the Dockerfile

  • FROM provides the Python base image.
  • WORKDIR creates the working directory inside the container.
  • COPY moves project files into the image.
  • RUN installs Python dependencies.
  • EXPOSE documents the application port.
  • CMD starts the Flask application.

13. Build the Docker Image

Build the image locally:

bash
docker build -t VaultMartappimage .

Check the available images:

bash
docker images

14. Run VaultMart Inside Docker

Run the container:

bash
docker run -p 5000:5000 VaultMartappimage

The port mapping means:

Host Port 5000
      │
      ▼
Container Port 5000
      │
      ▼
Flask Application

Open http://localhost:5000 and verify that the application works inside the container.

15. Create the GitHub Actions Workflow

Now we can start automating the process.

Create the following directory:

text
.github/
└── workflows/
    └── ci.yml

16. Configure the Workflow Trigger

The workflow can be triggered whenever code is pushed to the main branch.

yaml
name: VaultMart CI/CD

on:
push:
branches:
- main

17. Build the CI Pipeline

The first part of the workflow is continuous integration. It should validate the code before deployment.

Checkout the Code

yaml
- name: Checkout code
  uses: actions/checkout@v4

Set Up Python

yaml
- name: Set up Python
  uses: actions/setup-python@v5
  with:
    python-version: "3.11"

Install Dependencies

yaml
- name: Install dependencies
  run: pip install -r requirements.txt

Run Pytest

yaml
- name: Run tests
  run: pytest
GitHub Push
     ↓
Checkout
     ↓
Python Setup
     ↓
Install Dependencies
     ↓
Pytest
     ↓
Tests Passed

18. Configure AWS IAM for GitHub Actions

GitHub Actions needs permission to communicate with AWS services such as Amazon ECR and Systems Manager.

For this project, a dedicated IAM identity was used rather than using the AWS root account.

🔐 Security reminder: Never use the AWS root account for routine CI/CD automation. Permissions should be limited to what the workflow actually needs.

Why IAM Matters

IAM determines which AWS resources an identity can access and which operations it is allowed to perform.

GitHub Actions
      ↓
IAM Identity
      ↓
AWS Permissions
      ↓
ECR / SSM / Other Services

19. Create AWS Access Credentials

After creating the CI/CD IAM identity, the required credentials can be configured for the GitHub workflow.

These credentials should never be placed directly inside the workflow YAML file.

20. Store AWS Credentials in GitHub Secrets

GitHub repository secrets can be used to keep sensitive values outside the source code.

Typical values for this project include:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • ECR_REPOSITORY
⚠️ Never commit AWS credentials to Git. Secrets belong in the secret-management system, not in the repository.

21. Create the Amazon ECR Repository

Amazon Elastic Container Registry, or ECR, provides a private registry for storing Docker images.

GitHub Actions
      ↓
Docker Image
      ↓
Amazon ECR
      ↓
VaultMart:latest

Create a repository for the VaultMart container image and keep its URI available for the deployment workflow.

22. Authenticate GitHub Actions with AWS

The workflow uses the AWS credentials stored in GitHub Secrets to authenticate with AWS.

GitHub Actions
      ↓
GitHub Secrets
      ↓
AWS Authentication
      ↓
Amazon ECR

The credentials remain outside the workflow source itself.

23. Authenticate Docker with ECR

After AWS authentication, Docker also needs to authenticate with the ECR registry before it can push an image.

AWS Authentication
        ↓
ECR Login
        ↓
Docker Authenticated
        ↓
Image Can Be Pushed

24. Build the Docker Image in CI

After the tests pass, GitHub Actions builds the production image.

bash
docker build -t VaultMart .

The image is created inside the GitHub Actions runner.

25. Tag the Docker Image

Docker needs to know which ECR repository the image belongs to. Therefore, the local image is tagged with the ECR repository URI.

bash
docker tag VaultMart:latest $ECR_REPOSITORY:latest

26. Push the Image to ECR

Once the image has been tagged, push it to Amazon ECR:

bash
docker push $ECR_REPOSITORY:latest

The complete image publishing flow is:

GitHub Actions
      ↓
Docker Build
      ↓
Docker Image
      ↓
Docker Tag
      ↓
ECR Login
      ↓
Docker Push
      ↓
Amazon ECR

27. Create the EC2 Production Server

The next component is the EC2 instance that will run the live VaultMart container.

AWS
 │
 └── EC2
      │
      └── Linux Server
            │
            └── Docker
                  │
                  └── VaultMart

28. Configure the EC2 Security Group

The EC2 Security Group controls inbound network traffic. For a basic version of this project, the required ports are:

Protocol Port Purpose
TCP 22 Initial SSH administration
TCP 5000 VaultMart application
Production consideration: Do not broadly expose administrative ports when it is unnecessary. Restrict access to trusted sources and use a more appropriate production architecture for public applications.

29. Install Docker on EC2

Connect to the Linux server and install Docker.

bash
sudo apt update

sudo apt install docker.io -y

sudo systemctl enable --now docker

docker --version

30. Create an IAM Role for EC2

The EC2 server also needs permission to communicate with AWS. Instead of storing long-term AWS access keys on the server, an IAM Role can be attached directly to the instance.

This is different from the IAM identity used by GitHub Actions.

GitHub Actions
      ↓
IAM Identity
      ↓
AWS Services

EC2
↓
IAM Role
↓
AWS Services 

31. EC2 IAM Permissions

The project uses AWS-managed policies for the services required by the EC2 deployment environment.

Policy Purpose
AmazonEC2ContainerRegistryReadOnly Allows EC2 to retrieve container images from ECR
AmazonSSMManagedInstanceCore Allows Systems Manager to communicate with the instance
CloudWatchAgentServerPolicy Supports CloudWatch monitoring and log collection

32. Attach the IAM Role to EC2

In the AWS Console, the instance IAM role can be associated with the EC2 instance through its security settings.

EC2
 ↓
Instance
 ↓
Security
 ↓
Modify IAM Role
 ↓
Select VaultMart Role

Once attached, EC2 can communicate with the required AWS services without storing long-term AWS access keys directly on the server.

33. Configure AWS Systems Manager

AWS Systems Manager, commonly called SSM, can be used to execute commands remotely on the EC2 instance.

This means the deployment pipeline does not need to manually connect to the EC2 server through SSH for every application update.

GitHub Actions
      ↓
AWS Systems Manager
      ↓
EC2
      ↓
Deployment Commands

After configuring the IAM role, verify that the EC2 instance appears as a managed node in Systems Manager and reports an online state.

34. Retrieve the Image on EC2

The EC2 server now needs to retrieve the image produced by the CI/CD pipeline.

Amazon ECR
    ↓
ECR Authentication
    ↓
Docker Pull
    ↓
Latest VaultMart Image

The important concept is that the server is using the same container image that was built and published by the CI/CD pipeline.

35. Run the VaultMart Container

The updated image can now be used to run the application.

bash
docker run -d \
  --name VaultMart-container \
  -p 5000:5000 \
  <ECR-REPOSITORY>:latest

The application uses port 5000, with the container mapping represented as:

text
5000:5000

36. Connect Everything Together

At this point, the individual technologies are no longer isolated. They form a complete delivery pipeline.

Developer
    │
    │ Code Change
    ▼
GitHub
    │
    │ Push
    ▼
GitHub Actions
    │
    ├── Checkout
    ├── Python Setup
    ├── Install Dependencies
    ├── Pytest
    └── Docker Build
              │
              ▼
          Amazon ECR
              │
              │ Docker Image
              ▼
       AWS Systems Manager
              │
              │ Remote Command
              ▼
           Amazon EC2
              │
              ├── ECR Authentication
              ├── Docker Pull
              └── Updated Container
                        │
                        ▼
                 🎬 Live VaultMart

37. Test the Complete Automation

Now comes the most important practical test: make a change to the application and see whether the pipeline handles it automatically.

Make a Code Change

For example, update a piece of text in the VaultMart interface:

html
<p>🚀 Latest CI/CD Deployment Test</p>

Commit the Change

bash
git add .

git commit -m "Update VaultMart"

Push to GitHub

bash
git push origin main

That single push should trigger the GitHub Actions workflow.

38. Watch GitHub Actions

Open the repository on GitHub and navigate to:

GitHub Repository
      ↓
Actions
      ↓
VaultMart Workflow

The pipeline should move through stages similar to:

Checkout
   ↓
Install Dependencies
   ↓
Pytest
   ↓
Docker Build
   ↓
AWS Authentication
   ↓
ECR Push
   ↓
SSM Deployment

39. Verify the Deployment

After the workflow finishes successfully, the updated Docker image should be available in ECR and the deployment command should have been executed on the EC2 instance.

The EC2 server then runs the updated VaultMart container.

Code Change
     ↓
Git Push
     ↓
Automated Pipeline
     ↓
Updated Docker Image
     ↓
EC2 Deployment
     ↓
Updated Container
     ↓
Live Application
🚀 This is the key result: the deployment process becomes repeatable instead of manually rebuilding and deploying the application after every change.

40. Final Architecture

The entire project can be summarized with the following architecture:

┌──────────────────────────┐
│       Developer          │
│                          │
│       Git Push           │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│         GitHub           │
│      Source Code         │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│     GitHub Actions       │
│                          │
│  • Checkout              │
│  • Test                  │
│  • Docker Build          │
│  • AWS Authentication    │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│       Amazon ECR         │
│      Docker Image        │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│    AWS Systems Manager   │
│     Remote Commands      │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│         AWS EC2          │
│                          │
│         Docker           │
│            ↓             │
│    VaultMart Container   │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│     🎬 Live VaultMart    │
└──────────────────────────┘

41. How IAM Fits Into the Architecture

One of the most useful concepts from this project is that there are different AWS access paths for GitHub Actions and EC2.

GitHub Actions → AWS

GitHub Actions
      ↓
GitHub Secrets
      ↓
IAM Identity
      ↓
AWS Services

EC2 → AWS

EC2
 ↓
IAM Role
 ↓
AWS Services

Keeping these access paths separate makes the architecture easier to reason about and avoids placing long-term AWS credentials directly on the application server.

42. What I Learned From This Project

Before working on this project, it was easy to think about DevOps tools individually:

  • Git
  • Docker
  • GitHub Actions
  • AWS
  • EC2

The more valuable part was understanding how they connect.

  • Git manages code changes.
  • GitHub hosts the source code.
  • Pytest validates the application.
  • Docker packages the application.
  • GitHub Actions automates the workflow.
  • GitHub Secrets protect workflow credentials.
  • IAM controls AWS permissions.
  • ECR stores Docker images.
  • EC2 provides the compute environment.
  • SSM enables remote deployment commands.
  • Docker on EC2 runs the application.

43. The Biggest Takeaway

The most important thing I took from this project was not memorizing another Docker command or another YAML configuration.

It was understanding the complete lifecycle of an application:

Write Code
    ↓
Test Code
    ↓
Commit Code
    ↓
Push Code
    ↓
Run CI
    ↓
Build Docker Image
    ↓
Store Image
    ↓
Deploy Image
    ↓
Run Container
    ↓
Live Application

A simple:

bash
git push

can become the starting point for an automated journey from source code to a running application.

44. Final Thoughts

VaultMart started as a simple Flask application, but the real learning came from everything around it.

By connecting local development, Git, automated testing, Docker, GitHub Actions, AWS IAM, ECR, EC2 and Systems Manager, the project became a practical introduction to the DevOps lifecycle.

The goal was not simply to deploy a Flask application. The goal was to understand how software can move from code to production through an automated and repeatable process.

🚀 Final workflow: Code → Test → Commit → Push → CI → Docker → ECR → SSM → EC2 → Live Application

45. Explore the Project

The complete source code and CI/CD implementation can be made available through the project's GitHub repository.

👉 GitHub Repository: VaultMart-devops-cicd

If you are learning DevOps, try rebuilding the pipeline yourself instead of only reading through the commands. The real understanding comes from seeing each component work together.

Comments

Popular posts from this blog

How to Set Up .NET on Your PC: A Complete Beginner's Guid

Completely new to .NET? This beginner-friendly guide shows you how to install .NET, set up Visual Studio, configure your development environment, create your first C# project, understand the project structure, run your application, and take your first steps into ASP.NET Core development. Introduction If you've never worked with .NET before, getting started can feel confusing. You may see terms such as C# , .NET SDK , ASP.NET Core , Visual Studio , NuGet , and CLI everywhere. So instead of jumping directly into a complicated application, this guide starts from the beginning. We will set up a complete .NET development environment on a Windows PC, create our first C# application, understand the basic project structure, run the application, and finally create a simple ASP.NET Core Web API. You don't need previous .NET experience to follow this...

Setting Up the Ultimate Development Environment: From Zero to Code with KpolitX

 Welcome to the official KpolitX tech blog! Today, we are kicking things off by pulling back the curtain on how to set up a robust, efficient, and multi-language development environment from scratch. Whether you're working with systems languages like Go and Rust, or building full-stack web applications, having a clean workspace is the foundation of writing great code. In this guide, we will walk through setting up a professional developer workstation step by step. Step 1: Choosing and Configuring Your Editor Every good developer environment starts with a powerful code editor. Visual Studio Code (VS Code): Highly extensible and lightweight. Essential Extensions: GitLens for advanced version control tracking. Language-specific extensions (like the official Go and Rust Analyzer extensions). Docker extension to manage containers directly from your editor sidebar. Step 2: Setting Up the Toolchain (Go & Rust) To handle modern multi-language and systems programming tasks, you ne...

How to Build Your Personal Agentic System Using Ollama: A Step-by-Step Guide

Build your own private AI assistant that can plan tasks, use tools, work with files, maintain memory, and operate under controlled guardrails using Ollama and Python. Most people use AI as a chatbot: ask a question, receive an answer, and repeat. A personal agentic system is different. It can understand a goal, break it into steps, use tools, remember useful context, and produce a useful result while running largely on your own computer. In this guide, you'll build a simple but extensible personal AI agent using Ollama and Python . By the end, it will be able to: Accept a task in natural language Plan the steps needed Read and write local files Search notes or documents Call tools safely Keep a record of what it did Improve through feedback 💡 The key idea: An agent is not just a model; it is a model plus tools, memory, planning, and gu...