A unified web app for complete control of your network infrastructure, designed as an integrated frontend for BIND (DNS), Kea DHCP, and with automatic support for Let’s Encrypt certificates.
The application includes:
- Integrated frontend
- Administrator authentication
- Simple and lightweight SQLite database
- Automatic generation of DNS and DHCP configurations from a defined domain
- Local versioning of configurations, with change history and rollback capability
This solution allows you to manage hosts, DNS zones, DHCP leases, and certificates from a single centralized interface, reducing manual errors and greatly simplifying operations.
Designed to run easily via Docker and Docker Compose, with configuration via environment variables.
This project is currently under development. For upcoming tasks and planned improvements, please refer to the TODO list file.
- Static frontend served by the application (
FRONTEND_DIR) - Persistent SQLite database (
/data/database.db) - Configurable logging to console and/or file
- Login protection with configurable rate-limit
- Admin credentials configurable via env or Docker secrets
- Support for
SESSION_SECRET: custom key for cookies (if missing, it is generated automatically)
- Docker = 20.x
- Docker Compose = v2
project/
+- docker-compose.yml
+- .env
+- secrets/
¦ +- admin_password_hash
+- data/
# --- Host & Web ---
DOMAIN=example.com
EXTERNAL_NAME=dyndns.example.com
HTTP_PORT=8000
# --- Admin ---
ADMIN_USER=admin
ADMIN_PASSWORD=admin
# In production use ADMIN_PASSWORD_HASH_FILE
# --- Login rate limit ---
LOGIN_MAX_ATTEMPTS=5
LOGIN_WINDOW_SECONDS=600
# --- Log ---
LOG_LEVEL=INFO
LOG_TO_FILE=false
# --- Session secret (optional but recommended in production) ---
# SESSION_SECRET=****ReplaceWithYourSecret*****If SESSION_SECRET is not set, the app generates a random key on each restart -> existing sessions become invalid.
services:
network-manager:
image: ghcr.io/xraver/network-manager:latest
container_name: network-manager
restart: unless-stopped
ports:
- "${HTTP_PORT:-8000}:8000"
environment:
# Frontend
FRONTEND_DIR: "/app/frontend"
# Database
DB_FILE: "/data/database.db"
DB_RESET: "${DB_RESET:-false}"
# Log
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
LOG_TO_FILE: "${LOG_TO_FILE:-false}"
LOG_FILE: "/data/app.log"
LOG_ACCESS_FILE: "/data/access.log"
# Host
DOMAIN: "${DOMAIN:-example.com}"
EXTERNAL_NAME: "${EXTERNAL_NAME:-dyndns.example.com}"
# Web
HTTP_PORT: "${HTTP_PORT:-8000}"
LOGIN_MAX_ATTEMPTS: "${LOGIN_MAX_ATTEMPTS:-5}"
LOGIN_WINDOW_SECONDS: "${LOGIN_WINDOW_SECONDS:-600}"
# Admin
ADMIN_USER: "${ADMIN_USER:-admin}"
ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}"
ADMIN_PASSWORD_HASH_FILE: "/run/secrets/admin_password_hash"
# Session key (optional)
# SESSION_SECRET: "****ReplaceWithYourSecret*****"
volumes:
- ./data:/data
secrets:
- admin_password_hash
secrets:
admin_password_hash:
file: ./secrets/admin_password_hash| Variable | Default | Description |
|---|---|---|
FRONTEND_DIR |
/app/frontend | Frontend directory |
DATA_PATH |
/data | Data Path for DB and Backups |
DB_FILE |
database.db | SQLite file |
DB_RESET |
false | Reset DB on every startup |
LOG_LEVEL |
info | Log level |
LOG_TO_FILE |
false | Enable file logging |
LOG_FILE |
app.log | Application log file |
LOG_ACCESS_FILE |
access.log | HTTP access log |
DOMAIN |
example.com | Public domain |
EXTERNAL_NAME |
dyndns.example.com | External Name |
HTTP_HOST |
0.0.0.0 | IP address the server binds to |
HTTP_PORT |
8000 | Internal HTTP port |
LOGIN_MAX_ATTEMPTS |
5 | Login attempts |
LOGIN_WINDOW_SECONDS |
600 | Attempt window |
ADMIN_USER |
admin | Admin username |
ADMIN_PASSWORD |
admin | Admin password (development) |
ADMIN_PASSWORD_HASH_FILE |
/run/secrets/admin_password_hash | Admin password hash |
SESSION_SECRET |
(auto-generated) | Session secret |
DNS_HOST_FILE |
/dns/etc/{DOMAIN}/hosts.inc | BIND9 Hosts file |
DNS_ALIAS_FILE |
/dns/etc/{DOMAIN}/alias.inc | BIND9 Alias file |
DNS_REVERSE_FILE |
/dns/etc/reverse/hosts.inc | BIND9 Reverse Hosts file |
DHCP4_HOST_FILE |
/dhcp/etc/hosts-ipv4.json | KEA-DHCP4 Hosts file |
DHCP4_LEASES_FILE |
/dhcp/lib/dhcp4.leases | KEA-DHCP4 leases file |
DHCP6_HOST_FILE |
/dhcp/etc/hosts-ipv6.json | KEA-DHCP6 Hosts file |
DHCP6_LEASES_FILE |
/dhcp/lib/dhcp6.leases | KEA-DHCP6 leases file |
ADMIN_USER=admin
ADMIN_PASSWORD=adminpython - <<‘PY’
import bcrypt
pwd = b“SecurePassword”
print(bcrypt.hashpw(pwd, bcrypt.gensalt()).decode())
PYSave the hash in ./secrets/admin_password_hash.
Docker compose will mount it in:
/run/secrets/admin_password_hash
Used to sign cookies. If set, the app generates a new key each time and all sessions expire on each restart. Generate a strong secret:
openssl rand -base64 64Then:
SESSION_SECRET: “paste-the-secret-here”
Map /data as a volume:
volumes:
- ./data:/dataNormal startup:
docker compose upIn the background:
docker compose up -dLog:
docker compose logs -f network-managerContainer recreation:
docker compose up -d --force-recreateContainer rebuild & recreation:
docker compose up --build -d --force-recreate
- Use
ADMIN_PASSWORD_HASH_FILEin production - Disable
SESSION_SECRETfor automatic generation - Set
secure=Trueon cookies if you use HTTPS - Use a reverse proxy with TLS
- Do not put passwords in the repository
MIT – see the local LICENSE file © Giorgio Ravera