Skip to content

Instantly share code, notes, and snippets.

@Jip-Hop
Last active January 14, 2026 19:32
Show Gist options
  • Select an option

  • Save Jip-Hop/c5b920f3123bec0e3a67353833f661c6 to your computer and use it in GitHub Desktop.

Select an option

Save Jip-Hop/c5b920f3123bec0e3a67353833f661c6 to your computer and use it in GitHub Desktop.
Simple but Effective Outbound "Firewall" using Vanilla Docker-Compose

Domain‑Based Egress Control in Docker Using Internal Networks and a Unified TCP Proxy

Modern Kubernetes clusters offer powerful primitives like FQDN‑based network policies (e.g., via Cilium, Calico, or Gatekeeper). These let you express rules such as “this workload may only talk to github.com and example.com” without worrying about IP churn, TLS hostname validation, or container‑level DNS quirks.

Docker, however, does not provide anything comparable out of the box.

This article documents a practical approach to implementing domain‑based egress control in plain Docker Compose, without modifying application containers, without terminating TLS, and without introducing heavyweight service meshes. It also covers the pitfalls we encountered—especially around QUIC/HTTP‑3—and compares our approach with the pattern suggested in the Creating a Simple but Effective Outbound "Firewall" using Vanilla Docker-Compose by Forest Johnson.


🎯 Goal

Create a Docker‑native mechanism that:

  • Restricts outbound connections per domain, not per IP.
  • Allows different containers to access different external domains.
  • Preserves TLS end‑to‑end (no MITM, no certificate rewriting).
  • Requires no modification of application containers.
  • Uses only Docker Compose, no Kubernetes, no custom CNI.
  • Works with multiple isolated internal networks.
  • Uses a single egress container for all outbound traffic.

đź“‹ Requirements

We set the following constraints:

Functional

  • Each internal network should map to a specific external domain.
  • Applications should connect using the real hostname (e.g., github.com).
  • The proxy must forward TCP traffic transparently.

Security

  • Application containers must remain unprivileged.
  • The proxy container should run as a non‑root user.
  • No TLS termination or certificate injection.
  • No modification of /etc/hosts, /etc/nsswitch.conf, or entrypoints inside app containers.

Operational

  • No static IP hacks inside app containers.
  • No cron jobs.
  • No custom DNS servers inside apps.
  • No patching of upstream images.

đź§­ Approach

We built a unified TCP proxy container using GOST, combined with Docker internal networks and DNS aliases.

1. Internal networks per domain

Each domain gets its own internal network:

github_net → github.com
example_net → example.com
quic_net → quic.nginx.org

These networks are marked internal: true, meaning they cannot reach the internet directly.

2. A single proxy container attached to all networks

The proxy container receives a static IP on each internal network:

172.16.20.2 → github_net
172.16.30.2 → example_net
172.16.40.2 → quic_net

3. Docker DNS aliases

Inside each network, the proxy is aliased to the real domain:

github.com → 172.16.20.2
example.com → 172.16.30.2
quic.nginx.org → 172.16.40.2

Applications simply connect to:

curl https://github.com

…and Docker resolves that to the proxy container.

4. GOST forwards TCP traffic

GOST listens on each internal IP and forwards traffic to the real domain:

-L=tcp://172.16.20.2:443/github.com:443
-L=tcp://172.16.30.2:443/example.com:443
-L=tcp://172.16.40.2:443/quic.nginx.org:443

5. Override Docker’s internal DNS inside the proxy

To prevent the proxy from resolving its own aliases (which would cause loops), we mount the host’s /etc/resolv.conf into the proxy container:

/etc/resolv.conf:/etc/resolv.conf:ro

This forces the proxy to use real upstream DNS.

                          +----------------------+
                          |      External        |
                          |      Internet        |
                          |----------------------|
                          |  github.com          |
                          |  example.com         |
                          |  quic.nginx.org      |
                          +----------+-----------+
                                     ^
                                     |  (TCP forwarding)
                                     |
                         +-----------+------------+
                         |   Unified GOST Proxy   |
                         |------------------------|
                         | 172.16.20.2 (github)   |
                         | 172.16.30.2 (example)  |
                         | 172.16.40.2 (quic)     |
                         +-----------+------------+
                                     ^
         +---------------------------+---------------------------+
         |                           |                           |
         |                           |                           |
+--------+--------+        +---------+---------+        +--------+--------+
|   github_net    |        |   example_net     |        |  quic_nginx_net  |
|  (internal)     |        |   (internal)      |        |   (internal)     |
|-----------------|        |-------------------|        |------------------|
| DNS alias:      |        | DNS alias:        |        | DNS alias:       |
| github.com ---> |        | example.com ----> |        | quic.nginx.org ->|
| 172.16.20.2     |        | 172.16.30.2       |        | 172.16.40.2      |
+--------+--------+        +---------+---------+        +--------+--------+
         |                           |                           |
         |                           |                           |
   +-----+-----+               +-----+-----+               +-----+-----+
   |   App 1   |               |   App 2   |               |   App 2   |
   |-----------|               |-----------|               |-----------|
   | curl https|               | curl https|               | curl https|
   |   github  |               |  example  |               |   quic    |
   +-----------+               +-----------+               +-----------+

⚠️ Pitfalls & Lessons Learned

1. Docker’s embedded DNS always resolves container aliases

Even if you configure custom DNS servers, Docker’s internal DNS will always resolve:

github.com → 172.16.20.2

inside the proxy container.

This causes infinite loops unless you override /etc/resolv.conf.

2. QUIC/HTTP‑3 does not work through GOST

We initially attempted to support QUIC/HTTP‑3 by enabling UDP forwarding:

-L=udp://172.16.40.2:443/quic.nginx.org:443

This failed with errors like:

curl: (56) QUIC connection has been shut down

Reason: QUIC is stateful over UDP and requires NAT‑style forwarding.
GOST is a TCP/UDP proxy, not a NAT router, so QUIC flows break.

We ultimately dropped the HTTP/3 requirement for this solution.

3. No need for static IPs inside app containers

Unlike the SequentialRead approach, we do not need:

  • /etc/hosts hacks
  • /etc/nsswitch.conf hacks
  • static IPs inside app containers
  • running apps as root
  • patching entrypoints

Docker DNS aliasing handles everything cleanly.

4. Internal networks exclusively

Currently this approach only works with containers which are connected exclusively to internal networks. This means it can't work for containers which publish ports on the host. E.g. it can't be used to restrict outbound traffic from a traefik reverse proxy container with port 443 mapped to the host.


đź”­ Future Research

Although this solution works well for TCP‑only traffic, there is a clear path forward for supporting QUIC/HTTP‑3 and more advanced egress policies.

Router‑in‑a‑container

A privileged container running:

  • iptables or nftables
  • NAT (MASQUERADE)
  • dnsmasq or CoreDNS for dynamic FQDN → IP sets

This would allow:

  • QUIC/HTTP‑3 end‑to‑end
  • dynamic FQDN‑based ACLs
  • transparent L3/L4 routing
  • no TLS termination

📚 Sources & References


đź’¬ Closing Thoughts

Docker doesn’t make domain‑based egress control easy, but with internal networks, DNS aliases, and a unified TCP proxy, it’s possible to build a clean, maintainable solution that works across many applications without modifying them.

Dropping QUIC/HTTP‑3 support was a pragmatic compromise, but the architecture remains solid—and future work on NAT‑based routing could bring full protocol transparency.

If you’re running Docker Compose in production and want Kubernetes‑style FQDN policies, this pattern is a strong foundation.

services:
# ---------------------------------------------------
# Simple but Effective Outbound "Firewall" using Vanilla Docker-Compose
# ---------------------------------------------------
proxy:
image: gogost/gost:3.2
container_name: unified-proxy
command: >
-L=tcp://172.16.10.2:443/github.com:443
-L=tcp://172.16.20.2:443/example.com:443
-L=tcp://172.16.30.2:443/quic.nginx.org:443
volumes:
- /etc/resolv.conf:/etc/resolv.conf:ro # <‑‑ override Docker DNS
networks:
github_net:
ipv4_address: 172.16.10.2
aliases:
- github.com
example_net:
ipv4_address: 172.16.20.2
aliases:
- example.com
quic_nginx_net:
ipv4_address: 172.16.30.2
aliases:
- quic.nginx.org
external_net:
user: "65532:65532"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges=true
tmpfs:
- /tmp
- /run
app1:
image: alpine/curl
hostname: app1
configs:
- demo
command: sh /demo
depends_on:
- proxy
networks:
- github_net
app2:
image: alpine/curl
hostname: app2
configs:
- demo
command: sh /demo
depends_on:
- proxy
networks:
- github_net
- example_net
- quic_nginx_net
networks:
github_net:
internal: true
ipam:
config:
- subnet: 172.16.10.0/24
example_net:
internal: true
ipam:
config:
- subnet: 172.16.20.0/24
quic_nginx_net:
internal: true
ipam:
config:
- subnet: 172.16.30.0/24
external_net:
driver: bridge
configs:
demo:
content: |
#!/bin/sh
while :; do
for u in \
"https://github.com" \
"https://example.com" \
"https://quic.nginx.org"
do
curl -o /dev/null -s "$${u}" && result=success || result=fail
echo "$(hostname): $${u} -> $${result}"
done
sleep 5
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment