Skip to content

Service per Container

You have several services to deploy. Each has its own runtime, its own set of libraries, and its own version of those libraries. One service needs an older OpenSSL, another needs a specific Java release, a third bundles a native image-processing library. In the early days you ran them all directly on a shared host, installing each runtime side by side.

That worked for two services. With ten, the host turns into a minefield: upgrading a shared library for one service breaks another, “works on my machine” becomes a daily refrain, and nobody can reproduce production exactly. You need a way to ship a service together with everything it needs to run, and to place many such services on a machine without them interfering.

You want each service to be deployed as a self-contained, immutable unit that includes its own dependencies, so that what you tested is exactly what runs. You want that unit to start in seconds, not minutes, so deploys and autoscaling are fast. You want strong isolation so one service’s libraries, file system, and resource usage do not leak into another’s. And you want all of this without paying for a full virtual machine per service.

So the forces are: strong isolation and reproducibility, but with fast startup and high density on each machine, and a single artifact that runs the same everywhere.

Package each service as a container image: a layered, immutable filesystem snapshot that bundles the service binary, its runtime, and its dependencies, built once from a declarative recipe. At runtime that image is instantiated as a container — an isolated process with its own filesystem, network namespace, and resource limits, sharing the host kernel rather than booting a full guest OS.

The defining rule is one service per container. You do not pack several services into one image. Each service gets its own image and its own container, so it can be built, versioned, deployed, scaled, and rolled back independently of every other service.

Because containers are lightweight, you run many of them per machine. You do not place them by hand: an orchestrator such as Kubernetes takes your desired state (“run three replicas of the order service”) and schedules containers onto machines, restarts the ones that die, and reschedules them when a node fails.

flowchart TB
  subgraph Build[Build once]
    Dockerfile[Dockerfile] --> Image[order-service:1.4.2 image]
  end
  Image --> Reg[(Image registry)]
  subgraph Cluster[Orchestrated cluster]
    subgraph N1[Node A]
      C1[order-service container]
      C2[order-service container]
    end
    subgraph N2[Node B]
      C3[order-service container]
      C4[inventory-service container]
    end
  end
  Reg --> C1
  Reg --> C2
  Reg --> C3
  Sched[Scheduler / control plane] --> N1
  Sched --> N2
One image built once, pulled from a registry, run as one-service-per-container and scheduled across nodes by the orchestrator

The recipe that builds the image is a Dockerfile. A multi-stage build compiles the service in a heavy build image, then copies only the finished binary into a tiny runtime image, so the shipped artifact stays small and has little to attack.

# Stage 1: build
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/order-service ./cmd/order
# Stage 2: minimal runtime — one service, nothing else
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/order-service /order-service
USER nonroot
EXPOSE 8080
ENTRYPOINT ["/order-service"]

That image is then declared to the orchestrator as a Deployment. Notice that it asks for three replicas, sets resource limits so one container cannot starve its neighbours, and wires up a health check the orchestrator uses to decide when a container is ready.

apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: registry.example.com/order-service:1.4.2
ports:
- containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 3
periodSeconds: 5

What you gain:

  • Isolation and reproducibility. Each service ships with its own dependencies in an immutable image, so the artifact you tested is byte-for-byte the one that runs. Library conflicts between services disappear, and “works on my machine” largely goes away.
  • Fast, dense, portable. Containers start in seconds and pack many to a host because they share the kernel rather than booting a VM. The same image runs unchanged on a laptop, in CI, and in production.
  • Independent lifecycle. One service per container means you build, version, scale, and roll back each service on its own schedule.

What it costs you:

  • Orchestration complexity. Running containers at scale means adopting an orchestrator, which is a substantial system in its own right — scheduling, networking, storage, and security all become things your team must understand and operate.
  • Image and supply-chain hygiene. Images can carry vulnerable base layers and bloat. You need a discipline of small, regularly rebuilt, scanned images.
  • Weaker isolation than VMs. Containers share the host kernel, so a kernel-level exploit has a larger blast radius than with full virtualization. Untrusted or high-risk workloads may still warrant stronger boundaries.
  • Externalized Configuration — keeps environment-specific settings out of the image so one image runs everywhere.
  • Service Mesh — adds a sidecar container alongside each service container for consistent networking.
  • Serverless Deployment — an alternative runtime model when you would rather not run the containers yourself.
What is the defining rule of the Service per Container pattern?
Why do containers start faster and pack more densely than virtual machines?
What does an orchestrator such as Kubernetes provide?
Which is a genuine cost of adopting containers at scale?