Crypto News

Why I Built My Own Ingress Proxy for Docker Swarm — And You Might Want It Too

Ashamed to admit, I still use Swarm in 2025. It’s easy to explain, quick to set up, and simple to use. It’s a starting point in the world of containers when full-blown Kubernetes is overkill. But Docker’s evolution hasn’t been the most straightforward. Over time, it began to lack a minimal dynamic proxy for Swarm – one that you run once, which configures traffic routing to microservices automatically – and forget about it. That’s how Millau was born – an ingress-proxy and load balancer based on labels. It currently powers its own website and a few other projects.

Why another proxy?

If you’ve ever deployed services in Docker Swarm, you’ve encountered the question of routing external traffic: how to organize access from the outside to the desired services inside the stack. Sure, you can use Nginx, HAProxy, or Traefik – and many do. But each of these solutions has its drawbacks. For instance, Nginx and HAProxy are classics, but they require constant rebuilding and redeploying of the config file. Traefik is a great universal tool, but its flexibility and middleware architecture can make configuration overly complicated.

In my view, the task of proxying should be as simple as possible and free from anything resembling business logic. If you’ve ever debugged header rewriting or URL part substitution by the server after yet another deployment, you know what I mean. Metaprogramming in a proxy is still readable as a file, and its syntax can be linted. But in labels, the same config turns into an unmaintainable mess of delimited parameters. I wanted a simple and declarative solution with self-configuration, requiring no redeploy and being fully transparent to clients in front and microservices behind.

How it works

From Docker’s perspective, Millau is a container with read-only access to the Docker Engine socket to listen to events. Creating, updating, and deleting services (Swarm mode) and containers (Compose mode) triggers proxy rule updates. A service announces itself to Millau via metadata in labels. For example, to add a new service with port 9000 in a Swarm stack, just specify two labels in its manifest:

deploy:
  labels:
    - "millau.enabled=true"
    - "millau.port=9000"

Once the service is deployed, Millau detects the labels and adds a new route to it. By default, any HTTP host and any HTTP path will be proxied. That’s it. This is the typical SPA deployment case on a single domain.

If there are multiple domains, you need to tell Millau which hosts this service should serve. A third label is added to the service manifest:

deploy:
  labels:
    - "millau.enabled=true"
    - "millau.port=9000"
    - "millau.hosts=example.com"

But what if there are more services? For instance, another typical deployment case – React frontend and API backend microservices. Both live on the same domain, and the only difference is the /api/ prefix in the backend URL. A fourth label is added to the manifest:

# backend
deploy:
  labels:
    - "millau.enabled=true"
    - "millau.port=9000"
    - "millau.hosts=example.com"
    - "millau.path=/api/"

Labels for frontend can remain unchanged. All requests with the /api/ prefix will be routed to backend, and the rest will go to frontend. A detailed description of Host and Path Matching mechanisms with examples is available in the documentation on millau.net.

Guaranteed delivery

A key feature – load balancing between multiple services. Millau distributes traffic among them and automatically excludes those that behave unreliably. On failure or slowdown (30 seconds by default) of a service, the request switches to the next active one. After a defined time (60 seconds by default), the proxy retries and, if everything is okay, returns the service to operation. If a service has several replicas, the proxy tries the replicas first before marking the service as inactive.

This way, you can deploy different versions (Docker images) of varying quality of the same microservice. For example, if blue, green, and red serve example.com, the proxy distributes traffic to all using a Round-Robin algorithm. If red crashes, the proxy marks it as inactive and forwards the request to the next – blue or green. If blue suddenly exceeds its configured timeout, the proxy forwards the request to green.

TLS

Millau can terminate HTTPS traffic. To do this, just pass the public and private TLS certificate keys via the manifest. Keys can be passed directly or via environment variables – Docker substitutes their values automatically during deployment. KEY and CERT below must be base64-encoded. Example setup:

deploy:
  labels:
    - "millau.enabled=true"
    - "millau.port=9000"
    - "millau.key=${KEY}"
    - "millau.cert=${CERT}"

mTLS

Another common case – securing traffic between Cloudflare and the Docker Swarm cluster.

EncryptionEncryption

Cloudflare issues a long-lived wildcard TLS certificate, which is used to encrypt the connection all the way to the microservice. After receiving the certificate, its keys are exported to the environment variables mentioned above. Credit where it’s due – mTLS is available even on the free plan.

Monitoring

Millau provides two types of metrics: for Docker healthcheck (HTTP 200 for healthy, 503 for unhealthy status) and for Prometheus integration.

Prometheus metrics:

  • number of open connections
  • number of received requests
  • total volume of inbound and outbound data
  • number of successfully and unsuccessfully processed requests
  • number of request retries
  • current service status: 0 – unavailable, 1 – active
  • histogram of request processing time by microservice.

MetricsMetrics

A Grafana Dashboard is made for visualizing the metrics.

Under the hood

Millau is written in Golang. It exposes ports for receiving HTTP and HTTPS traffic. There’s also a third port for telemetry, but it’s not accessible from outside by default.

For diagnostics and debugging, a logging system with five levels is provided:

  • FATAL: critical error after which the Millau process exits;
  • ERROR: proxy failure, process continues running;
  • WARN: incorrect behavior on the client or microservice side, process continues running;
  • INFO: normal output, default value;
  • DEBUG: step-by-step output for development and analysis.

What it doesn’t have

Millau focuses on a narrow niche of the Docker ecosystem: Swarm, Compose, and Testcontainers. Kubernetes is not supported and not planned.

ACME protocol support is not yet implemented, so automatic TLS certificate retrieval via Let’s Encrypt is unavailable. Certificates must be copied into the manifest or passed via environment variables as shown above.

Postscript

Millau is not a commercial product. The proxy is free for any use, but the code is currently closed. I respect open source and have learned a lot from the community, and whenever possible, I give back. Some of my open-source projects are actually used. But they rarely go beyond a narrow niche. My goal is not just to write useful code, but to create a product with a long lifecycle until Swarm do us part – release Millau under an open license and make it the standard for Docker Swarm. You can get a license key in a minute on millau.net. Every key, star, or review helps attract investor attention and bring us closer to that goal.

If, like me, you’re still practicing Swarm in 2025 and looking for a simple ingress-proxy with self-configuration, Millau might suit you. If you use Docker Swarm and built ingress differently – I’d love to know how. GitHub is open for suggestions, discussions, and pull requests with tests – there’s never too many of those.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button