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_IPwith 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.