How To: Install and Securely Configure Docker Compose with `/data/docker`, Global Networks, and a Default Management Stack

How To: Install and Securely Configure Docker Compose with /data/docker, Global Networks, and a Default Management Stack

Purpose: Install Docker and Docker Compose, move Docker’s data root to /data/docker, define global Docker networks (SERVERS, DMZ, INTERNAL), and deploy a default non-root stack with Nginx (reverse proxy), Portainer, and Homarr.

Applies To: Rocky Linux 8/9, Ubuntu 22.04/24.04 (amd64)

Last Updated: YYYY-MM-DD

Difficulty: Intermediate–Advanced


Overview

This extended guide adds:

Docker’s user-defined bridge networks isolate traffic by default; only containers on the same network can see each other, and the traffic stays on the host unless ports are published.23

💡 TIP: Think of each Docker network as a dedicated Layer 2 domain internal to the host. Use networks to express trust boundaries: DMZ (internet-facing), SERVERS (backend), INTERNAL (sidecar/tooling).42


Prerequisites

All from the previous guide still apply:

Additionally:


Step-by-Step Instructions

1. Create Global Docker Networks

Create user-defined bridge networks once at the host level.52

# DMZ: public-facing frontends, reverse proxy
docker network create DMZ

# SERVERS: backend services only
docker network create SERVERS

# INTERNAL: monitoring, admin, glue between others
docker network create INTERNAL

Verify:

docker network ls

You should see DMZ, SERVERS, INTERNAL with driver bridge.

💡 TIP: User-defined bridge networks provide built-in name-based service discovery and container-to-container isolation, unlike the default bridge network.21


2. Reference Global Networks from Compose Stacks

Compose can attach services to pre-existing networks by marking them as external.1

Example top-level networks section (reusable pattern):

networks:
  DMZ:
    external: true
  SERVERS:
    external: true
  INTERNAL:
    external: true

Any stack that includes the above networks section can attach services to these same shared networks, allowing cross-stack communication without recreating networks.1

⚠️ WARNING: Do not redefine these networks with driver: bridge inside other Compose files; use external: true so they all point to the same global network objects.1


3. Default Stack Layout and Users

Assume:

Create host users:

sudo useradd -r -u 10100 -s /usr/sbin/nologin nginxrp
sudo useradd -r -u 10101 -s /usr/sbin/nologin portainer
sudo useradd -r -u 10102 -s /usr/sbin/nologin homarr

Create data directories:

sudo mkdir -p /data/docker/volumes/nginx/conf
sudo mkdir -p /data/docker/volumes/nginx/html
sudo mkdir -p /data/docker/volumes/portainer/data
sudo mkdir -p /data/docker/volumes/homarr/config

sudo chown -R 10100:10100 /data/docker/volumes/nginx
sudo chown -R 10101:10101 /data/docker/volumes/portainer
sudo chown -R 10102:10102 /data/docker/volumes/homarr
sudo chmod -R 750 /data/docker/volumes/nginx \
                  /data/docker/volumes/portainer \
                  /data/docker/volumes/homarr

On Rocky with SELinux enforcing:

sudo semanage fcontext -a -t container_file_t "/data/docker/volumes(/.*)?"
sudo restorecon -Rv /data/docker/volumes

4. Example Default Stack docker-compose.yml

Create a directory, for example /data/docker/stacks/core:

sudo mkdir -p /data/docker/stacks/core
cd /data/docker/stacks/core

Create docker-compose.yml:

version: "3.9"

services:
  reverse-proxy:
    image: nginx:alpine
    container_name: reverse-proxy
    user: "10100:10100"
    networks:
      - DMZ
      - INTERNAL
    ports:
      - "80:8080"    # Nginx listens on 8080 in container, 80 on host
      - "443:8443"   # 8443 in container, 443 on host
    volumes:
      - /data/docker/volumes/nginx/conf:/etc/nginx/conf.d:ro
      - /data/docker/volumes/nginx/html:/usr/share/nginx/html:ro
    restart: unless-stopped

  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    user: "10101:10101"
    networks:
      - INTERNAL
    # Bind UI only to LAN IP or localhost as desired
    ports:
      - "127.0.0.1:9443:9443"
    volumes:
      - /data/docker/volumes/portainer/data:/data
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: unless-stopped

  homarr:
    image: ghcr.io/ajnart/homarr:latest
    container_name: homarr
    user: "10102:10102"
    networks:
      - INTERNAL
    volumes:
      - /data/docker/volumes/homarr/config:/app/data
    restart: unless-stopped

networks:
  DMZ:
    external: true
  SERVERS:
    external: true
  INTERNAL:
    external: true

Key points:

⚠️ WARNING: Mounting the Docker socket (/var/run/docker.sock) is powerful; keep Portainer on a restricted network and control who can reach the UI. Consider a docker-socket-proxy pattern if you want finer-grained control.4


5. Wiring Other Stacks to the Global Networks

Any future stack can use the same global networks. For example, an “app” stack:

version: "3.9"

services:
  myapp:
    image: myorg/myapp:latest
    container_name: myapp
    user: "10010:10010"
    networks:
      - SERVERS
      - INTERNAL
    volumes:
      - /data/docker/volumes/myapp:/app/data
    restart: unless-stopped

  mydb:
    image: postgres:16-alpine
    container_name: mydb
    user: "10011:10011"
    networks:
      - SERVERS
    volumes:
      - /data/docker/volumes/mydb:/var/lib/postgresql/data
    restart: unless-stopped

networks:
  DMZ:
    external: true
  SERVERS:
    external: true
  INTERNAL:
    external: true

Effects:

Inside the Nginx config you can then use myapp:port as upstream, leveraging Docker’s internal DNS on the shared network.21


Verification

  1. Check networks exist and are shared
docker network ls

You should see DMZ, SERVERS, INTERNAL as bridge networks.

  1. Bring up the core stack
cd /data/docker/stacks/core
docker compose up -d
docker compose ps
  1. Confirm network membership
docker inspect reverse-proxy | grep -A3 '"DMZ"' 
docker inspect reverse-proxy | grep -A3 '"INTERNAL"'
docker inspect portainer | grep -A3 '"INTERNAL"'

Each service should show connectivity to the intended networks.52

  1. Test name resolution across stacks

After starting another stack on INTERNAL or SERVERS, from inside reverse-proxy:

docker exec -it reverse-proxy sh
ping -c 3 myapp

You should see successful resolution and replies (assuming a myapp service on a shared network).21

  1. Verify non-root inside containers
docker exec -it reverse-proxy id
docker exec -it portainer id
docker exec -it homarr id

UIDs must be non-zero and not root.76


Troubleshooting


Security Considerations

⚠️ WARNING: Avoid network_mode: host unless absolutely necessary; it bypasses Docker’s network isolation.810


Notes/Tips


To download this as a .md file, copy the raw Markdown above into a file or use the copy button.

How would you like to map your Fortigate/Ubiquiti VLANs to these Docker networks (DMZ/SERVERS/INTERNAL) in your homelab, and do you want Nginx to terminate TLS for everything or only for some services? 1213

  1. https://docs.docker.com/compose/how-tos/networking/  2 3 4 5 6 7 8 9 10

  2. https://docs.docker.com/engine/network/  2 3 4 5 6 7 8

  3. https://www.reddit.com/r/docker/comments/klj6vn/docker_containers_exposing_containers_to_the/  2 3 4 5

  4. https://www.reddit.com/r/selfhosted/comments/1sjivk5/how_to_properly_set_up_containers_networks/  2 3 4 5 6

  5. https://spacelift.io/blog/docker-networking  2 3 4

  6. https://lours.me/posts/compose-tip-014-non-root-users/  2 3

  7. https://oneuptime.com/blog/post/2026-01-16-docker-run-non-root-user/view  2

  8. https://forums.balena.io/t/reaching-other-container-with-network-mode-host/217325  2

  9. https://www.youtube.com/watch?v=WDQIv-Kd6hk 

  10. https://developer.toradex.com/torizon/application-development/use-cases/networking-connectivity/how-to-setup-network-between-containers/  2

  11. https://www.reddit.com/r/docker/comments/ynug2i/running_docker_containers_is_equivalent_to_having/ 

  12. https://www.youtube.com/watch?v=itZ_x_nDBxU 

  13. https://github.com/moby/moby/issues/30053