There's a very normal point in a container setup where a service stops being “just a service” and starts dragging infrastructure concerns in behind it.
A Postgres container needs better log handling. A vendor image needs a reverse proxy in front of it. An internal app needs auth, metrics, or traffic shaping, but rebuilding the image or the application just to bolt that on feels wrong. Not because it never works, but because every little operational requirement starts forcing changes into the thing that was supposed to stay simple.
That's where sidecars get useful in Docker Compose. They give you a way to add supporting behavior around a container instead of pushing more responsibility into the container itself.
What a sidecar actually is
A sidecar is just another container whose job is to support a primary container rather than be its own piece of business logic.
In Docker Compose, that usually means one of two things:
- It shares a volume with the main service.
- It talks to the main service over the Compose network.
And in the tighter version of the pattern, it can also share the main service's network namespace with network_mode: "service:...", which means both containers effectively live on the same localhost.
That last one is useful, but it's not the default answer to everything. Most of the time, regular Compose networking is enough and the sidecar can just talk to the main container by service name.
Why this pattern is useful
The main benefit of a sidecar is that it lets the core service stay boring.
That's a good thing.
Instead of baking logging, proxying, auth, metrics, and every other operational concern into one increasingly weird image, you leave the main service alone and attach the extra behavior beside it. That keeps responsibilities cleaner and makes it easier to swap the supporting pieces later.
It also changes the way changes happen:
- You stop forking upstream images for tiny operational tweaks.
- You can replace the helper without touching the core container.
- The boundary becomes obvious: a port, a shared volume, or a shared network namespace.
- The main service keeps doing its actual job.
That's the bit that makes sidecars worth learning. They're less about "patterns" and more about not turning every infrastructure need into application surgery.
Example 1: a Postgres log collector sidecar
This is one of the few examples that doesn't feel made up for a blog post.
Postgres logs are genuinely useful, but they're also something teams often handle badly. Either everything gets dumped into docker logs and forgotten, or someone starts rebuilding the database image to cram in extra tooling. A sidecar gives you a cleaner middle ground: let Postgres write logs to a shared directory, and let another container deal with collecting or shipping them.
Here's a simple Compose setup:
version: "3.9"
services:
postgres:
image: postgres:16
container_name: app-postgres
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: appsecret
POSTGRES_DB: appdb
command:
- postgres
- -c
- logging_collector=on
- -c
- log_directory=/var/log/postgresql
- -c
- log_filename=postgresql.log
- -c
- log_statement=all
volumes:
- postgres_data:/var/lib/postgresql/data
- postgres_logs:/var/log/postgresql
ports:
- "5432:5432"
postgres-log-sidecar:
image: alpine:3.20
container_name: postgres-log-sidecar
depends_on:
- postgres
command: >
sh -c '
touch /logs/postgresql.log &&
tail -F /logs/postgresql.log
'
volumes:
- postgres_logs:/logs
volumes:
postgres_data:
postgres_logs:
What this gets right:
- The Postgres image stays untouched.
- Logging is enabled through runtime config instead of a custom Dockerfile.
- The sidecar only cares about one contract: log files show up in a shared directory.
That contract is the important part. The database container stays stable, while the behavior around it becomes replaceable. Today the sidecar is just tail -F. Tomorrow it could be Fluent Bit, Vector, or something that parses and forwards logs to Loki or Elasticsearch.
That's what makes the pattern useful in real life. You are not teaching Postgres how to become a log pipeline. You are letting Postgres be a database, and giving the log handling job to something sitting next to it.
A couple of practical notes:
log_statement=allis fine for demos and debugging, but usually too noisy for production.- If you care about structured logs, you'll probably want to move beyond a tailing container pretty quickly.
- Even then, the sidecar still holds up — you just swap the helper, not the database image.
Example 2: putting a login wall in front of an old internal app
A very common case for a proxy sidecar is an old internal tool that suddenly needs to be reachable by people outside the office or VPN.
That kind of thing happens all the time. Some admin panel or reporting tool was built for a trusted internal network, so it never got proper authentication, never expected internet-facing traffic, and nobody wants to reopen the application just to retrofit a login flow into a codebase that has been quietly minding its own business for years.
In that situation, a proxy sidecar is a very practical move. You leave the application exactly as it is, and put something in front of it that handles the new edge concerns.
A minimal Compose setup might look like this:
version: "3.9"
services:
app:
image: myorg/legacy-admin:latest
container_name: legacy-admin
expose:
- "8080"
app-proxy:
image: nginx:1.27-alpine
container_name: legacy-admin-proxy
depends_on:
- app
ports:
- "8088:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/.htpasswd:/etc/nginx/.htpasswd:ro
And the Nginx config:
server {
listen 80;
server_name _;
location / {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://app:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
You can generate the password file with something like:
docker run --rm httpd:2.4-alpine htpasswd -nbB admin supersecret > ./nginx/.htpasswd
This is the kind of change that sidecars are great at.
The application still just listens on port 8080 and does whatever weird old internal apps do. The proxy sidecar becomes the public edge: it prompts for credentials, forwards requests, and gives you one place to add more external-facing concerns later, like IP allowlists, TLS termination, or extra headers.
That is a much cleaner move than reopening the app just because its environment changed. The app did not suddenly need to become more sophisticated. It just needed a better boundary in front of it.
When to use network_mode: "service:..."
There's a loose version of the sidecar pattern and a tight one.
The loose version is what most people need: two services on the same Compose network, maybe sharing a volume. That covers both examples above.
The tighter version is when the sidecar needs to share the main service's network namespace so that 127.0.0.1 inside one container is effectively the same localhost as the other. In Docker Compose, that looks like this:
version: "3.9"
services:
main:
image: curlimages/curl:8.8.0
command: ["sh", "-c", "sleep infinity"]
sidecar:
image: nginx:1.27-alpine
network_mode: "service:main"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
This mode is handy when shared localhost is the whole point, like with local DNS helpers, traffic interceptors, VPN sidecars, or agents that need to behave as if they are attached directly to the main service.
But it is not the default recommendation. Once two containers share a network namespace, things get tighter and slightly less obvious. Port publishing and connectivity behave differently, and it's easier to confuse yourself if all you really needed was normal service-to-service traffic over the Compose network.
So the rule of thumb is simple: use network_mode: "service:..." when shared localhost is the requirement, not just because it makes the setup look more “sidecar-ish”.
A good smell test
A sidecar makes sense when you're adding operational behavior to an existing service without wanting to modify the service itself.
That usually includes things like:
- Log collection
- Reverse proxies
- Metrics or tracing agents
- DNS helpers
- VPN or tunnel containers
- Policy and traffic helpers
It usually does not make sense when you are really just introducing another application service with its own lifecycle and domain logic. At that point, it's probably not a sidecar. It's just another service, and that's fine too.
The trick is not to get religious about the pattern. A sidecar is useful when it keeps the core container stable and pushes supporting concerns outward. If it starts making the setup more confusing than helpful, you're probably forcing it.
Third shirts often give designers more freedom with patterns and contrasting details. For matchday clothing decisions, FC Barcelona jersey(camiseta del FC Barcelona) connects the selected design with its team identity. Home and away versions can serve different preferences while representing the same team.