JDM
← All posts
4 min read

AdGuard-Only Docker DNS: A Footgun for Self-Hosted Builds

When your Coolify host points Docker containers at a single AdGuard DNS server, any hiccup breaks builds and runtime fetches. Here is the diagnosis and the tradeoffs.

dockerdnshomelabcoolifygotcha

I spent three hours last month debugging why a Next.js build kept failing with ENETUNREACH during npm install. The host had internet. Curl worked from SSH. The container just would not resolve registry.npmjs.org.

The culprit was my own /etc/docker/daemon.json. I had set dns: ['192.168.0.88'] -- my NAS running AdGuard Home. No fallback. When AdGuard restarted for a blocklist update, every container lost DNS. Builds failed. Runtime fetches to Resend and OpenAI timed out.

This is a footgun. Here is how it works, how to spot it in 30 seconds, and what you can do about it.

The Setup That Breaks

My Coolify host has two DNS stacks. The host itself uses /etc/resolv.conf with Cloudflare and Google (1.1.1.1, 8.8.8.8). That works fine. But Docker containers do not inherit the host's resolvers. They use whatever you put in /etc/docker/daemon.json.

The dns key there is an explicit list. If you write ['192.168.0.88'], that is the only resolver any container will ever see. No fallback. No systemd-resolved. Just that single IP.

When AdGuard hiccups -- container restart, NAS hang, DNS rewrite update -- every Docker DNS lookup fails. The host keeps working. You SSH in and everything looks fine. But your containers are blind.

The 30-Second Diagnosis

Next time you see a build failure that looks like a network issue, check DNS first.

  1. SSH into the host and run cat /etc/docker/daemon.json. Look for the dns array. If it has one IP and that IP is your AdGuard server, you found the problem.

  2. Pull the deploy logs and grep for enetunreach, eai_again, getaddrinfo, or no such host. These are DNS failure signatures.

  3. If the container is still running, test from inside: docker exec <container> getent hosts api.openai.com. If it hangs or returns nothing, your container cannot resolve DNS.

I keep a one-liner in my shell history: ssh coolify 'cat /etc/docker/daemon.json && docker ps -q | head -1 | xargs docker exec getent hosts google.com'. Runs in under a second.

Build-Time vs Runtime Failures

Not all DNS failures are the same. The fix depends on when they happen.

Build-time failures are transient. AdGuard hiccups during a docker build. npm install hits ENETUNREACH. A Next.js build tries to fetch a Google Font and gets getaddrinfo EAI_AGAIN. These usually resolve on retry because AdGuard comes back up within seconds.

Runtime failures are trickier. Your production container occasionally fails to call api.resend.com or api.openai.com. One request in a hundred times out. The container is running fine otherwise. This is a code-level problem, not an infra problem, because you cannot control when AdGuard glitches.

Three Common Classes and Their Fixes

Class A: Next.js next/font/google Build-Time Fetch

This is the most common one I see. Next.js fetches Google Fonts during build. If DNS fails, the build fails. The symptom is a build log that says Failed to fetch X from Google Fonts and then exits.

The fix: switch to @fontsource-variable/<family> npm packages and import them via CSS @import. This removes the build-time DNS dependency entirely. The font is bundled as an npm dependency, not fetched at build time. I did this for all six of my sites. It took about 20 minutes per site and eliminated a whole class of build failures.

Class B: npm install Transient ENETUNREACH

This happens when AdGuard restarts during a docker build that needs to pull npm packages. The build fails halfway through.

The fix: pre-pull the base image with docker pull node:24-slim before building. This does not fix the DNS issue, but it reduces the window of vulnerability. Then retry the deploy. Usually succeeds in one or two attempts. I have a script that retries the Coolify deploy up to three times with a 10-second delay between attempts.

Class C: Runtime Container Fetches

Your running app calls an external API. One call in a hundred fails with EAI_AGAIN, ENOTFOUND, or ECONNRESET. This is the hardest to debug because it is intermittent.

The fix: add retry logic to your fetch helper. I use a simple backoff: 200ms on first failure, 600ms on second, then give up. Catch the specific DNS error codes and retry. For fire-and-forget secondary calls (analytics, logging), use Next.js after() so they do not stall the response. This pattern covers the 99% case without adding complexity.

The Permanent Fix (Heavy)

Edit /etc/docker/daemon.json to add fallback DNS servers:

{
  "dns": ["192.168.0.88", "1.1.1.1", "8.8.8.8"]
}

AdGuard stays primary, so your blocklists still apply. But if AdGuard hiccups, containers fall back to Cloudflare or Google. This fixes both build-time and runtime DNS failures.

The cost: you must restart the Docker daemon, which restarts every container on the host. That is a maintenance window. I do it during low-traffic hours and warn users with a maintenance page.

The Honest Tradeoff

I run no-fallback on my production host. I know the failure mode. I accept it because I would rather catch every AdGuard hiccup than let a single leaked tracker through. My blocklists are strict. When AdGuard restarts, I see the build failure, I retry, and it works. The runtime retry code handles the sporadic failures.

But this is a config choice with a known cost. If you value build resilience over strict ad-blocking, add the fallbacks. If you value strict filtering and can tolerate occasional retries, keep the single DNS entry.

Neither answer is wrong. The footgun is not knowing you made the choice at all.

For more on how I migrated from Vercel to this Coolify setup, see the Vercel-to-Coolify migration post. The DNS piece was one of the last gremlins I fixed.