GitHub Actions Deploy to EC2
Ship on every push instead of deploying by hand.
Overview
Your first real CI/CD pipeline. On every push to main, GitHub Actions builds the Docker image, then connects to the EC2 instance over SSH and redeploys the container. Secrets live in GitHub, not in your shell history. Deployment stops being a thing you do and becomes a thing that happens.
Architecture
A push to main triggers a GitHub Actions workflow. The runner builds the image, copies it (or pulls it) to the instance over SSH, and restarts the container. Credentials come from GitHub Secrets.
- 1You push to the main branch.
- 2The workflow checks out code and builds the Docker image.
- 3Using an SSH key from Secrets, the runner connects to the instance.
- 4It loads/rebuilds the image and restarts the container.
A push to GitHub triggers an Actions runner that builds a Docker image and connects over SSH to an EC2 instance to redeploy the container, using GitHub Secrets for credentials.
What you'll understand
- Understand the anatomy of a CI/CD pipeline: triggers, jobs, steps.
- Store and use secrets safely with GitHub Encrypted Secrets.
- Build a Docker image in CI and deploy it over SSH.
- Recognize the limits of building-on-push and SSH deploys (motivates Level 4).
Prerequisites
- Completed Level 2 (Terraform EC2)
- A GitHub repository for your app
- An SSH key whose public half is on the EC2 instance
- The instance reachable on port 22 from GitHub runners (or a self-hosted runner)
Generated files
The files this template produces. Copy any of them straight into your project.
1 files
Builds the image and deploys to EC2 over SSH on every push to main.
name: Deploy to EC2
on:
push:
branches: [main]
# Least privilege: this workflow only needs to read the repo.
permissions:
contents: read
concurrency:
group: deploy-ec2
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t web:${{ github.sha }} .
- name: Save image to a tarball
run: docker save web:${{ github.sha }} | gzip > web.tar.gz
- name: Copy image to the instance
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ec2-user
key: ${{ secrets.EC2_SSH_KEY }}
source: web.tar.gz
target: /home/ec2-user
- name: Load and restart on the instance
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.EC2_HOST }}
username: ec2-user
key: ${{ secrets.EC2_SSH_KEY }}
script: |
gunzip -f web.tar.gz
docker load -i web.tar
docker stop web || true && docker rm web || true
docker run -d --name web -p 80:80 --restart unless-stopped web:${{ github.sha }}Step-by-step guide
- 1
Add a Dockerfile
Your app needs to build into an image. A minimal, multi-stage Dockerfile keeps the result small and reproducible.
- 2
Create the GitHub Secrets
In repo Settings → Secrets and variables → Actions, add EC2_HOST (the public IP/DNS) and EC2_SSH_KEY (the private key). These are encrypted and never printed in logs.
Use a deploy-only key, not your personal SSH key.
- 3
Commit the workflow
Drop deploy.yml into .github/workflows/. Note the `permissions: contents: read` line — the workflow gets only what it needs.
git add .github/workflows/deploy.yml && git commit -m 'ci: deploy on push' - 4
Push and watch it run
Push to main and open the Actions tab. Watch the build → copy → restart steps. Your first automated deploy.
git push origin main
AI insight
Ask the assistant to explain, review, or recommend — authored for this template.
What this pipeline is doing
A push to main triggers a runner that builds your image, then connects to the instance over SSH and restarts the container. Credentials come from GitHub's encrypted Secrets, not your laptop.
- —Trigger: push to main.
- —Build on the runner, ship to the box over SSH.
- —Secrets are encrypted and masked in logs.
Security notes
Secrets are out of your shell
InfoCredentials live in GitHub Encrypted Secrets and are masked in logs — a real improvement.
Workflow permissions are scoped
InfoSetting `permissions: contents: read` follows least privilege for the GITHUB_TOKEN.
SSH open to GitHub's runner IPs
MediumHosted runners use a wide IP range, so 'restrict SSH' is hard without a self-hosted runner or bastion.
Level 4 removes server-side building; later you can deploy via SSM with no inbound SSH at all.
Image is rebuilt every deploy
LowBuilding in CI and shipping a tarball works but isn't an auditable, versioned artifact.
Level 4 pushes immutable images to ECR instead.
Cost notes
GitHub Actions minutes
Free tier includes 2,000 minutes/month for private repos; public repos are free.
EC2 instance
Unchanged from earlier levels — the pipeline deploys to the same box.
Cleanup guide
Tear it down when you're done — the fastest way to avoid a surprise bill.
- 1
Disable or delete the workflow if you stop using the repo.
- 2
Rotate or remove the deploy SSH key from the instance and Secrets.
- 3
Terminate the EC2 instance (terraform destroy if you're on Level 2).
Billing