Install EliHole with Docker

Install EliHole, a self-hosted DNS sinkhole, with Docker Compose: env setup, port 53 options, network-wide ad blocking, DoH, and updates — in minutes.

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 legacy docker-compose v1 binary fails on the ${VAR:?} interpolation syntax used in the Compose file — if docker compose version prints 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:

VariableDefaultPurpose
DNS_PORT5354UDP port the DNS server listens on
DNS_UPSTREAMS8.8.8.8:53,8.8.4.4:53Upstream resolvers, comma-separated
PHX_PORT4410Web UI port
PHX_HOSTPublic hostname for the web UI
FORCE_SSLRedirect web traffic to HTTPS
DOT_PORT / DOT_CERT_PATH / DOT_KEY_PATHDNS-over-TLS; disabled until a cert is configured
INSTANCE_ROLEstandalonestandalone, 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_HOST and FORCE_SSL if the UI is reachable beyond your LAN, and confirm your iptables redirect persists after a reboot.

Frequently asked questions

Can EliHole listen on port 53 directly?
Not by default. The container runs as a non-root user, which cannot bind ports below 1024. Either add a docker-compose.override.yml that sets user: "0:0" and DNS_PORT=53, or keep the default port 5354 and redirect port 53 to it with an iptables PREROUTING rule.
Does EliHole need a separate database?
No. The Docker Compose file includes a Postgres container, and EliHole runs its database migrations automatically on startup. You don't need to install or configure anything database-related yourself.
How much RAM does EliHole need?
Running EliHole is light, but building the Elixir release image needs more than 1 GB of RAM at peak. On low-RAM hosts, build the image on a stronger machine and ship it over SSH with docker save and docker load.
How do I update EliHole?
Run git pull followed by docker compose up -d --build in the repository directory. Compose rebuilds the image, restarts the app, and migrations run automatically on startup.