DEV Community

Cover image for Auto Deploy Docker Compose Apps to AWS EC2 using GitHub Actions (Production-Style Guide)
Gravox
Gravox

Posted on

Auto Deploy Docker Compose Apps to AWS EC2 using GitHub Actions (Production-Style Guide)


Uploading image

Manual deployment quickly becomes unsustainable once infrastructure complexity increases.

Repeated SSH sessions, manual git pull, rebuilding containers, restarting services, and validating application health introduce operational inconsistency and deployment risk.

This walkthrough documents a reproducible CI/CD deployment pipeline where every push to GitHub automatically rebuilds and redeploys Docker Compose services on AWS EC2.

The final deployment architecture consisted of:

GitHub Repository


GitHub Actions


SSH into AWS EC2


Docker Compose Orchestration

┌──────┼────────┐
▼ ▼ ▼
Nginx Flask PostgreSQL


Public Endpoint (:8080)

The implementation surfaced multiple real-world infrastructure issues:

workflow detection failures

missing YAML assumptions

Docker Compose context problems

healthy containers with inaccessible public services

AWS networking restrictions

Rather than omitting failures, this guide documents the actual debugging process and resolutions.


Prerequisites

Before starting, provision the following:

Infrastructure

AWS EC2 instance (Ubuntu 22.04 LTS)

GitHub repository

Docker

Docker Compose

SSH access

Skills

Basic familiarity with:

Linux terminal

Git

Docker

GitHub Actions


Step 1 — Provision AWS EC2

Launch an Ubuntu EC2 instance.

Recommended configuration:

Setting Value

OS Ubuntu 22.04 LTS
Instance t2.micro (lab)
Storage 20GB
Security Group SSH + App Port


Validate SSH Connectivity

Before touching CI/CD, validate server accessibility.

From local terminal:

nc -zv 22

Expected result:

Connection to port 22 succeeded

Then SSH into the instance:

ssh ubuntu@

Successful connection should display Ubuntu system information.


Verify SSH Service

Once connected:

sudo systemctl status ssh

Expected output:

Active: active (running)

This confirms remote deployment automation will be possible.


Step 2 — Configure Repository Structure

Repository structure matters for GitHub Actions detection.

Final structure:

devops-lab/

├── .github/
│ └── workflows/
│ └── compose-rebuild-test.yml

└── compose-rebuild-test/
├── app/
├── nginx/
├── docker-compose.yml
└── test.txt

A common failure occurs when workflow files are placed inside nested folders.

Incorrect:

project-folder/.github/workflows

Correct:

repository-root/.github/workflows

GitHub only detects workflows at repository root.


Step 3 — Create Docker Compose Stack

The stack consisted of:

Flask application

PostgreSQL database

Nginx reverse proxy


docker-compose.yml

version: "3.9"

services:

flask:
build: ./app
container_name: flask_app

environment:
  POSTGRES_HOST: postgres
  POSTGRES_DB: mydatabase
  POSTGRES_USER: myuser
  POSTGRES_PASSWORD: mypassword

depends_on:
  - postgres

restart: unless-stopped

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
  interval: 10s
  timeout: 3s
  retries: 3
Enter fullscreen mode Exit fullscreen mode

postgres:
image: postgres:15
container_name: postgres_db

environment:
  POSTGRES_DB: mydatabase
  POSTGRES_USER: myuser
  POSTGRES_PASSWORD: mypassword

volumes:
  - postgres_data:/var/lib/postgresql/data

restart: unless-stopped

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U myuser -d mydatabase"]
  interval: 10s
  timeout: 5s
  retries: 5
Enter fullscreen mode Exit fullscreen mode

nginx:
image: nginx:latest
container_name: nginx_server

ports:
  - "8080:80"

volumes:
  - ./nginx/default.conf:/etc/nginx/conf.d/default.conf

depends_on:
  - flask

restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

volumes:
postgres_data:


Why This Architecture?

Flask

Application runtime.

Responsibilities:

business logic

database communication

API handling

Health checks ensure unhealthy containers are detected:

healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]


PostgreSQL

Persistent storage layer.

Volume mounting ensures state persistence:

volumes:

  • postgres_data:/var/lib/postgresql/data

This prevents data loss during redeployments.


Nginx

Acts as public ingress layer.

Traffic flow:

Internet


Nginx (:8080)


Flask (:5000)


PostgreSQL (:5432)


Step 4 — Configure GitHub Secrets

GitHub Actions requires secure authentication to EC2.

Navigate to:

Repository Settings
→ Secrets and Variables
→ Actions

Add:

Secret Purpose

EC2_HOST EC2 Public IP
EC2_USER ubuntu
EC2_SSH_KEY private SSH key


Step 5 — Create GitHub Actions Workflow

Create:

.github/workflows/compose-rebuild-test.yml

Add:

name: Compose Rebuild Test Deploy

on:
push:
branches:
- main
paths:
- 'compose-rebuild-test/**'

jobs:
deploy:
runs-on: ubuntu-latest

steps:
  - name: Checkout repo
    uses: actions/checkout@v4

  - name: Deploy compose-rebuild-test
    uses: appleboy/ssh-action@v1.0.3
    with:
      host: ${{ secrets.EC2_HOST }}
      username: ${{ secrets.EC2_USER }}
      key: ${{ secrets.EC2_SSH_KEY }}
      script_stop: true
      timeout: 60s
      command_timeout: 20m

      script: |
        echo "Connected to EC2"

        cd /home/ubuntu/devops-lab

        git pull origin main

        cd compose-rebuild-test

        docker compose down || true
        docker compose up --build -d

        docker ps
Enter fullscreen mode Exit fullscreen mode

Deployment Behavior

Every push to:

main

that modifies:

compose-rebuild-test/**

automatically triggers:

  1. Workflow execution

  2. SSH into EC2

  3. Repository sync

  4. Compose teardown

  5. Image rebuild

  6. Container restart

Deployment becomes deterministic.


Step 6 — Validate Containers

After workflow execution:

SSH into EC2:

ssh ubuntu@

Check containers:

docker ps -a

Expected:

nginx_server Up
flask_app Healthy
postgres_db Healthy


Step 7 — First Major Failure

At this stage, infrastructure appeared healthy.

Containers:

✅ running

Health checks:

✅ healthy

Application:

❌ inaccessible publicly

This created misleading signals.

Local validation succeeded:

curl http://localhost:8080

Response:

CI/CD Auto Deploy Working

Yet:

http://:8080

failed externally.


Root Cause Analysis

This was not an application issue.

This was not Docker failure.

This was not Nginx misconfiguration.

Root cause:

AWS Security Group ingress restrictions

Port 8080 was blocked.


Step 8 — Fix AWS Security Group

Navigate to:

EC2
→ Security Groups
→ Inbound Rules

Add:

Type Port Source

Custom TCP 8080 0.0.0.0/0

Save changes.

Retry:

http://:8080

Success:

CI/CD Auto Deploy Working

Deployment validated.


Common Errors Encountered

Error 1 — Missing deploy.yml

Attempt:

cat .github/workflows/deploy.yml

Error:

No such file or directory

Cause:

Incorrect workflow assumption.

Actual file:

compose-rebuild-test.yml


Error 2 — Docker Compose Failure

Attempt:

docker compose ps

Error:

no configuration file provided

Cause:

Executed outside compose directory.

Fix:

cd compose-rebuild-test
docker compose ps


Error 3 — Healthy Containers, Broken Access

Cause:

Cloud networking layer.

Fix:

Open security group ingress.


Final Deployment Flow

Final architecture:

git push


GitHub Actions


SSH into EC2


git pull origin main


docker compose down
docker compose up --build -d


Health validation


Public endpoint live


Key Engineering Lessons

Healthy containers do not guarantee reachable services

Application validation must include:

Container
Host
Firewall
Cloud Security Group
Public Access


Repository topology matters

GitHub workflows must exist at:

.github/workflows


Docker Compose is execution-context sensitive

Directory placement matters.


Cloud networking frequently masquerades as application failure

Infrastructure debugging should never stop at container health.


Closing Thoughts

CI/CD implementation rarely fails for a single reason.

The real complexity emerges from interactions between:

cloud networking

orchestration

repository topology

deployment automation

infrastructure permissions

Once validated, the result becomes a reproducible deployment system where every push automatically rebuilds and redeploys production-like workloads.

Top comments (0)