EliHole is an open-source, self-hosted DNS sinkhole built on Elixir — a Pi-hole alternative that blocks ads and trackers for every device on your network. This guide walks you through the full EliHole install with Docker Compose: configuring the environment, getting DNS answered on port 53, pointing your network at it, and keeping it updated.
The short version:
cp .env.example .env
# edit .env: SECRET_KEY_BASE, ADMIN_USERNAME, ADMIN_PASSWORD
docker compose up -d Postgres is included in the Compose file and migrations run automatically on startup. The rest of this page covers the details that matter in production.
Prerequisites
- A Linux host with Docker and Docker Compose v2 (
docker compose, the plugin). The legacydocker-composev1 binary fails on the${VAR:?}interpolation syntax used in the Compose file — ifdocker compose versionprints nothing, upgrade before continuing. - A clone of the EliHole repository on the host.
- Roughly 1 GB of free RAM during the image build (see Low-RAM hosts if you don’t have it).
Note that EliHole’s app container uses network_mode: host for low-latency UDP, so the DNS and web ports bind directly on the host rather than through Docker’s port mapping.
Configure the environment
From the repository root, copy the example environment file:
cp .env.example .env Open .env and set three values:
SECRET_KEY_BASE — the Phoenix secret used to sign sessions. Generate one:
openssl rand -base64 48 ADMIN_USERNAME and ADMIN_PASSWORD — credentials for the admin panel. The password must be at least 8 characters. The demo Compose setup ships with admin / administrator — change both before exposing the instance to anything beyond your desk.
Other variables you may want to touch now or later:
| Variable | Default | Purpose |
|---|---|---|
DNS_PORT | 5354 | UDP port the DNS server listens on |
DNS_UPSTREAMS | 8.8.8.8:53,8.8.4.4:53 | Upstream resolvers, comma-separated |
PHX_PORT | 4410 | Web UI port |
PHX_HOST | — | Public hostname for the web UI |
FORCE_SSL | — | Redirect web traffic to HTTPS |
DOT_PORT / DOT_CERT_PATH / DOT_KEY_PATH | — | DNS-over-TLS; disabled until a cert is configured |
INSTANCE_ROLE | standalone | standalone, master, or slave for multi-instance setups |
Start the stack
docker compose up -d This starts Postgres and the EliHole app. Database migrations run automatically on startup — no manual setup step.
Verify DNS is answering:
dig @127.0.0.1 -p 5354 google.com Run it twice: the second request is served from EliHole’s cache and comes back noticeably faster.
The web UI is now on port 4410. Visit http://<host-ip>:4410/setup for first-run setup, then manage the instance at http://<host-ip>:4410/admin.
Get DNS on port 53
Clients expect DNS on port 53, but the EliHole container runs as a non-root user and cannot bind ports below 1024. You have two options.
Option A: run the container as root
Create a docker-compose.override.yml next to the main Compose file:
services:
app:
user: "0:0" Set DNS_PORT=53 in .env, then docker compose up -d. Simple, at the cost of running the process as root.
Option B: keep 5354 and redirect with iptables
Leave the container non-root and translate incoming port 53 traffic to 5354:
sudo iptables -t nat -A PREROUTING -p udp --dport 53 ! -s 172.16.0.0/12 -j REDIRECT --to-port 5354 The ! -s 172.16.0.0/12 exclusion keeps Docker’s own networks from being caught in the redirect. Make the rule survive reboots:
sudo apt install iptables-persistent -y
sudo netfilter-persistent save One common snag: on many distributions, port 53 is already occupied by systemd-resolved’s stub listener. If something else is squatting on the port, see freeing port 53 from systemd-resolved before wiring up either option.
Point your network at EliHole
To block ads on every device at once, set your router’s DHCP DNS server to the EliHole host’s IP. Devices pick up the new resolver on their next DHCP lease, and the whole network routes DNS through EliHole.
To use EliHole as the system resolver on a single Linux machine, add a systemd-resolved drop-in instead:
sudo mkdir -p /etc/systemd/resolved.conf.d
sudo tee /etc/systemd/resolved.conf.d/elihole.conf <<'EOF'
[Resolve]
DNS=<host-ip>
DNSOverTLS=no
EOF
sudo systemctl restart systemd-resolved Replace <host-ip> with the address of the machine running EliHole.
Encrypted DNS: DoH and DoT
DNS-over-HTTPS works out of the box at /dns-query, following RFC 8484 — point any DoH-capable client at https://<your-host>/dns-query.
DNS-over-TLS (RFC 7858) ships disabled. To enable it, set DOT_PORT, DOT_CERT_PATH, and DOT_KEY_PATH in .env with a valid certificate and key, then restart the stack.
Low-RAM hosts
The Elixir release build needs more than 1 GB of RAM at peak, which rules out building directly on small VPSes and single-board computers. Build the image on a stronger machine and ship it over SSH:
docker save eli-hole-app | gzip | ssh HOST 'gunzip | docker load' Then run docker compose up -d on the target host as usual — running EliHole takes far less memory than building it.
Updating
git pull
docker compose up -d --build Compose rebuilds the image and restarts the app; migrations for the new version run automatically on startup.
Next steps
- Coming from Pi-hole? You can migrate your Pi-hole config — blocklists, whitelist, and settings — instead of rebuilding by hand.
- Still deciding? The EliHole vs Pi-hole comparison lays out where the two differ and which fits your setup.
- Lock down the basics: change the default admin credentials, set
PHX_HOSTandFORCE_SSLif the UI is reachable beyond your LAN, and confirm your iptables redirect persists after a reboot.