Skip to content

Architecture at a glance

The lab is one writable domain controller plus a set of optional sibling services that behave like separate machines on the LAN. The tricky part is networking: a domain controller cannot be cleanly contained, and the sibling services need real LAN identities, so two different Docker network modes are in play at once.

flowchart TB
  client["LAN clients"]
  subgraph LAN["LAN · 10.22.22.0/24"]
    ndc1["dc1 · 10.22.22.22<br/><b>host networking</b>"]
    nprint1["print1 · 10.22.22.23<br/>macvlan · member + CUPS"]
    nrodc1["rodc1 · 10.22.22.24<br/>macvlan · read-only replica"]
    nprintsnmp["print-snmp · 10.22.22.25<br/>macvlan · SNMP device"]
    nprintipp["print-ipp · 10.22.22.26<br/>macvlan · IPP device"]
  end
  client --> nprint1
  client --> ndc1
  nprint1 -. "/32 detour" .-> ndc1
  nrodc1 -. "/32 detour" .-> ndc1
  nprintsnmp -. "/32 detour" .-> ndc1
  nprintipp -. "/32 detour" .-> ndc1

Generated from .env by make diagrams. Solid arrows are the LAN paths clients use; dotted arrows are the /32 bridge detour each macvlan child needs to reach the DC (its own macvlan parent).

  • dc1 runs on network_mode: host. A domain controller serves DNS (53), Kerberos (88), LDAP (389/636), SMB (445), kpasswd (464), the Global Catalog (3268/3269), and a dynamic RPC range from 49152. You cannot publish that last range with -p, so the DC owns those ports on the host’s LAN IP, exactly as a real DC would. See Why host networking.
  • print1, rodc1, print-snmp, print-ipp each get their own LAN IP via macvlan, so a real Windows client mounts \\print1 on native port 445 with no NAT. But a macvlan child cannot address its own parent host (which is dc1), so these services also attach the shared samba bridge and pin a /32 route to the DC over it. See The macvlan + bridge detour.

An app you are testing (a fax broker, an IdP, a directory-aware service) joins the shared external samba bridge network and reaches the DC through the host gateway:

services:
my-app:
networks: [samba]
extra_hosts:
- "host.docker.internal:host-gateway"
dns:
- 172.17.0.1 # host gateway = the DC's internal DNS
# LDAP URL: ldap://host.docker.internal
# Realm: value of SAMBA_REALM
networks:
samba:
external: true

For TLS (LDAPS on 636) you must connect by the certificate name, not through the gateway. The details and the reason are on the host networking page.

The defaults below come from .env. The full table of variables is in the .env reference, and the addressing rationale is in the network map.

HostDefault IPNetwork modeServes
dc110.22.22.22hostDNS, Kerberos, LDAP/LDAPS, SMB, GC
print110.22.22.23macvlan + bridgeSMB print (445), CUPS/IPP (631)
rodc110.22.22.24macvlan + bridgeread-only DNS + LDAP
print-snmp10.22.22.25macvlan + bridgeSNMP (161) printer device MIBs
print-ipp10.22.22.26macvlan + bridgeIPP Everywhere (8631-8633)

The macvlan siblings above live in this repo’s compose. The lab also has three larger sibling stacks, each its own repository, that turn the DC into a working hospital: OpenEMR (the electronic record), Simulated Hospital (a live HL7v2 feed), and the OpenIntegrationEngine (the interface engine that routes the feed into the record). Together with the DC they form four planes — identity, application, activity, integration — connected by a dedicated hl7-bus network.

See The four-plane architecture for how they fit and Run the clinical ecosystem for the runbook. Simulated Hospital has its own deep dive.

All durable state lives in named volumes (samba-data holds the AD database, samba-conf the config, plus logs and home directories). Provisioning keys off sam.ldb, so make down followed by make up brings the same domain back. make nuke deletes the volumes to start fresh. That disposability is what makes the lab safe to experiment in.