Recently inside a Discord server for Pangolin I had the most interesting discussion with somebody. I was seeking help as my service for vaultwarden had gone down... again. So I went seeking help. When I shared my docker-compose.yml file at the time it had made the other’s curious about why I approached my network in this way. Here is the docker-compose.yml for you to make fun of.

YAML
name: vaultwarden

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    hostname: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: https://vault.whoisjason.me
    volumes:
      - /media/jason/Storage/BitWarden:/data
    labels:
      icon: https://icon.casaos.io/main/all/vaultwarden.png
    networks:
      - vaultwarden

  vaultwarden-newt:
    image: fosrl/newt
    container_name: vaultwarden-newt
    restart: unless-stopped
    environment:
      - PANGOLIN_ENDPOINT=https://pangolin.whoisjason.me
      - NEWT_ID=palqb5rnuzkk49h
      - NEWT_SECRET=mvjn9yscn0dja26bvhzojxk4pb9svbqgpmnnrhoh0peacr7v
    networks:
      - vaultwarden

networks:
  vaultwarden:
    name: vaultwarden

Now before anything, that newt has been deleted to sharing those ‘keys’ is fine here. Secondly, do you notice how there is a whole newt attached here, totally unnecessary. But what did I gain from that? Well the docker container was not exposed on my LAN, as vaultwarden is very picky about HTTPS and does not allow insecure HTTP traffic. So there was no point in leaving the http open on my LAN. This closed it down, and doubled the amount of newts I actually needed in my setup.

Why not?

Well, it doesn’t really hurt anything, but I want my network to be as efficient as possible, and it’s really cool when you can use hostnames, and even cooler when you can use hostnames and keep your network more secure.

But then I got to wondering, why have 2 newts, when a single docker network is all I really need. a docker network acts like a router, assigning IP addresses, and sharing a central point of communication, and fundamentally that is what a router is, and no I’m not talking about Docker’s IPVLAN networks, but those are cool too.

What do you do instead?

The solution was painful, and locked me out of my server a few times in the process as I was testing Portainer under a domain, a very bad idea.

The solution, was to modify the pre-existing newt to this:

YAML
services:
  newt:
    image: fosrl/newt
    container_name: working-newt-home
    restart: unless-stopped
    environment:
      - PANGOLIN_ENDPOINT=https://pangolin.whoisjason.me
      - NEWT_ID=LMAOJKJK
      - NEWT_SECRET=IMACTUALLYHIDINGTHISONE
    networks:
      - newtnet

networks:
  newtnet:
    name: newtnet
    driver: bridge

Now what is cool about this is, the ‘newtnet’ which allows me to connect other docker containers I have running, to newtnet.
Then I can go over to a service like vaultwarden, where I
1. Want to use the hostname
2. Don’t want it on the LAN
and I modify the compose to look like this

YAML
name: vaultwarden

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    hostname: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: https://vault.whoisjason.me
    volumes:
      - /media/jason/Storage/BitWarden:/data
    networks:
      - vaultnet

networks:
  vaultnet:
    name: newtnet
    external: true

Check out here how we can call it whatever we want, like vaultnet, but within the compose we say that vaultnet=newtnet

Why is this better?

Because!!! if I have another service like emby, you can notice here that nothing is special about it at all, it connects straight onto the LAN, so I can watch my movies without internet. All while still using that same newt, using ‘vaultwarden:80’ and not exposing it to the LAN. All without opening up a second wireguard tunnel connection.

YAML
services:
  emby:
    image: emby/embyserver:latest
    container_name: emby
    restart: unless-stopped

    environment:
      - PUID=1000
      - PGID=1000
      - TZ=US/Arizona

    ports:
      - 8096:8096
      - 8920:8920

    devices:
      - /dev/dri:/dev/dri

    volumes:
      - /DATA/AppData/emby/config:/config
      - /media/jason/14TB-Beast/TV:/media/jason/14TB-Beast/TV
      - /media/jason/14TB-Beast/Movies:/media/jason/14TB-Beast/Movies

But wait there’s more

So then from there, what if you wanted to add another service, a service that again, doesn’t need to touch the LAN, but this time a trickier, more sinister service, that I had to write my own documentation for Pangolin for! 😡

Rybbit example

Code
services:
  clickhouse:
    container_name: clickhouse
    image: clickhouse/clickhouse-server:25.4.2
    volumes:
      - clickhouse-data:/var/lib/clickhouse
    configs:
      - source: clickhouse_network
        target: /etc/clickhouse-server/config.d/network.xml
      - source: clickhouse_json
        target: /etc/clickhouse-server/config.d/enable_json.xml
      - source: clickhouse_logging
        target: /etc/clickhouse-server/config.d/logging_rules.xml
      - source: clickhouse_user_logging
        target: /etc/clickhouse-server/config.d/user_logging.xml
    environment:
      - CLICKHOUSE_DB=${CLICKHOUSE_DB:-analytics}
      - CLICKHOUSE_USER=${CLICKHOUSE_USER:-default}
      - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-frog}
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"]
      interval: 3s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped

  postgres:
    image: postgres:17.4
    container_name: postgres
    environment:
      - POSTGRES_USER=${POSTGRES_USER:-frog}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-frog}
      - POSTGRES_DB=${POSTGRES_DB:-analytics}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 3s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped

  backend:
    image: ghcr.io/rybbit-io/rybbit-backend:${IMAGE_TAG:-latest}
    container_name: backend
    environment:
      - NODE_ENV=production
      - CLICKHOUSE_HOST=http://clickhouse:8123
      - CLICKHOUSE_DB=${CLICKHOUSE_DB:-analytics}
      - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-frog}
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
      - POSTGRES_DB=${POSTGRES_DB:-analytics}
      - POSTGRES_USER=${POSTGRES_USER:-frog}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-frog}
      - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
      - BASE_URL=${BASE_URL}
      - DISABLE_SIGNUP=${DISABLE_SIGNUP}
      - DISABLE_TELEMETRY=${DISABLE_TELEMETRY}
      - MAPBOX_TOKEN=${MAPBOX_TOKEN}
    depends_on:
      clickhouse:
        condition: service_healthy
      postgres:
        condition: service_started
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3001/api/health"]
      interval: 3s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped
    networks:
      - default
      - newtnet

  client:
    image: ghcr.io/rybbit-io/rybbit-client:${IMAGE_TAG:-latest}
    container_name: client
    environment:
      - NODE_ENV=production
      - NEXT_PUBLIC_BACKEND_URL=${BASE_URL}
      - NEXT_PUBLIC_DISABLE_SIGNUP=${DISABLE_SIGNUP}
    depends_on:
      - backend
    restart: unless-stopped
    networks:
      - default
      - newtnet

volumes:
  clickhouse-data:
  postgres-data:

configs:
  clickhouse_network:
    content: |
      <clickhouse>
          <listen_host>0.0.0.0</listen_host>
      </clickhouse>

  clickhouse_json:
    content: |
      <clickhouse>
          <settings>
              <enable_json_type>1</enable_json_type>
          </settings>
      </clickhouse>

  clickhouse_logging:
    content: |
      <clickhouse>
        <logger>
            <level>warning</level>
            <console>true</console>
        </logger>
        <query_thread_log remove="remove"/>
        <query_log remove="remove"/>
        <text_log remove="remove"/>
        <trace_log remove="remove"/>
        <metric_log remove="remove"/>
        <asynchronous_metric_log remove="remove"/>
        <session_log remove="remove"/>
        <part_log remove="remove"/>
        <latency_log remove="remove"/>
        <processors_profile_log remove="remove"/>
      </clickhouse>

  clickhouse_user_logging:
    content: |
      <clickhouse>
        <profiles>
          <default>
            <log_queries>0</log_queries>
            <log_query_threads>0</log_query_threads>
            <log_processors_profiles>0</log_processors_profiles>
          </default>
        </profiles>
      </clickhouse>

networks:
  newtnet:
    name: newtnet
    external: true

Now look at that, 2 networks! meaning that postgres never touches the internet, and Rybbit works flawlessly. and my Pangolin configuration can look like this

Wrapping up

So I found this all kind of cool and a way for me to learn more about docker networking and become more familiar with my own network. I came from using Cloudflare tunnels where it look me way too long to realize that I could re-use a single tunnel if it was on a different port. Which has led me here today with a greater knowledge of how docker and networking all works. Docker networks can be confusing to manage and a lot to wrap your head around at a first, but with enough trail and error and crashing services you are trying to desperately to rely on will get you far.