AWS Builder Center

Automating EC2 Deployments with GitHub Actions and Docker

A practical guide to building a CI/CD pipeline that builds a Docker image on every push and deploys it to an AWS EC2 instance automatically using GitHub Actions.

Introduction

Manually SSHing into a server to pull code and restart a service works fine until you forget a step, or you're deploying at 11 PM before a deadline. This article builds a CI/CD pipeline that, on every push to main, builds a Docker image, pushes it to Docker Hub, and deploys it to an EC2 instance automatically using GitHub Actions.
By the end you will have:
  • A Dockerized app
  • A GitHub Actions workflow that builds and pushes an image on every push
  • An EC2 instance that pulls and runs the latest image automatically
  • Secrets handled correctly, never hardcoded

Architecture

1
2
3
4
git push -> GitHub Actions -> build image -> push to Docker Hub
|
v
EC2 pulls image and restarts container

Step 1: Dockerize the app

A minimal Dockerfile for a Python app:
1
2
3
4
5
6
7
8
9
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
Test it locally:
1
2
docker build -t myapp:local .
docker run -p 8000:8000 myapp:local

Step 2: Prepare the EC2 instance

  1. Launch an Ubuntu EC2 instance with inbound rules: SSH (22) from your IP only, and your app's port (e.g. 8000 or 80) open as needed.
  2. Install Docker:
1
2
3
4
5
6
ssh -i my-key.pem ubuntu@<EC2_PUBLIC_IP>

sudo apt update
sudo apt install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker ubuntu
Log out and back in for the group change to apply.

Step 3: Create a Docker Hub repository

Push one image manually first to confirm it works:
1
2
3
docker login
docker tag myapp:local <dockerhub-username>/myapp:latest
docker push <dockerhub-username>/myapp:latest

Step 4: Store secrets in GitHub

In your repo, go to Settings → Secrets and variables → Actions and add:
SecretValue
DOCKERHUB_USERNAMEyour Docker Hub username
DOCKERHUB_TOKENa Docker Hub access token
EC2_HOSTyour EC2 public IP
EC2_SSH_KEYcontents of your .pem private key
Never commit any of these to the repository.

Step 5: Write the GitHub Actions workflow

Create .github/workflows/deploy.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
name: Build and Deploy

on:
push:
branches: [main]

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest

deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Deploy to EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
script: |
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest
docker stop myapp || true
docker rm myapp || true
docker run -d --name myapp -p 8000:8000 \
--restart unless-stopped \
${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest
The build-and-push job builds the image. The deploy job only runs after it succeeds, SSHes into EC2, pulls the new image, and restarts the container. --restart unless-stopped means the container survives instance reboots too.

Step 6: Push and watch it run

1
2
3
git add .
git commit -m "Add CI/CD pipeline"
git push origin main
Open the Actions tab in GitHub to watch the build and deploy steps run. Once it finishes:
1
curl http://<EC2_PUBLIC_IP>:8000/health

Troubleshooting

SymptomWhere to look
SSH step fails in ActionsCheck EC2_SSH_KEY is the full private key including header/footer lines, and the security group allows SSH from GitHub's IP ranges or is open to the key-based connection
Image pulls but container won't startdocker logs myapp on the EC2 instance
Push to Docker Hub failsConfirm DOCKERHUB_TOKEN has write access, not just read
Old container keeps runningCheck the
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article