Template Catalog
L4RegistryAdvanced

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

GitHub Actions
OIDC → IAM role
Amazon ECR
EC2 instance
Docker Compose

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.

  1. 1Push to main triggers the build job.
  2. 2Actions assumes an IAM role via OIDC (no stored AWS keys).
  3. 3It builds, tags by commit SHA, and pushes the image to ECR.
  4. 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

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.

.github/workflows/deploy.yml
yaml
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 -d

Step-by-step guide

  1. 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/web

    Or manage it in Terraform alongside your other infra.

  2. 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. 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. 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/web

    Lists the immutable tags you've pushed.

AI insight

Ask the assistant to explain, review, or recommend — authored for this template.

AI insightAuthored

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

    Info

    OIDC issues short-lived credentials per run — there are no static AWS secrets to leak.

  • Immutable, auditable artifacts

    Info

    Each image is tagged by commit SHA, so you know exactly what's running and can roll back.

  • Enable ECR scan-on-push

    Low

    ECR 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

    Low

    Hardcoding 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

Low cost~$10/mo~$0.01/hr if left running · free-tier eligible
  • 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. 1

    Add an ECR lifecycle policy to expire old images automatically.

  2. 2

    Delete the ECR repository if you're done.

    Billing
    aws ecr delete-repository --repository-name devops-launchpad/web --force
  3. 3

    Terminate the EC2 instance and remove the OIDC IAM role.

Troubleshooting

Where to go next