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 containerStep 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:localStep 2: Prepare the EC2 instance
- 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.
- 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 ubuntuLog 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:latestStep 4: Store secrets in GitHub
In your repo, go to Settings → Secrets and variables → Actions and add:
| Secret | Value |
|---|---|
DOCKERHUB_USERNAME | your Docker Hub username |
DOCKERHUB_TOKEN | a Docker Hub access token |
EC2_HOST | your EC2 public IP |
EC2_SSH_KEY | contents 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:latestThe
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 mainOpen the Actions tab in GitHub to watch the build and deploy steps run. Once it finishes:
1
curl http://<EC2_PUBLIC_IP>:8000/healthTroubleshooting
| Symptom | Where to look |
|---|---|
| SSH step fails in Actions | Check 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 start | docker logs myapp on the EC2 instance |
| Push to Docker Hub fails | Confirm DOCKERHUB_TOKEN has write access, not just read |
| Old container keeps running | Check the |
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article