Before: The 5-Day Deployment
Early in the project, deploying a release meant: manually building the JAR, SSHing into the EC2 instance, stopping the service, uploading the file, restarting — and repeating for each environment. A single production deploy with testing took the better part of a week.
This is not sustainable.
The Setup
The automated pipeline I settled on has three stages:
1. Build and Test (on every push)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
- name: Build with Maven
run: mvn clean package
- name: Run tests
run: mvn test
2. Docker Build and Push
On merge to main, the workflow builds a Docker image and pushes it to Amazon ECR:
- name: Build Docker image
run: docker build -t my-app:${{ github.sha }} .
- name: Push to ECR
run: |
aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URI
docker push $ECR_URI/my-app:${{ github.sha }}
3. Deploy to EC2
The final step SSH's into EC2, pulls the new image, and does a zero-downtime swap using Docker Compose:
- name: Deploy
run: |
ssh ec2-user@$EC2_HOST "
docker pull $ECR_URI/my-app:${{ github.sha }} &&
docker compose up -d --no-deps app
"
Lessons Learned
- Secrets management: Store all credentials in GitHub Actions secrets, never hardcode. Use AWS IAM roles with the minimum required permissions.
- Health checks: Add a Docker health check so Compose won't route traffic to a container that hasn't finished starting.
- Rollback: Tag images by commit SHA so you can roll back instantly by redeploying the previous tag.
Result
Total deploy time: under 30 minutes, fully automated, with tests as a gate. The mental overhead of deployments dropped to essentially zero.