GitHub Actions + ECR + Docker Compose
Build once, push an immutable image, pull to deploy.
Overview
A registry changes everything. Instead of building on the server, CI builds the image once, tags it by commit, and pushes it to Amazon ECR. The instance simply pulls that exact image and runs it with Docker Compose. Now you have versioned, auditable, rollback-able artifacts — the way real teams ship.
Architecture
CI builds and pushes a commit-tagged image to ECR using short-lived OIDC credentials. The instance pulls that image and runs it via Docker Compose — building and running are now fully separated.
- 1Push to main triggers the build job.
- 2Actions assumes an IAM role via OIDC (no stored AWS keys).
- 3It builds, tags by commit SHA, and pushes the image to ECR.
- 4The instance pulls the tag and `docker compose up -d` runs it.
GitHub Actions authenticates to AWS via OIDC, builds and pushes a tagged image to Amazon ECR; the EC2 instance pulls the image and runs it with Docker Compose.
What you'll understand
- Understand why immutable, registry-stored images beat building on the server.
- Authenticate to AWS from GitHub Actions using OIDC — no long-lived keys.
- Push and pull tagged images with Amazon ECR.
- Run a multi-container app declaratively with Docker Compose.
Prerequisites
- Completed Level 3 (Actions → EC2)
- An ECR repository (can be created with Terraform)
- An IAM role GitHub can assume via OIDC
- Docker Compose available on the instance
Generated files
The files this template produces. Copy any of them straight into your project.
2 files
Builds, pushes to ECR via OIDC, then triggers a pull-and-run on the instance.
name: Build & Deploy (ECR)
on:
push:
branches: [main]
permissions:
id-token: write # required for OIDC
contents: read
env:
AWS_REGION: ap-southeast-1
ECR_REPO: devops-launchpad/web
jobs:
build-push:
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Log in to ECR
id: login
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, push
id: meta
run: |
REG=${{ steps.login.outputs.registry }}
IMAGE=$REG/${{ env.ECR_REPO }}:${{ github.sha }}
docker build -t "$IMAGE" .
docker push "$IMAGE"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
deploy:
needs: build-push
runs-on: ubuntu-latest
steps:
- name: Pull & run on the instance
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.EC2_HOST }}
username: ec2-user
key: ${{ secrets.EC2_SSH_KEY }}
envs: IMAGE
script: |
aws ecr get-login-password --region ${{ env.AWS_REGION }} \
| docker login --username AWS --password-stdin "${IMAGE%%/*}"
IMAGE="${{ needs.build-push.outputs.image }}" docker compose pull
IMAGE="${{ needs.build-push.outputs.image }}" docker compose up -dStep-by-step guide
- 1
Create an ECR repository
Make a private ECR repo for your image. With Terraform that's a single aws_ecr_repository resource.
aws ecr create-repository --repository-name devops-launchpad/webOr manage it in Terraform alongside your other infra.
- 2
Set up OIDC trust
Create an IAM role GitHub can assume via OIDC, scoped to your repo. This replaces long-lived AWS access keys with short-lived tokens.
OIDC is the single biggest security win at this level — no static cloud keys in GitHub.
- 3
Add the workflow and compose file
Commit deploy.yml and docker-compose.yml. The build job pushes to ECR; the deploy job pulls and runs.
- 4
Push and verify the image in ECR
After the run, check the ECR console — you'll see an image tagged with the commit SHA. That tag is your rollback target.
aws ecr list-images --repository-name devops-launchpad/webLists the immutable tags you've pushed.
AI insight
Ask the assistant to explain, review, or recommend — authored for this template.
What this pipeline is doing
Build and run are now fully separated. CI authenticates to AWS via OIDC, builds the image once, tags it by commit SHA, and pushes to ECR. The instance just pulls that tag and runs it with Docker Compose.
- —OIDC = short-lived AWS creds, no stored keys.
- —ECR = your private, versioned image registry.
- —Compose = declarative run on the instance.
Security notes
No long-lived AWS keys
InfoOIDC issues short-lived credentials per run — there are no static AWS secrets to leak.
Immutable, auditable artifacts
InfoEach image is tagged by commit SHA, so you know exactly what's running and can roll back.
Enable ECR scan-on-push
LowECR can scan images for CVEs automatically, but it's off unless you enable it.
Turn on scan-on-push; Level 5 adds Trivy scanning in the pipeline too.
Keep secrets out of compose
LowHardcoding secrets in docker-compose.yml ends up in your repo history.
Use an env file injected at deploy time, or a secrets manager.
Cost notes
Amazon ECR storage
First 500 MB/month free for 12 months, then ~$0.10/GB-month. Old tags add up.
Data transfer (pull to EC2)
Pulling within the same region is free; cross-region or egress can cost.
Untagged image sprawl
Without a lifecycle policy, every commit leaves an image behind forever.
Cleanup guide
Tear it down when you're done — the fastest way to avoid a surprise bill.
- 1
Add an ECR lifecycle policy to expire old images automatically.
- 2
Delete the ECR repository if you're done.
Billingaws ecr delete-repository --repository-name devops-launchpad/web --force - 3
Terminate the EC2 instance and remove the OIDC IAM role.