Isolating n8n from other services using Docker networks comes down to one rule: put every container on a user-defined bridge network, never the default one, and give n8n its own private network for its database plus a second shared network only for the reverse proxy. Containers sitting on separate user-defined bridges cannot reach each other at all, not by IP and not by name, so your Postgres instance stops being visible to every other stack on the box. On a single VPS running n8n alongside other apps, that usually means two networks (n8n_internal marked internal: true, and a shared edge network for Caddy, Traefik or nginx), zero published ports on the database, and no 5678 port mapping exposed to the public interface.
The whole change is about fifteen lines of Docker Compose, and it holds up as your stack grows.
Why Docker networks isolate containers in the first place
Docker’s networking model decides who can talk to whom at the bridge level, before any application-layer auth gets involved. Every user-defined bridge network you create gets its own subnet, its own gateway, and its own embedded DNS entry list. A container attached to net-a resolves and reaches other containers on net-a.
It has no route to a container that only lives on net-b. There’s no firewall rule to write and nothing to maintain: the isolation is a property of the network itself.
That’s the behaviour you’re building on. Two things break it, and both are common.
The default bridge network is the one that catches people out
If you run docker run without specifying a network, the container lands on the pre-built network called bridge. Everything else that landed there the same way shares that subnet. Those containers can reach each other by IP address on any port the process is listening on, whether or not you used EXPOSE, whether or not you published a port.
So your n8n container and some random test container from three months ago sit on the same flat segment.
The pre-built bridge also doesn’t give you automatic name resolution between containers, which is why people fall back to hardcoded IPs or the old --link flag. User-defined bridges give you DNS by container name and service name through Docker’s internal resolver, per Docker’s own networking documentation. That’s both a nicer setup and a safer one.
Docker Compose quietly creates a network for you
Run docker compose up in a folder and Compose creates a network named after the project directory, something like n8n_default. Every service in that file joins it. That’s decent isolation from other Compose projects, and it’s the reason a lot of people never think about networks at all.
The problem shows up the moment you want a shared reverse proxy, or you run several stacks and start attaching things to each other’s networks for convenience. One networks: block copied from a blog post and suddenly your Nextcloud container can open a socket to your n8n Postgres.
Note: Compose’s default network is not internal. Containers on it have full outbound internet access and can reach the host’s published ports through the gateway address. Isolation from other stacks is not the same thing as isolation from the outside world.
Can containers on two different Docker networks communicate?
No, not directly. There’s no route between two user-defined bridges, and Docker’s DNS won’t resolve a name across them. The only ways across are attaching a container to both networks, going out through the host (published ports plus the gateway IP), or something like host.docker.internal.
That’s exactly what makes the two-network pattern below work.
What you need before you start
Difficulty: moderate. If you’ve written a Compose file before, this is straightforward. Time: about 30 to 45 minutes for a fresh setup, or 15 minutes to retrofit an existing n8n container, plus a few minutes of image pulling on the first run.
You’ll want a Linux VPS with Docker Engine and the Compose plugin installed, root or sudo access, and a domain or subdomain pointed at the server if you plan to use webhooks. n8n’s webhook nodes need a public HTTPS URL to receive calls from outside, which is why the reverse proxy is part of this rather than an afterthought. If you’re weighing whether a single small box can carry this, our notes on outgrowing a shared plan cover the resource signals worth watching.
Back up first. If you already have n8n running with SQLite or Postgres, copy the volume before you touch the network config. Snapshot policies vary by host, so it’s worth knowing how long daily snapshots stick around on your plan before you rely on them as your only safety net.
How to isolate n8n with Docker networks, step by step
-
Create the shared proxy network by hand, outside any Compose file. Run
docker network create edge. This one network is the only place where your reverse proxy and your app containers meet. Creating it manually means it survivesdocker compose downon any individual stack and can be referenced as external by all of them. -
Write the internal network into your Compose file. Under the top-level
networks:key, definen8n_internalwithinternal: true, and declareedgewithexternal: trueso Compose uses the network you made in step 1 instead of creating a new one. Theinternal: trueflag removes the default route from that network, so containers attached only to it get no outbound access at all. -
Attach Postgres to
n8n_internalonly, and give it noports:mapping. The database needs to be reachable by n8n and by nothing else. Leaving outports:means port 5432 is never bound on the host, so it’s unreachable from the internet even if someone gets the password.
⚠️ Warning: do not add ports:, "5432:5432" “temporarily” to check something with a GUI client. On most default Docker installs that binds to 0.0.0.0 and punches straight through a ufw deny rule. Use docker compose exec n8n-db psql -U n8n instead.
-
Attach the n8n container to both networks. n8n needs
n8n_internalto reach Postgres andedgeso the reverse proxy can reach it and so its own HTTP Request nodes can get out to the internet. Order matters slightly for the default gateway, so keep the internal network listed first and let egress ride onedge. -
Point n8n at the database by service name, not by IP. Set
DB_TYPE=postgresdbandDB_POSTGRESDB_HOST=n8n-db, matching the service name exactly. Docker’s embedded DNS resolves it inside the network, and the address stays correct after every restart, which hardcoded container IPs will not. -
Drop the published
5678port entirely. Once the proxy is on theedgenetwork it reacheshttp://n8n:5678directly, so nothing needs to be bound on the host. If you genuinely need local access for testing, bind it to loopback with127.0.0.1:5678:5678rather than the bare5678:5678. -
Set the public URL variables so webhooks resolve correctly. Add
N8N_HOST,N8N_PROTOCOL=httpsandWEBHOOK_URL=https://your.domain/to the environment block. Without these, n8n hands out webhook URLs containing the container’s internal hostname, and every external service that tries to call back will fail. -
Bring the stack up and watch the first boot. Run
docker compose up -d && docker compose logs -f n8n. Postgres initialises its data directory on first start, so n8n may log a couple of connection retries for a few seconds before it settles. That’s normal; a retry loop that never ends is not. -
Prove the isolation actually holds. From a container on a different network, try to reach the database:
docker run --rm --network some-other-net busybox ping -c1 n8n-dbshould fail to resolve the name. Then confirm the intended path works:docker compose exec n8n wget -qO- http://n8n-db:5432will error on protocol but will connect, which tells you DNS and routing are fine. -
Attach your reverse proxy to
edgeand nothing else. Whichever proxy you use, its Compose file should reference the same externaledgenetwork and publish only 80 and 443. That gives you one entry point into the whole box. -
Write down what lives where. A three-line note listing each network, which containers join it, and why saves you an hour the next time you add a service. If you keep a running runbook for the server, our rundown of readable setup notes is a decent starting shape.
The Compose file this produces
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=n8n-db
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.example.com/
- GENERIC_TIMEZONE=America/New_York
volumes:
- n8n_data:/home/node/.n8n
networks:
- n8n_internal
- edge
depends_on:
- n8n-db
n8n-db:
image: postgres:16
restart: unless-stopped
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- n8n_db_data:/var/lib/postgresql/data
networks:
- n8n_internal
networks:
n8n_internal:
internal: true
edge:
external: true
volumes:
n8n_data:
n8n_db_data:
Keep POSTGRES_PASSWORD and N8N_ENCRYPTION_KEY in a .env file next to the Compose file, and keep that file out of git. Losing the encryption key means every stored credential in n8n becomes unreadable, and there’s no recovery for that.
Variations worth knowing
Several stacks sharing one proxy
This is where the pattern earns its keep. Say you’re running n8n, a WordPress site and a Baserow instance on one VPS. Each stack gets its own private network for its own database, and all three app containers join edge.
The proxy routes by hostname. Your WordPress container can talk to n8n’s HTTP API through edge if you want it to, but it has no path whatsoever to n8n’s Postgres.
If one of those stacks is a WordPress install that keeps stalling, resource contention is usually the culprit rather than networking. The causes behind sluggish WordPress apply pretty much identically on a shared Docker host.
Queue mode with Redis and workers
Once you move n8n into queue mode, you add Redis plus one or more worker containers. Redis belongs on n8n_internal with no published port, exactly like Postgres. Workers join n8n_internal for Redis and Postgres access, and they only need edge if their workflows make outbound HTTP calls, which in practice they usually do.
The main n8n container is the only one that needs to be reachable by the proxy.
Blocking egress on purpose
You can go further and put n8n itself on an internal-only network, then route its outbound traffic through an explicit proxy container. That gives you an allowlist of external hosts your workflows can reach. It’s real work to maintain and it breaks community node installs, so I’d only do it if you have a compliance reason.
For most self-hosted setups, private database network plus no exposed ports covers the realistic risk.
One subdomain per service
Hostname-based routing on the proxy means each container gets a clean address without any port numbers in the URL. Setting that up is DNS work rather than Docker work, and the process for running things on a subdomain is the same whether the target is n8n or anything else. You’ll also want TLS on each hostname, so check what’s included on your plan before buying anything; the details on getting a certificate sorted cover the free versus paid split.
Where Docker network isolation stops helping
Network segmentation solves a specific problem. It does not make n8n secure on its own, and pretending otherwise leads to bad decisions.
n8n runs arbitrary code by design. The Code node executes JavaScript, and the HTTP Request node will call any URL you give it. Anyone who can log into your n8n UI can make requests from inside your Docker network, including to other containers on edge and to the cloud provider metadata endpoint at 169.254.169.254. Treat the n8n login as a high-value credential, turn on two-factor authentication in the owner settings, and put the whole thing behind a proxy that terminates TLS. n8n’s guidance on hardening a self-hosted instance is worth reading in full on the official n8n docs.
Never mount the Docker socket into n8n. You’ll find tutorials that bind-mount /var/run/docker.sock so a workflow can control containers. That hands root on the host to anything running inside n8n, and no network layout undoes it.
Published ports bypass ufw on most Linux setups. Docker writes its own iptables rules, and they’re evaluated before the chains ufw manages. A ufw deny 5678 rule with a 5678:5678 port mapping in place gives you a false sense of a closed port. Bind to 127.0.0.1 or add rules to the DOCKER-USER chain, which is the hook Docker leaves for exactly this.
network_mode: host throws all of it away. A container in host network mode shares the host’s stack, so it reaches every listening service including databases you thought were private. If you see it in an n8n tutorial, skip that tutorial.
internal: true blocks outbound traffic completely. That’s the point, and it’s also the thing people trip over. A container on an internal-only network cannot pull from npm, cannot reach an external API, and cannot resolve public DNS. Keep it for the database and Redis, not for n8n itself.
Note: none of this protects the data at rest. If someone gets shell on the host, they can read the Postgres volume directly. Network isolation limits lateral movement between containers, which is a genuinely useful thing, and that’s the extent of it.
Common mix-ups
Mix-up: Docker containers are isolated from each other by default.
Reality: Containers on the default bridge network can reach every port on every other container attached to it. Isolation only happens when you create separate user-defined networks and attach deliberately.
Mix-up: Leaving EXPOSE/expose: out of the config keeps a container port private from other containers.
Reality: expose: is documentation only. Any container on the same network can connect to any port your process is listening on. Network membership is the boundary, not the port declaration.
Mix-up: depends_on guarantees Postgres is ready before n8n connects.
Reality: It controls start order, not readiness. Postgres accepts connections a few seconds after the container starts, so n8n will log connection errors first. Add a healthcheck with condition: service_healthy if the retries bother you.
Fixing the four things that usually break
Problem: n8n logs getaddrinfo ENOTFOUND n8n-db or ECONNREFUSED 127.0.0.1:5432.
Fix: The database host is wrong or the containers aren’t sharing a network. 127.0.0.1 inside the n8n container means the n8n container itself, never the host or another service. Set DB_POSTGRESDB_HOST to the Postgres service name and confirm both services list n8n_internal.
Problem: docker compose up fails with network edge declared as external, but could not be found.
Fix: Run docker network create edge first. External networks are never created by Compose, which is deliberate: it stops one stack from owning a network that several stacks depend on.
Problem: The reverse proxy returns 502 Bad Gateway.
Fix: Nine times out of ten the proxy isn’t on edge, or it’s configured to forward to localhost:5678 instead of n8n:5678. Run docker network inspect edge and check both containers appear in the output.
Problem: Logging in over http://server-ip:5678 fails with a secure cookie error.
Fix: n8n sets a secure cookie by default and refuses non-HTTPS access from anything other than localhost. Finish the HTTPS proxy setup, or set N8N_SECURE_COOKIE=false for a temporary local test and remove it afterwards.
What this means for your setup
If you’re already running n8n on a VPS with a handful of other containers, the practical takeaway is small: create one external edge network, move each database onto a private internal network with no published ports, and delete the 5678 mapping. That’s an afternoon at most, and it removes the most likely path from a compromised container to your workflow credentials.
The one hard requirement is a server you control the Docker daemon on. Shared hosting can’t do this, and container platforms that manage networking for you often won’t let you mark a network internal. A basic VPS with 2GB of RAM handles n8n plus Postgres plus a proxy without complaint; queue mode with multiple workers is where you start wanting 4GB and up.
Managed WordPress hosts sometimes bundle container tooling too, and the tooling worth having on bigger managed stacks overlaps more than you’d expect.
If you need a box to build this on, Hostinger’s VPS plans ship with Docker preinstalled as a template option, and the coupon code hHostCouponHub takes up to 85% off the first term. Root access and full iptables control are the two things to confirm before you buy anywhere, whoever you pick.
Frequently asked questions
Can containers on two different Docker networks reach each other?
No. There’s no route between two user-defined bridge networks, and Docker’s embedded DNS won’t resolve names across them. To connect them you have to attach a container to both networks with docker network connect or a second entry in the Compose networks: list.
Do I still need to publish port 5678 for n8n?
Only if you want to reach n8n directly from the host or the internet without a proxy. With a reverse proxy on the same edge network, the proxy connects to http://n8n:5678 over the Docker network and no host binding is needed. For local testing, bind it to 127.0.0.1:5678:5678 so it’s not reachable from outside.
How do I let n8n reach a database running on the host instead of in Docker?
Add extra_hosts:, "host.docker.internal:host-gateway" to the n8n service, then use host.docker.internal as the database host. The host service must be listening on the Docker bridge interface, not only on 127.0.0.1, or the connection will be refused.
Is Docker network isolation enough to secure a self-hosted n8n instance?
No, it’s one layer. You still need HTTPS, two-factor authentication on the n8n owner account, a strong encryption key kept out of version control, regular image updates, and no Docker socket mounted into the container. Isolation limits how far an attacker moves after getting in; it doesn’t stop them getting in.
Set up the two networks first, confirm the database is unreachable from anywhere else, then add services one at a time so you always know what changed. When you’re ready for a server that gives you full daemon access, grab the discount on a Hostinger VPS with the code hHostCouponHub and build it properly from the start.
This page contains affiliate links. If you purchase through the links on this page, we may earn a commission, at no extra cost to you.





1 thought on “Isolate n8n with Docker Networks to Lock Down Your Stack”