Sign inSign up

zedmanny/dhcpd-container

By zedmanny

•Updated 4 months ago

Image
0

2.0K

zedmanny/dhcpd-container repository overview

⁠dhcpd-container

An ISC dhcpd DHCP server packaged with a web UI for managing pools, ranges, DNS, NTP, gateways and static reservations. Runs anywhere containers do — any Linux host with Docker, a VM, or an edge/container platform.

Image: zedmanny/dhcpd-container:latest (multi-arch: amd64 + arm64)

⁠Tags
TagVariant
latest, 1.2, multi-vlan, 2.0Multi-interface — one VLAN/DHCP scope per NIC, editable scope in the UI, live Topology view.
1.1, customizableSingle interface, scope editable in the UI.
1.0, read-only-configSingle interface, scope from env only (UI read-only).

⁠What it does

  1. Serves DHCP to one or more VLANs using ISC dhcpd — one scope per interface.
  2. Web UI on port 8080 — edit scopes, manage reservations, view leases, and a live network topology.
  3. Configures itself from environment variables so it can start serving on first boot with no manual steps.

⁠How it works

The container assigns itself a static IP, seeds a DHCP scope from environment variables, then runs dhcpd and the web UI under supervisord. All state lives in the /data volume (config.json, dhcpd.conf, dhcpd.leases) so it survives restarts.

env vars ─► config.json (source of truth) ─► dhcpd.conf ─► dhcpd (all interfaces)
                        ▲
                web UI (scopes + reservations)

⁠Configuration (environment variables)

Set these as container environment variables — via docker run -e, Compose environment:, or whatever mechanism your platform uses to pass container env (including cloud-init runcmd on edge platforms). With DHCP_FORCE_SEED=true the scope is applied on every boot, so the container serves DHCP immediately.

docker run -d --name dhcpd --network host \
  --cap-add NET_ADMIN --cap-add NET_RAW \
  -v dhcpd-data:/data \
  -e STATIC_IP=192.168.254.100/24 \
  -e STATIC_GW=192.168.254.1 \
  -e DHCP_SUBNET=192.168.254.0 \
  -e DHCP_NETMASK=255.255.255.0 \
  -e DHCP_RANGE_START=192.168.254.150 \
  -e DHCP_RANGE_END=192.168.254.200 \
  -e DHCP_GATEWAY=192.168.254.1 \
  -e DHCP_DNS=1.1.1.1 \
  -e DHCP_FORCE_SEED=true \
  zedmanny/dhcpd-container:latest

DHCP_SUBNET must be the same network as STATIC_IP, or dhcpd won't serve it. Keep the pool clear of the server IP and the gateway.

⁠Static IP (server's own address)
VarDefault
STATIC_IP192.168.254.100/24container's static IP (must include prefix)
STATIC_GW192.168.254.1default gateway
STATIC_IFACEeth0container interface
⁠DHCP scope
VarNotes
DHCP_SUBNETrequired
DHCP_NETMASKdefault 255.255.255.0
DHCP_RANGE_STARTrequired
DHCP_RANGE_ENDrequired
DHCP_GATEWAYCSV allowed
DHCP_DNSCSV
DHCP_NTPCSV, optional
DHCP_DOMAINoptional
DHCP_LEASE_DEFAULTseconds, optional
DHCP_LEASE_MAXseconds, optional
DHCP_BROADCASTauto-derived if unset
DHCP_FORCE_SEEDtrue = re-seed scope every boot (UI edits become transient)
⁠Multiple interfaces (one VLAN/scope per NIC)

For a container with several NICs, use indexed NET{i}_* groups (1–8) instead of the single STATIC_IP/DHCP_* set. Each interface gets its own static IP and DHCP scope, and dhcpd serves them all:

VarNotes
NET{i}_IFACEinterface name (default eth{i-1})
NET{i}_IPinterface IP with prefix, e.g. 192.168.10.1/24 — subnet/netmask/broadcast derived from it
NET{i}_RANGE_START / NET{i}_RANGE_ENDpool
NET{i}_GATEWAYgateway handed to clients (CSV)
NET{i}_DNS / NET{i}_NTPCSV, optional
NET{i}_DOMAINoptional

Example — eth0 → VLAN 254, eth1 → VLAN 255:

-e NET1_IFACE=eth0 -e NET1_IP=192.168.254.100/24 \
-e NET1_RANGE_START=192.168.254.150 -e NET1_RANGE_END=192.168.254.200 \
-e NET1_GATEWAY=192.168.254.1 -e NET1_DNS=1.1.1.1 \
-e NET2_IFACE=eth1 -e NET2_IP=192.168.255.100/24 \
-e NET2_RANGE_START=192.168.255.150 -e NET2_RANGE_END=192.168.255.200 \
-e NET2_GATEWAY=192.168.255.1 -e NET2_DNS=1.1.1.1
⁠Web UI auth
VarNotes
USER_NAMElogin user (default admin)
USER_PASSSHA-256 hex hash of the password; set to require login
WEBUI_USER / WEBUI_PASSWORDplaintext fallback

⁠Networking

DHCP needs Layer-2 broadcast access to the LAN it serves, so the container must sit directly on that segment:

  • Linux host: --network host (simplest), or a macvlan network on the LAN NIC.
  • VLANs: put the container on the target VLAN (e.g. an access port / tagged sub-interface on the host or switch).

The container needs NET_ADMIN / NET_RAW to set its static IP and open raw DHCP sockets. Expose TCP 8080 for the web UI; DHCP uses UDP 67.

dhcpd serves every interface that has a matching subnet declaration — you just need the interface's IP (STATIC_IP) to fall inside DHCP_SUBNET.

⁠Deploy on EVE-OS

Same image and same variables — EVE just delivers them through the app's cloud-init runcmd:

#cloud-config
runcmd:
 - HOSTNAME="dhcpd-1"
 - EVE_CONTAINER_NO_POWEROFF=1
 - STATIC_IP="192.168.254.100/24"
 - STATIC_GW="192.168.254.1"
 - DHCP_SUBNET="192.168.254.0"
 - DHCP_NETMASK="255.255.255.0"
 - DHCP_RANGE_START="192.168.254.150"
 - DHCP_RANGE_END="192.168.254.200"
 - DHCP_GATEWAY="192.168.254.1"
 - DHCP_DNS="1.1.1.1"
 - DHCP_FORCE_SEED="true"
 - USER_NAME="admin"
 - USER_PASS="<sha256-hash>"
  • Attach the app to a Switch (L2) network instance on the port facing the LAN.
  • For a tagged VLAN, set the app interface's access_vlan_id to that VLAN ID.
  • Mount a persistent volume at /data.

For multiple VLANs, give the app one interface per NIC (eth0, eth1, …), each with its own access_vlan_id, and use the NET{i}_* vars above (NET1 → eth0, NET2 → eth1, …).

EVE-only vars: HOSTNAME (container hostname) and EVE_CONTAINER_NO_POWEROFF=1 (keep the container running).

⁠Web UI

Open http://STATIC_IP:8080⁠.

TabPurpose
Globaledit global options (authoritative, lease times, domain, DNS, NTP)
Subnetsedit subnets/pools — network, netmask, gateway, DNS/NTP, ranges; broadcast auto-fills from the netmask
Reservationsadd/remove static host mappings (name → MAC → fixed IP)
Leasesactive leases parsed from the lease DB
Topologylive diagram: container → interfaces → VLAN clouds → leased devices (auto-refreshes)
dhcpd.confthe rendered config dhcpd is running

Validate dry-runs dhcpd -t; Save & Apply persists to config.json and reloads dhcpd. Edits persist on /data — unless DHCP_FORCE_SEED=true, which reasserts the env scope on every boot (use it for fixed, declarative deploys).

The read-only-config (1.0) tag shows the scope read-only instead; use latest/customizable for an editable UI.

⁠Authentication

The web UI is unauthenticated by default. Set a password to require HTTP Basic auth.

Preferred — hashed (safe to put in config/cloud-init):

  • USER_NAME — login user (default admin)
  • USER_PASS — SHA-256 hex hash of the password

Generate the hash:

printf '%s' 'yourpassword' | openssl dgst -sha256 | awk '{print $NF}'
# or: printf '%s' 'yourpassword' | sha256sum | cut -d' ' -f1

Plaintext fallback: WEBUI_USER / WEBUI_PASSWORD (e.g. -e WEBUI_PASSWORD=yoursecret).

⁠Run locally (Linux, for testing)

docker compose up -d --build      # uses network_mode: host

Then open http://localhost:8080⁠.

On macOS/Windows Docker there is no real --network host, so dhcpd can't bind to a LAN and will restart in a loop — the web UI still works for trying out the flow. Real DHCP serving needs Linux host networking or macvlan.

⁠Configuration reference

config.json (in /data) is the source of truth. It's seeded from the environment and holds reservations added in the UI. Example:

{
  "global": {
    "authoritative": true,
    "default_lease_time": 600,
    "max_lease_time": 7200,
    "domain_name": "lan",
    "dns_servers": ["1.1.1.1", "8.8.8.8"],
    "ntp_servers": ["192.168.254.1"]
  },
  "subnets": [
    {
      "subnet": "192.168.254.0",
      "netmask": "255.255.255.0",
      "ranges": [{ "start": "192.168.254.150", "end": "192.168.254.200" }],
      "routers": ["192.168.254.1"],
      "broadcast_address": "192.168.254.255"
    }
  ],
  "reservations": [
    { "name": "printer", "mac": "aa:bb:cc:dd:ee:ff", "ip": "192.168.254.50" }
  ]
}

NTP/DNS values must be IP addresses (ISC dhcpd option types require it).

⁠API

MethodPathPurpose
GET/api/configcurrent structured config
PUT/api/configvalidate + persist + reload (full scope)
POST/api/config/validatedhcpd -t dry-run
PUT/api/reservationsreplace reservations + reload dhcpd
GET/api/topologyinterfaces, their networks, and leases (drives the Topology tab)
GET/api/dhcpd.confrendered config preview
GET/api/leasesparsed active leases
GET/api/service/statusRUNNING / STOPPED
POST/api/service/restartreload dhcpd
GET/healthzliveness probe
GET/api/docsOpenAPI / Swagger UI

Tag summary

Content type

Image

Digest

sha256:f6a89909b…

Size

74.9 MB

Last updated

4 months ago

docker pull zedmanny/dhcpd-container