millim.no

Real life use case for Docker networks

admin, June 21, 2024, 8:45 a.m.

Virtual networks are almost as omnipresent in Docker projects as Docker volumes. Especially in Compose they're a magic sause that makes developing and hosting services so easy when using Docker. Yet I've struggled to find examples of advanced use of Docker networks -- and so I decided to share an example of mine.

If you just came here for the sauce, feel free to just browse to the code.

I rarely host a single web service on one host, and so often need a reverse proxy to access these services. Even when there only is one service, the app might not have built in support for SSL certificates, thus necessitating a reverse proxy. Sometimes these proxy set ups might become confusing, especially if one doesn't revisit them often. Docker's networking stack makes it easy to not also let multiple containers talk to each other, but even multiple container stacks. Then it's a breeze to expand the host with new services, all behind the same reverse proxy.

An example running on my home lab is my observability stack. It consists of Grafana and Prometheus; but only Grafana needs HTTP access. I also have a custom httpd container in a separate stack. These are tied together with a virtual network calledmillim-proxy in the following way:

  1. The network is defined in the proxy stack.
  2. It is referenced in the observability stack as external network.
  3. Grafana container gets a memorable host name.
  4. The Apache config refers to http://grafana instead of http://localhost.

Trade offs

Nothing has only upsides and no downsides, but I believe this kind of reverse proxy setup is more maintainable. The only real downside I see is somewhat increased complexity of the setup.

The benefits, though, are a few. Fewer ports are exposed on localhost, increasing security. With a misconfigured firewall a hacker can bypass the reverse proxy, and access a service that only communicates over insecure HTTP. Network port configuration gets simpler, since the port number is not configured in docker-compose.yml. The code gets easier to read and maintain with host names instead of localhost everywhere. Although it just sounds like syntactic sugar, for me it's important that my code is easy to read, especially if I don't look at it often.

Code sample

Here's how the real code looks:

services:
  prometheus:
    container_name: prometheus
    image: prom/prometheus
    dns: 100.100.100.100
    networks:
      - default
    volumes:
      - "/srv/prometheus:/etc/prometheus"
      - "data:/prometheus"
  grafana:
    container_name: grafana
    image: grafana/grafana:latest
    dns: 100.100.100.100
    restart: always
    networks:
      - millim-proxy
      - default
    environment:
      - GF_DEFAULT_INSTANCE_NAME="grafana.ts.millim.no"
    volumes:
      - "grafana-data:/var/lib/grafana"
      - "/srv/grafana:/etc/grafana"
volumes:
  data:
  grafana-data:
networks:
  default:
  millim-proxy:
    external: true
<VirtualHost *:443>
    ServerName grafana.ts.millim.no

    SSLEngine   On
    SSLCertificateFile /srv/certs/cert.pem
    SSLCertificateKeyFile   /srv/certs/privkey.pem
    SSLCertificateChainFile /srv/certs/fullchain.pem

    ProxyPreserveHost   On
    ProxyPass "/api/live/ws"    "wss://grafana:3000/api/live/ws"
    ProxyPassReverse "/api/live/ws" "wss://grafana:3000/api/live/ws"
    ProxyPass   /   http://grafana:3000/
    ProxyPassReverse    /   http://grafana:3000/
</VirtualHost>

What do you think?

I'd like to hear your thoughts on this post! Please hit me up on either LinkedIn or Mastodon.