DockerNetworkingSelf-Hosting

Docker Daemon Proxy Configuration: The Fixes I Actually Use When Pulls, Builds, and Containers Refuse to Reach the Internet

A practical Linux homelab guide to Docker daemon proxy configuration, client proxy settings, NO_PROXY patterns, and build/runtime troubleshooting.

AU

Author

Marcus Chen

FTC disclosure: This article contains affiliate links. If you purchase through these links, we may earn a commission at no additional cost to you.

After running Docker on everything from disposable lab VMs to a box I unfortunately trusted in production, I can tell you this with a straight face: proxy problems are rarely Docker problems. They are scope problems.

Someone sets proxy variables for the daemon but not the client. Or they fix docker pull and then wonder why docker build still fails on apt update. Or they mean reverse proxy and end up editing daemon.json like a person trying to fix a leaking sink with a firmware update.

I have made all of those mistakes. The server was unimpressed.

Key Takeaways

  • docker pull, docker build, and traffic from inside containers do not all use the same proxy configuration path.
  • For a Linux Docker host, the cleanest base setup is usually a daemon-level config plus a systemd drop-in when needed for startup environment clarity.
  • ~/.docker/config.json affects new containers and builds, not the daemon itself.
  • NO_PROXY is where otherwise competent people lose an afternoon. Include localhost, local subnets, internal hostnames, and any private registry endpoints.
  • Docker Desktop behaves differently from Docker Engine on Linux. daemon.json proxy settings that make sense on Linux can be ignored on Desktop.
  • If your actual goal is publishing apps behind Nginx Proxy Manager, Caddy, or Traefik, this is a different problem entirely. Read our Nginx Proxy Manager setup guide after this one.

First, the proxy misunderstanding that causes half the mess

There are two proxy conversations people keep jamming together:

  1. Outbound HTTP/HTTPS proxy - Docker uses this to reach the internet through a filtered network.
  2. Inbound reverse proxy - Nginx Proxy Manager, Caddy, or Traefik routes traffic to your apps.

This article is about the first one.

If you are trying to publish services to your LAN or the internet, read our reverse proxy comparison or the hands-on Nginx Proxy Manager on Docker walkthrough.

If you are trying to make docker pull, docker build, or a containerized app stop timing out behind a restrictive upstream, stay here.

What “Docker proxy configuration” actually means

Docker has multiple layers that can need proxy settings:

Layer What it affects Typical config location
Docker daemon docker pull, registry access, swarm node reachability /etc/docker/daemon.json and sometimes systemd env
Docker client / container defaults env vars injected into new containers and builds ~/.docker/config.json
Build-time access package installs and downloads during docker build build args / client proxy config
Runtime container traffic apps inside containers reaching outside services env vars or Compose environment

That split matters.

If you only configure the daemon, your image pulls may work while your builds still fail.

If you only configure the client, a new container may inherit proxy variables correctly while the host still cannot pull from Docker Hub.

This is why proxy fixes feel random. They are not random. They are just annoyingly specific.

When you actually need this in a homelab

People hear “corporate proxy” and assume it has nothing to do with homelabs. Cute idea.

You still care about this if your Docker host sits behind:

  • a school or office connection with outbound filtering
  • a parent network or shared building network
  • a security appliance doing forced proxying or allowlists
  • a lab environment where outbound traffic is intentionally constrained
  • a caching proxy you use to speed up repeated downloads

I have also seen people use this pattern in segmented labs where a utility VM acts as the egress choke point. It is not common, but it is absolutely a thing.

The setup I actually recommend on Linux

For a normal Linux Docker Engine host, I recommend this order:

  1. set the daemon proxy in /etc/docker/daemon.json
  2. add a systemd drop-in if you need startup-environment clarity or your environment expects it
  3. configure ~/.docker/config.json if builds and new containers also need proxy defaults
  4. verify each layer separately instead of assuming one success means the rest work

That is the least confusing path I have found.

Step 1 - Configure the Docker daemon

If your main pain is docker pull failing, start here.

Create or edit /etc/docker/daemon.json:

{
  "proxies": {
    "http-proxy": "http://proxy.example.com:3128",
    "https-proxy": "http://proxy.example.com:3128",
    "no-proxy": "localhost,127.0.0.1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,registry.lab.example,harbor.lan"
  }
}

A few notes before you paste that everywhere like it is holy text:

  • If your proxy uses authentication, use the real authenticated URL.
  • Keep internal services in no-proxy.
  • If your internal registry is on a private DNS suffix, include it explicitly.
  • If you are on Docker Desktop, read the official warning first - daemon.json proxy settings are ignored there. Use the Desktop settings UI instead.

Official reference: Docker daemon proxy configuration

Restart Docker after the change:

sudo systemctl restart docker

Then verify what the daemon thinks it knows:

docker info | grep -i proxy

If you get nothing useful back, do not assume the file worked. Assume it did not.

Step 2 - Add a systemd drop-in when the host needs it

I prefer daemon.json as the primary config, but in the real world I still end up using a systemd drop-in often.

Why? Because it makes the daemon startup environment explicit, which helps when you are troubleshooting on a box that has been “helpfully” configured by three different past versions of yourself.

Create the directory:

sudo mkdir -p /etc/systemd/system/docker.service.d

Create the drop-in file:

# /etc/systemd/system/docker.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:3128"
Environment="HTTPS_PROXY=http://proxy.example.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,registry.lab.example,harbor.lan"

Reload and restart:

sudo systemctl daemon-reload
sudo systemctl restart docker

Then inspect the live unit environment:

systemctl show --property=Environment docker

This is one of those commands that saves you from inventing folklore. Either the unit has the variables or it does not.

The mistake I made

The first time I fought this on a Ubuntu host, I edited the file correctly and forgot daemon-reload.

Then I spent far too long testing broken pulls and blaming Docker. Docker was innocent for once, which was honestly rude.

Step 3 - Configure the client defaults for builds and new containers

Now we deal with the layer people skip.

Create ~/.docker/config.json for the user running Docker commands:

{
  "proxies": {
    "default": {
      "httpProxy": "http://proxy.example.com:3128",
      "httpsProxy": "http://proxy.example.com:3128",
      "noProxy": "localhost,127.0.0.1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,registry.lab.example,harbor.lan"
    }
  }
}

This does not configure the daemon.

What it does is pass proxy-related environment variables into new containers and builds.

Official reference: Use a proxy server with the Docker CLI

You do not need to restart Docker for this file. But it only applies to new containers and builds.

Test it:

docker run --rm alpine sh -c 'env | grep -i _PROXY'

If you see the variables you expect, good.

If not, stop editing daemon.json. You are in the wrong layer.

Step 4 - Make builds work on purpose

This is where people think Docker is “still broken” after docker pull works.

A simple test Dockerfile:

FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl ca-certificates
RUN curl -I https://example.com

Build it:

docker build -t proxy-test .

If this still fails, check three things:

  1. your build is not inheriting proxy values from ~/.docker/config.json
  2. your NO_PROXY is wrong for internal endpoints
  3. the proxy allows the outbound destination used during the build

If you need to pass build args explicitly, do it like this:

docker build \
  --build-arg HTTP_PROXY=http://proxy.example.com:3128 \
  --build-arg HTTPS_PROXY=http://proxy.example.com:3128 \
  --build-arg NO_PROXY=localhost,127.0.0.1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 \
  -t proxy-test .

It is not glamorous. It is effective.

Step 5 - Handle Docker Compose without turning the file into soup

If you have services that genuinely need outbound access through the proxy, define it clearly in Compose:

services:
  app:
    image: ghcr.io/example/app:latest
    environment:
      HTTP_PROXY: http://proxy.example.com:3128
      HTTPS_PROXY: http://proxy.example.com:3128
      NO_PROXY: localhost,127.0.0.1,.local,db,redis,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16

Notice the service names in NO_PROXY.

That matters more than people expect. If app talks to db or redis on the same Docker network, you do not want that traffic trying to bounce through an outbound proxy like it just discovered performance art.

If you need a refresher on how container-to-container traffic works, read our Docker networking deep dive.

The NO_PROXY pattern I trust most in homelabs

This is the part competitors usually wave at and then flee from.

A decent starting point looks like this:

localhost,127.0.0.1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,db,redis,minio,registry.lab.example,harbor.lan

What I usually include:

  • localhost
  • 127.0.0.1
  • .local if I am using local mDNS names
  • RFC1918 ranges for internal subnets
  • container service names that should stay local
  • internal registry names
  • NAS, artifact mirror, or package cache hostnames

Why this matters

A bad NO_PROXY list creates the stupidest failures.

Your container can reach the internet fine, but suddenly fails to talk to minio, your internal registry, or a package mirror on the LAN because it tries to send local traffic through the upstream proxy. That is how you end up muttering at the screen while a container claims your NAS is on the internet.

A full verification sequence I actually use

After configuration, I test in this order:

1. Confirm the unit environment

systemctl show --property=Environment docker

2. Confirm what Docker reports

docker info | grep -i proxy

3. Test a simple pull

docker pull alpine:latest

4. Test a runtime container

docker run --rm alpine sh -c 'apk add --no-cache curl >/dev/null && curl -I https://example.com'

5. Test a build

docker build -t proxy-build-test .

If one of those fails, you now know which layer failed.

That narrows the problem from “Docker proxy configuration is broken” to something fixable in one pass.

Security notes people skip

Proxy settings often contain credentials.

That means:

  • do not commit authenticated proxy URLs into Git
  • do not paste them into screenshots you plan to share
  • remember container environment variables are not secret storage
  • avoid hard-coding sensitive values directly into Dockerfiles

This is also a good time to revisit our Docker Compose best practices guide and Docker security hardening guide because proxy config is one of those tiny operational details that becomes a larger security mess if handled lazily.

Troubleshooting by symptom

When a proxy setup fails, the error usually tells you which layer is wrong if you stop reading it like a crime scene and start reading it like a scope problem.

Symptom 1 - docker pull times out or cannot resolve the registry

If docker pull alpine fails before a container ever starts, focus on the daemon first.

Checks I run:

systemctl show --property=Environment docker
docker info | grep -i proxy
sudo journalctl -u docker -n 100 --no-pager

What I am looking for:

  • missing or malformed environment variables in the unit
  • a typo in daemon.json
  • a proxy URL with special characters that were not escaped correctly
  • a private registry hostname that should have been excluded via NO_PROXY

On Linux hosts using systemd, special characters in authenticated proxy URLs can be annoying. Docker's docs note that some values need escaping in systemd unit files. If you skipped that detail, the value may look fine in the file and still be wrong at runtime.

Symptom 2 - docker pull works, but docker build fails during package installs

This one is common.

The daemon can reach Docker Hub, but build steps such as apt-get update, apk add, pip install, or curl inside the build context still need usable proxy values.

My quick test is a tiny Dockerfile that does nothing interesting except try to leave the network:

FROM alpine:3.22
RUN apk add --no-cache curl
RUN curl -I https://example.com

If that build fails, I check whether:

  • ~/.docker/config.json exists for the user running the build
  • proxy variables are reaching BuildKit or classic builds as expected
  • I need explicit --build-arg flags for the toolchain in question
  • NO_PROXY excludes internal mirrors if I am pulling from local package caches

This is one reason I tell people to keep a tiny “network sanity” Dockerfile around. It is faster than pretending your application image is the right debugging surface.

Symptom 3 - build works, but the running container cannot reach the internet

That pushes me toward runtime environment handling.

Test the container directly:

docker run --rm alpine sh -c 'env | grep -i _PROXY'
docker run --rm alpine sh -c 'apk add --no-cache curl >/dev/null && curl -I https://example.com'

If the proxy variables are missing, the issue is usually one of these:

  • no client proxy config in ~/.docker/config.json
  • Compose file omitted the required environment values
  • container was created before the proxy config changed

Remember that old containers do not magically inherit new settings. Containers are many things. Telepathic is not one of them.

Symptom 4 - the container can reach the internet, but fails against local services

This is almost always a NO_PROXY problem.

If your app cannot reach db, redis, minio, a NAS hostname, or an internal registry, expand your exclusions and test again. I often add local service names one by one while validating with curl, wget, or the app's own health check.

A practical example:

NO_PROXY=localhost,127.0.0.1,.local,db,redis,minio,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,registry.lab.example

The reason I keep repeating this is simple: half of proxy troubleshooting is not “how do I send traffic through the proxy?” It is “how do I stop obviously local traffic from doing something absurd?”

Rootless Docker and Docker Desktop caveats

These are the two paths that break copy-paste guides fastest.

Rootless Docker

If you are using rootless Docker, the systemd paths are different because Docker runs as a user service instead of a system service.

That means the drop-in path changes to something like:

~/.config/systemd/user/docker.service.d/

And the commands change too:

systemctl --user daemon-reload
systemctl --user restart docker
systemctl --user show --property=Environment docker

If you use rootless mode and follow a system-wide /etc/systemd/system/docker.service.d/ guide, it will not work. The file can be perfectly written and still be attached to the wrong service. That is one of my least favorite Linux genres because it looks correct until you notice nothing changed.

Docker Desktop

Docker Desktop deserves its own paragraph because it quietly ignores Linux assumptions.

If you are on Docker Desktop, the official guidance is to configure proxies in the Desktop settings UI, not by assuming Linux Engine behavior maps over cleanly. That matters for people who bounce between a Windows laptop and a Linux homelab box and expect identical rules.

Official reference: Docker Desktop proxy settings

The configuration bundle I keep in my notes

When I know a host will live behind a proxy for a while, I keep a tiny, boring checklist in my lab notes:

/etc/docker/daemon.json

{
  "proxies": {
    "http-proxy": "http://proxy.example.com:3128",
    "https-proxy": "http://proxy.example.com:3128",
    "no-proxy": "localhost,127.0.0.1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,registry.lab.example"
  }
}

/etc/systemd/system/docker.service.d/http-proxy.conf

[Service]
Environment="HTTP_PROXY=http://proxy.example.com:3128"
Environment="HTTPS_PROXY=http://proxy.example.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,registry.lab.example"

~/.docker/config.json

{
  "proxies": {
    "default": {
      "httpProxy": "http://proxy.example.com:3128",
      "httpsProxy": "http://proxy.example.com:3128",
      "noProxy": "localhost,127.0.0.1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,registry.lab.example"
    }
  }
}

And then I keep the four verification commands right below it:

systemctl show --property=Environment docker
docker info | grep -i proxy
docker pull alpine:latest
docker run --rm alpine sh -c 'env | grep -i _PROXY'

It is not elegant. It is repeatable. I have learned to value repeatable more than elegant.

Common mistakes

1. Fixing the daemon and assuming builds will work

They might. They also might not.

Build-time behavior often depends on client-side proxy config or explicit build args.

2. Forgetting daemon-reload

Systemd is not psychic.

If you changed a drop-in file, reload systemd before restarting Docker.

3. Using the same advice on Docker Desktop and Linux Engine

Desktop has its own behavior. Read the official Docker Desktop proxy docs before copying Linux-only patterns over.

4. Writing a weak NO_PROXY list

This is the main source of “almost working” setups.

If internal traffic is still trying to leave through the proxy, your exclusions are incomplete.

5. Confusing outbound proxy config with reverse proxy publishing

If your actual issue is HTTPS for a self-hosted app, you probably want Nginx Proxy Manager or a reverse proxy decision guide instead.

Recommended gear for a reliable Docker host

You do not need to buy hardware to fix proxy settings. I am obligated to say that because it is true.

You may, however, want better hardware if your Docker host currently lives on a mystery box with one flaky NIC and a power brick that feels warm enough to cook opinions.

Those are not proxy fixes. They are quality-of-life improvements for the kind of person who will absolutely be debugging this at an inconvenient time.

My opinionated recommendation

If you are running Docker Engine on Linux, do this:

  • put the daemon proxy in daemon.json
  • use a systemd drop-in when you need startup-environment clarity
  • use ~/.docker/config.json for new containers and build defaults
  • be aggressive about NO_PROXY
  • verify every layer independently

That is the setup I actually trust.

The minimalist “I exported HTTP_PROXY once in my shell and now I hope for the best” approach does work sometimes. So does driving with the check engine light on. Neither is a strategy.

FAQ

Do I need both daemon.json and a systemd drop-in?

Not always.

If daemon.json alone handles your daemon traffic cleanly, you may be fine. I still like the drop-in on Linux hosts where I want startup behavior to be explicit and easy to inspect.

Why does docker pull work but docker build still fail?

Because the daemon and build/runtime layers are not identical.

The daemon can reach the registry while the build process still lacks the right proxy values or exclusions.

Does ~/.docker/config.json affect existing containers?

No.

It applies to new containers and builds. Existing containers will not magically pick up the new settings.

What should go in NO_PROXY for a homelab?

At minimum: localhost, loopback, your private subnets, local DNS suffixes, and any internal services or registries containers should reach directly.

If local traffic still tries to leave through the proxy, keep expanding the list until it stops being ridiculous.

What if I am using Docker Desktop?

Read Docker's Desktop proxy documentation and use the Desktop settings UI.

Do not assume a Linux Engine guide maps cleanly onto Desktop behavior.

What to learn next

If this host is part of a bigger lab, the next skills that usually matter are:

If nothing else, remember this: when Docker proxy config breaks, stop asking “is Docker broken?” and start asking “which layer am I actually configuring?”

That question fixes more labs than another hour of random edits ever will.