Skip to content
+1 (813) 212-3723 support@layeronecloud.com
Self-hosting VPS guides

How to run Pi-hole on a VPS without creating an open DNS resolver

Deploy Pi-hole v6 with Docker on a small VPS, serve DNS only over a private LayerOne network or VPN, and keep the admin interface off the public internet.

6 min read

Pi-hole blocks selected DNS names before clients connect to advertising, tracking, and malware domains. A VPS is useful when several LayerOne servers or VPN-connected devices need the same resolver. It is not safe to publish DNS port 53 to the entire internet: an unrestricted recursive resolver will be abused for reflection attacks and can expose query history.

This guide gives Pi-hole a LayerOne private-network address, publishes DNS only on that address, and reaches the web interface through an SSH tunnel. Home and mobile devices need a VPN route to the private address; use the WireGuard VPS guide rather than opening DNS globally.

VPS size, CPU, storage, and price

Pi-hole's official prerequisites list 512 MB RAM, at least 2 GB of free disk, and 4 GB of free disk as the recommendation.

Item Planning value
Minimum RAM 512 MB upstream minimum
Recommended RAM 1 GB for Ubuntu, Docker, Pi-hole, query history, and update headroom
Expected CPU usage About 0.01–0.05 vCPU idle for a small client set; 0.1–0.5 vCPU during gravity-list rebuilds or DNS bursts
Storage requirement 2 GB free minimum, 4 GB free recommended by Pi-hole; retain more for the OS, Docker images, query database, and logs
Exact LayerOne SKU gc.nano — slug layerone-nano, 1 vCPU, 1 GB RAM, 20 GB disk
Expected monthly cost $0.0041 per active hour; about $2.99 per 730 hours against the advertised $3.00 730-hour equivalent

A 744-hour month is about $0.0041 × 744 = $3.05. DNS volume, list size, logging, storage growth, transfer, and optional services vary. LayerOne meters the allocated VPS hourly, including while stopped; read How hourly billing works.

The CPU ranges are operational estimates, not limits or guarantees. A public resolver under abuse has no meaningful “expected” load, which is another reason never to expose it.

Assumptions and network plan

  • Ubuntu 22.04 or 24.04 with Docker Engine and Compose; complete Running Docker first
  • A LayerOne private network such as 10.10.0.0/24
  • This VPS attached at 10.10.0.10; replace that documentation address with the private address you actually assign
  • Other LayerOne VPSs on that VNet, or VPN clients with a route to it
  • A sudo user and SSH key; replace ADMIN_IP with your fixed administration source address

Pi-hole needs a stable address. Do not use its DHCP server for remote clients: DHCP is broadcast-based and does not cross an ordinary routed VPS/VPN boundary. Do not publish UDP 67 or TCP/UDP 123 unless you have designed those optional services explicitly.

1. Firewall DNS to the private network

In the LayerOne cloud firewall, allow TCP 22 from ADMIN_IP/32. Do not add any public rule for TCP or UDP 53. If you filter the private interface, allow TCP and UDP 53 only from the actual VNet or VPN client prefix.

Apply the matching host rules:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from ADMIN_IP to any port 22 proto tcp
sudo ufw allow from 10.10.0.0/24 to 10.10.0.10 port 53 proto tcp
sudo ufw allow from 10.10.0.0/24 to 10.10.0.10 port 53 proto udp
sudo ufw enable
sudo ufw status verbose

Docker can insert rules ahead of UFW. The decisive safeguards below are that Docker binds DNS to 10.10.0.10, not 0.0.0.0, and that the cloud firewall has no public port-53 rule. See The Docker firewall trap.

2. Create a protected Compose project

sudo install -d -m 0750 /opt/pihole/etc-pihole
sudo chown -R "$USER":"$USER" /opt/pihole
cd /opt/pihole
umask 077
printf 'PIHOLE_PASSWORD=%s
' "$(openssl rand -hex 24)" > .env
chmod 600 .env

Save the generated password in a password manager; root-readable environment files are configuration, not a backup. Create /opt/pihole/compose.yaml:

services:
  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    restart: unless-stopped
    ports:
      - "10.10.0.10:53:53/tcp"
      - "10.10.0.10:53:53/udp"
      - "127.0.0.1:8080:80/tcp"
    environment:
      TZ: "America/New_York"
      FTLCONF_webserver_api_password: "${PIHOLE_PASSWORD}"
      FTLCONF_dns_listeningMode: "ALL"
    volumes:
      - ./etc-pihole:/etc/pihole

Change TZ to your IANA time zone. FTLCONF_dns_listeningMode=ALL is needed for Docker's bridge networking; the host binding and firewall still decide who can reach the service. Pi-hole's current Docker documentation uses latest as the latest release channel. For tightly controlled change windows, replace it with a date-based tag from an upstream release after testing.

No NET_ADMIN capability is needed because this design does not run Pi-hole as a DHCP server.

3. Start and test Pi-hole

cd /opt/pihole
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 pihole
docker exec pihole pihole status

From another VPS on 10.10.0.0/24, install dnsutils if necessary and query both UDP and TCP:

dig @10.10.0.10 example.com A
dig +tcp @10.10.0.10 example.com A

Both should return an answer. From a machine without the VNet or VPN route, queries to the VPS's public address on port 53 must time out.

4. Reach the web UI without publishing it

On your local workstation, open an SSH tunnel:

ssh -L 8080:127.0.0.1:8080 admin@SERVER_IP

Keep that session open and browse to http://127.0.0.1:8080/admin/. Read the password locally from the protected file if needed:

sudo cat /opt/pihole/.env

Choose upstream resolvers, review the default lists, and set an appropriate query-privacy level. Do not send the password or raw query log to a public support channel.

5. Point clients at Pi-hole

For other LayerOne VPSs on the VNet, configure 10.10.0.10 as their resolver. Keep a documented fallback procedure: if this single Pi-hole is down, clients that use only it cannot resolve names. For home and mobile devices, route 10.10.0.10/32 through WireGuard or another private VPN and assign the Pi-hole address as VPN DNS.

Avoid silently listing a public fallback resolver second on clients. Many operating systems query both resolvers rather than using the second only during failure, which bypasses filtering unpredictably.

Upgrades, backup, and restore

Read the Pi-hole Docker and component release notes first. A container upgrade replaces the container rather than running pihole -up inside it:

cd /opt/pihole
docker compose pull
docker compose down
docker compose up -d
docker compose logs --tail=100 pihole
docker exec pihole pihole status

Back up /opt/pihole/etc-pihole, compose.yaml, and the protected environment file to encrypted off-server storage. For a consistent filesystem copy, stop the container briefly:

cd /opt/pihole
docker compose stop
PIHOLE_BACKUP="/var/backups/pihole/pihole-$(date +%F).tar.gz"
sudo install -d -m 0700 -o "$USER" -g "$USER" /var/backups/pihole
sudo tar -C /opt -czf "$PIHOLE_BACKUP" pihole
sudo chown "$USER":"$USER" "$PIHOLE_BACKUP"
chmod 600 "$PIHOLE_BACKUP"
docker compose start
unset PIHOLE_BACKUP

Transfer the archive off the VPS immediately and test a restore on an isolated host. LayerOne does not back up the server; see Backups and snapshots.

Official and LayerOne references

Still stuck

Chat with us from the portal.

Ask the assistant from the Chat bar. During business hours you can ask for a person and a human joins live.