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.
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.jsonaffects new containers and builds, not the daemon itself.NO_PROXYis 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.jsonproxy 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:
- Outbound HTTP/HTTPS proxy - Docker uses this to reach the internet through a filtered network.
- 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:
- set the daemon proxy in
/etc/docker/daemon.json - add a systemd drop-in if you need startup-environment clarity or your environment expects it
- configure
~/.docker/config.jsonif builds and new containers also need proxy defaults - 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.jsonproxy 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:
- your build is not inheriting proxy values from
~/.docker/config.json - your
NO_PROXYis wrong for internal endpoints - 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:
localhost127.0.0.1.localif 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.jsonexists for the user running the build- proxy variables are reaching BuildKit or classic builds as expected
- I need explicit
--build-argflags for the toolchain in question NO_PROXYexcludes 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.
- Fanless N100 mini PC for a dedicated Docker host
- Managed 2.5GbE switch for VLANs and cleaner lab segmentation
- APC Back-UPS 1500VA to stop tiny outages from becoming filesystem adventures
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.jsonfor 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:
- Docker networking deep dive - so service-to-service traffic stays predictable
- Docker Compose best practices - so environment handling does not turn into YAML archaeology
- Docker security hardening - because restricted egress does not automatically mean secure containers
- Homelab network segmentation guide - because a clean network design prevents a surprising amount of proxy and routing weirdness
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.
