Building a Production Deployment Pipeline for Multiple Services Without Kubernetes

Modern backend applications are increasingly split into multiple services. Whether it's an API, an admin dashboard backend, or a worker service, deploying them reliably is often more challenging than writing the application itself.
Many teams assume the next step is Kubernetes. While it's an excellent platform, it also introduces a significant amount of operational complexity. For many applications, that complexity simply isn't necessary.
This article walks through a deployment pipeline built with Docker, Bitbucket Pipelines, Amazon ECR, Docker Compose, Nginx, and OIDC authentication. The goal wasn't to replace Kubernetes—it was to build a deployment process that is secure, repeatable, and easy to maintain.
The Architecture
Imagine a backend consisting of two independent services:
- API Service
- Admin Service
Both services live inside a monorepo and share common libraries, but each is packaged and deployed independently.
The deployment pipeline needs to answer a few important questions:
- How do we build both services consistently?
- How do we know exactly what was deployed?
- How do we authenticate with AWS securely?
- How do we minimize downtime?
- How do we keep production servers simple?
Let's tackle each one.
Build Once, Deploy Anywhere
One of the biggest mistakes a deployment pipeline can make is rebuilding code on the production server.
Instead, the production server should receive a finished artifact that has already been tested and verified.
During every commit, the CI pipeline builds a Docker image for each service:
api-service:<commit-sha>
admin-service:<commit-sha>
Using the Git commit SHA as the image tag makes every build immutable.
That means:
- every deployment can be traced back to a specific commit
- rollbacks become predictable
- production always runs a known artifact
- the server never compiles application code
This follows a simple principle:
Build once. Deploy many times.
Why Immutable Tags Matter
A common approach is using a mutable tag such as:
latest
or
production
While convenient, mutable tags quickly become difficult to reason about.
Imagine updating only the API service while leaving the admin service unchanged.
If both images are tagged latest, it becomes impossible to know which version is actually running.
Instead, every image receives an immutable tag:
api-service:a8f42d9
Optionally, a mutable tag like production can still exist as a convenience, but deployments should always reference the immutable image.
This makes deployments deterministic.
Secure Authentication Without AWS Keys
Most CI systems interact with AWS using long-lived access keys stored as secrets.
Those credentials have several drawbacks:
- they must be rotated
- they can be leaked
- they're often granted broader permissions than necessary
A better alternative is OpenID Connect (OIDC).
During a pipeline run:
- Bitbucket generates a signed identity token.
- AWS verifies that token.
- AWS allows the pipeline to assume an IAM role.
- Temporary credentials are issued.
- The pipeline pushes Docker images to Amazon ECR.
No permanent AWS credentials are stored in the repository.
When the pipeline finishes, those credentials expire automatically.
It's both simpler and more secure.
Separating Build From Runtime
Once images are pushed to Amazon Elastic Container Registry (ECR), the production server has only one responsibility:
Pull the requested image.
It never installs dependencies.
It never runs build commands.
It never compiles source code.
The CI system produces artifacts.
Production simply executes them.
Keeping those responsibilities separate greatly improves reliability and repeatability.
Running Services With Docker Compose
The production machine runs Docker Compose.
Each service runs inside its own container.
API Service
Admin Service
Redis
PostgreSQL
Deploying a new release becomes a straightforward process:
- Pull updated images.
- Recreate containers whose images changed.
- Leave everything else untouched.
Because containers are managed independently, updating one service doesn't require restarting every component in the system.
Routing Traffic With Nginx
Rather than exposing containers directly to the internet, Nginx acts as a reverse proxy.
Each container listens on an internal port:
API Service → localhost:8000
Admin Service → localhost:8001
Nginx receives all incoming requests and forwards them to the appropriate service.
Besides request routing, this also centralizes:
- HTTPS termination
- SSL certificate management
- request logging
- compression
- security headers
Keeping these responsibilities outside the application containers keeps each service focused on business logic.
Reducing Downtime During Deployments
One concern with container deployments is service interruption.
Fortunately, Docker Compose recreates containers individually.
A typical deployment looks like this:
Pull new image
↓
Create updated container
↓
Old container stops
↓
New container begins serving traffic
Only the service being updated experiences a brief restart.
The rest of the system continues operating normally.
For many products, this provides an excellent balance between availability and operational simplicity.
Lessons Learned
Several decisions made the deployment pipeline significantly easier to operate.
Using immutable Docker images eliminated uncertainty around deployments.
OIDC authentication removed the need to manage AWS credentials manually.
Amazon ECR provided a reliable source of versioned artifacts.
Docker Compose kept orchestration lightweight while still supporting independent service deployments.
Nginx simplified networking, HTTPS, and request routing.
Most importantly, the deployment process became predictable.
Every deployment follows the same sequence:
- Build
- Push
- Pull
- Restart
There are no manual build steps, no SSH sessions compiling code, and no uncertainty about what version is running.
Final Thoughts
A production deployment pipeline doesn't have to be complicated to be effective.
Before adopting a full orchestration platform, it's worth asking whether the additional operational complexity solves a problem you actually have.
For many teams, a combination of Docker, a CI pipeline, an image registry, and Docker Compose provides everything needed to deploy multiple services safely and consistently.
Good infrastructure isn't defined by the number of technologies involved. It's defined by how confidently your team can deploy software, recover from failures, and understand what's running in production.
Comments · 0
Be the first to comment.