When working with containerized services, you quickly run into a problem — how do you add extra functionality to your service when the image is supposed to be ephemeral and efficient? Usually, you would add an API, a shared library, or some other building block inside your code so thatyou can call upon it for help. But you can't do that with Docker, because often the containers aren't under your control, and you have to build outside of it.
And that's where sidecars come in. They're an additional container that runs alongside the original service, to extend functionality. If you've used Home Assistant, you've already used sidecars without knowing the term, because that's how Home Assistant addons work when you set them up.
One common way of using sidecars is to handle the networking layer for your containers, which simplifies deployment and development, as you can use local loopback addresses between your containerized service and the sidecar instead of working with the added complexity of DHCP. It's all about working smarter, and more efficiently, and it's a game-changer for the data center and your home lab.
Okay but why sidecars?
Abstract concepts get real-world results
When you are working with containers, the whole point is being able to run services more efficiently in the data center. We got here because, while servers can be used for many functions, in practice, we end up with one service per server. After all, there is already enough complexity in handling routing, load balancing, failover, and other considerations for improving uptime. This means we can abstract the physical server into a digital construct of a container, and just like Neo's travails in The Matrix, magical things can happen once something is free of physical constraints.
But we don't want to modify the container image when we need added functionality. That reintroduces complexity, and the whole point was to get away from that model. Instead, we hang another container onto the side of the first one, like a motorcycle sidecar. This is now a pod, and to the outside world, it looks like one container. Inside the pod, the two communicate via local loopback, making communication between the services easy.
It also means that the programming language, tools, SDKs, and dependencies you use don't have much impact anymore, because they are all abstracted away by the container-to-container logic. You can change the back-end without breaking how the sidecar functions, or change how the sidecar functions without affecting your core service.
Docker
A quick example with the ever-popular Tailscale
Blocks upon blocks is how the world was built in the first place
When I started self-hosting services, I set up a reverse proxy to access them all from a single domain name. That's fine when you only have a handful of services. Still, once you add more, the administration time it takes to tweak configuration files gets annoying, to say nothing about the thousands of services in any modern distributed data center. But there's a quick fix, and using sidecars is the answer.
Using Tailscale to create a trusted network isn't only about bringing in physical devices like clients and servers. Remember, we can think of containers as single-purpose servers, and we're already used to adding devices as nodes on our tailnet. By adding a Tailscale sidecar to each Docker container I'm running, those services now live on my tailnet, without configuration of reverse proxies, port forwarding, firewall rules, or tricks to bypass NAT issues.
I'm going to use an example from Tailscale's documentation here, because I'm not about to share the auth keys and other secrets of my own installation:
---
version: "3.7"
services:
ts-mealie:
image: tailscale/tailscale:latest
container_name: ts-mealie
hostname: mealie
environment:
- TS_AUTHKEY=tskey-client-kvtJAbRNotARealKey4d?ephemeral=false
- TS_EXTRA_ARGS=--advertise-tags=tag:container
- TS_SERVE_CONFIG=/config/mealie.json
- TS_STATE_DIR=/var/lib/tailscale
volumes:
- ${PWD}/ts-mealie/state:/var/lib/tailscale
- ${PWD}/ts-mealie/config:/config
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
restart: unless-stopped
mealie:
image: ghcr.io/mealie-recipes/mealie:v1.0.0
container_name: mealie
network_mode: service:ts-mealie
depends_on:
- ts-mealie
volumes:
- mealie-data:/app/data/
environment:
- ALLOW_SIGNUP=true
restart: unless-stopped
volumes:
mealie-data:
driver: local
ts-mealie:
driver: local
The magic part is the network_mode: service:ts-mealie line, which merges the network namespaces of the containers together inside the Linux kernel. That lets you access the Mealie container as if it was running inside the Tailscale container, which is mind-bendingly neat. The other important part here is the line with TS_SERVE_CONFIG=/config/mealie.json, which tells Tailscale Serve and Funnel how to proxy traffic for Mealie, so that anyone on our tailnet can access the self-hosted service.
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"${TS_CERT_DOMAIN}:443": {
"Handlers": {
"/": {
"Proxy": "http://127.0.0.1:9000"
}
}
}
},
"AllowFunnel": {
"${TS_CERT_DOMAIN}:443": false
}
} Now, anyone on my tailnet can go to mealie.auto-genereated.ts.net and use Mealie, with a valid HTTPS certificate for encrypted traffic. I can do this for every container I self-host, and now I don't have to configure reverse proxies, handle IP allocations, or know what port I need for each service. It just works, and is scalable no matter how many services I add. It also doesn't matter if those containers run on Linux, FreeBSD, Windows, macOS, or any other operating system, because I've abstracted away all the necessities.
Tailscale
Using sidecars with your containerized workflow improves every aspect
Once you stop thinking in terms of the old client-server model and realize that everything is code, and that Docker's approach of one service per container makes absolute sense for scaling deployments up to fit the needs of evolving workspaces, sidecars are the only responsible way to add additional functionality to your containers.
