Our deployment pipeline took 45 minutes. From the moment a developer merged a PR to the moment the change was live in production, 45 minutes elapsed. That meant we deployed twice a day at most. Bugs took hours to fix. Hotfixes required a developer to babysit the pipeline. The team had learned to deploy only in the morning, leaving the afternoon for "safer" work.
We got it down to 3 minutes. Here is how.
Step 1: Measure where the time goes
Before optimizing, we broke down the 45 minutes:
- Docker image build: 18 minutes
- Test suite: 14 minutes
- Infrastructure provisioning and deployment: 8 minutes
- Health checks and warmup: 5 minutes
The build and tests accounted for 32 of the 45 minutes. That is where we focused.
Step 2: Fix the Docker build (18 min to 2 min)
Our Dockerfile was installing all dependencies from scratch on every build. No layer caching. No multi-stage builds. No .dockerignore. Every npm install re-downloaded 800MB of node_modules.
Fixes applied:
- Layer ordering: Moved COPY package.json and npm install before COPY . so dependencies are only reinstalled when package.json changes. This single change cut average build time by 60%.
- Multi-stage build: Used a builder stage for compilation and a slim runtime stage for the final image. Reduced image size from 1.2GB to 180MB.
- .dockerignore: Excluded node_modules, .git, test files, and documentation from the Docker context. Reduced context size from 500MB to 15MB.
- BuildKit cache mounts: Used --mount=type=cache for npm and pip caches, so package downloads persist across builds.
Result: Build time dropped from 18 minutes to 2 minutes (cold build) or 30 seconds (cached build).
Step 3: Parallelize the test suite (14 min to 3 min)
We had 2,400 tests running sequentially. The fix was straightforward:
- Parallel execution: Split tests across 4 parallel runners using Jest is --shard flag. Each shard runs approximately 600 tests.
- Test database optimization: Instead of dropping and recreating the test database for each test file, we used transactions that roll back after each test. Database setup time dropped from 45 seconds to 2 seconds per test file.
- Removed slow tests: Found 15 tests that were each taking 10+ seconds because they were testing external API integrations synchronously. Moved these to a separate nightly suite and replaced them with mocked unit tests that run in milliseconds.
Result: Test suite dropped from 14 minutes to 3 minutes. The parallel runners cost $0.04 per run on GitHub Actions.
Step 4: Optimize deployment (8 min to 1 min)
We switched from recreating ECS tasks to rolling updates. Instead of stopping all containers and starting new ones, we start new containers alongside old ones, wait for health checks to pass, and drain connections from old containers. This eliminated the infrastructure provisioning step entirely.
We also pre-pulled the Docker image to our container instances, so the deployment just needs to start a new container from an already-present image.
Step 5: Reduce health check time (5 min to 30 sec)
Our health check interval was 30 seconds with a healthy threshold of 5 consecutive checks. That means the load balancer waited 2.5 minutes before routing traffic to the new container. We reduced the healthy threshold to 2 checks and the interval to 10 seconds. New containers start receiving traffic in 20-30 seconds.
The impact
Going from 45 minutes to 3 minutes changed our engineering culture. We went from 2 deploys per day to 15-20. Bugs are fixed and shipped within minutes. Developers no longer batch changes into large, risky releases. Feature flags became practical because deploying the flag change is instant.
The total investment was about 2 weeks of engineering time. The ROI is measured in developer hours saved every single day.
Need help speeding up your deployments?
traztech optimizes CI/CD pipelines for startups. We have cut deployment times by 80-95% across dozens of engagements. Faster deploys mean faster iteration and fewer production incidents.
Book a free strategy call