From Manual Deploys to Push-and-Ship: Building a CI/CD Pipeline for a Small Team
Replacing manual SSH deploys with a Docker-based pipeline on GitHub Actions and AWS — and what we learned about rollbacks and environment parity.
Context
When I joined, deployments meant SSH into a server, pull the latest code, run migrations, restart the process. It was error-prone, stressful, and blocked us from shipping frequently. We wanted to move to a pipeline where pushing to main would build, test, and deploy — with the ability to roll back quickly if something broke. The team was small, so the solution had to be simple to maintain.
Constraints
- Limited AWS experience — we needed something we could understand and debug
- Existing app ran in a single environment; we had to introduce staging without doubling infra cost
- Database migrations had to run safely — no downtime, no failed half-migrations
Architecture
We built a pipeline with GitHub Actions, Docker, and AWS. On push to main, Actions builds a Docker image, runs tests, pushes to ECR, and deploys to ECS (or EC2 with Docker, depending on setup). Migrations run as a separate step before the new container goes live — we use a migration job that runs, then the app deployment follows. We keep the last two images tagged so rollback is 'redeploy the previous image.' Staging mirrors production with a smaller instance. We added a simple health check endpoint so the pipeline can verify the app is up before considering the deploy successful.
Alternatives considered
- Use a managed platform (Vercel, Railway) for the whole stack: Our backend is Node.js + PostgreSQL with specific AWS integrations. Moving would have been a larger migration. We needed to improve our current setup first.
- Run migrations inside the app startup: Risky — if migrations fail, the app never starts and we're stuck. Separating migration from app deploy gives us a clear rollback path.
Lessons learned
- Rollback strategy matters before you need it. Tagging the previous image and having a one-command rollback saved us at least once.
- Environment parity is hard. Staging should match production as closely as possible — same Docker image, same migration path.
- Document the deploy process. When something breaks at 2am, you want the runbook written when you're thinking clearly.