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)
| Tag | Variant |
|---|---|
latest, 1.2, multi-vlan, 2.0 | Multi-interface — one VLAN/DHCP scope per NIC, editable scope in the UI, live Topology view. |
1.1, customizable | Single interface, scope editable in the UI. |
1.0, read-only-config | Single interface, scope from env only (UI read-only). |
dhcpd — one scope per interface.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)
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_SUBNETmust be the same network asSTATIC_IP, ordhcpdwon't serve it. Keep the pool clear of the server IP and the gateway.
| Var | Default | |
|---|---|---|
STATIC_IP | 192.168.254.100/24 | container's static IP (must include prefix) |
STATIC_GW | 192.168.254.1 | default gateway |
STATIC_IFACE | eth0 | container interface |
| Var | Notes |
|---|---|
DHCP_SUBNET | required |
DHCP_NETMASK | default 255.255.255.0 |
DHCP_RANGE_START | required |
DHCP_RANGE_END | required |
DHCP_GATEWAY | CSV allowed |
DHCP_DNS | CSV |
DHCP_NTP | CSV, optional |
DHCP_DOMAIN | optional |
DHCP_LEASE_DEFAULT | seconds, optional |
DHCP_LEASE_MAX | seconds, optional |
DHCP_BROADCAST | auto-derived if unset |
DHCP_FORCE_SEED | true = re-seed scope every boot (UI edits become transient) |
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:
| Var | Notes |
|---|---|
NET{i}_IFACE | interface name (default eth{i-1}) |
NET{i}_IP | interface IP with prefix, e.g. 192.168.10.1/24 — subnet/netmask/broadcast derived from it |
NET{i}_RANGE_START / NET{i}_RANGE_END | pool |
NET{i}_GATEWAY | gateway handed to clients (CSV) |
NET{i}_DNS / NET{i}_NTP | CSV, optional |
NET{i}_DOMAIN | optional |
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
| Var | Notes |
|---|---|
USER_NAME | login user (default admin) |
USER_PASS | SHA-256 hex hash of the password; set to require login |
WEBUI_USER / WEBUI_PASSWORD | plaintext fallback |
DHCP needs Layer-2 broadcast access to the LAN it serves, so the container must sit directly on that segment:
--network host (simplest), or a macvlan network on the LAN NIC.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.
dhcpdserves every interface that has a matchingsubnetdeclaration — you just need the interface's IP (STATIC_IP) to fall insideDHCP_SUBNET.
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>"
access_vlan_id to that VLAN ID./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).
Open http://STATIC_IP:8080.
| Tab | Purpose |
|---|---|
| Global | edit global options (authoritative, lease times, domain, DNS, NTP) |
| Subnets | edit subnets/pools — network, netmask, gateway, DNS/NTP, ranges; broadcast auto-fills from the netmask |
| Reservations | add/remove static host mappings (name → MAC → fixed IP) |
| Leases | active leases parsed from the lease DB |
| Topology | live diagram: container → interfaces → VLAN clouds → leased devices (auto-refreshes) |
| dhcpd.conf | the 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; uselatest/customizablefor an editable UI.
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 passwordGenerate 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).
docker compose up -d --build # uses network_mode: host
Then open http://localhost:8080.
On macOS/Windows Docker there is no real
--network host, sodhcpdcan'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.
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).
| Method | Path | Purpose |
|---|---|---|
| GET | /api/config | current structured config |
| PUT | /api/config | validate + persist + reload (full scope) |
| POST | /api/config/validate | dhcpd -t dry-run |
| PUT | /api/reservations | replace reservations + reload dhcpd |
| GET | /api/topology | interfaces, their networks, and leases (drives the Topology tab) |
| GET | /api/dhcpd.conf | rendered config preview |
| GET | /api/leases | parsed active leases |
| GET | /api/service/status | RUNNING / STOPPED |
| POST | /api/service/restart | reload dhcpd |
| GET | /healthz | liveness probe |
| GET | /api/docs | OpenAPI / Swagger UI |
Content type
Image
Digest
sha256:f6a89909b…
Size
74.9 MB
Last updated
4 months ago
docker pull zedmanny/dhcpd-container