I'm building a containerized application where both the frontend and backend are served from the same Compose file:
version: '3.8'
services:
frontend:
build: ./frontend
ports: ['8001:80']
backend:
build: ./backend
ports: ['8002:5000']
My frontend code needs to request a JSON resource from the backend. Since both the frontend and the backend are in the same Compose file, I'd expect that Docker networking will allow me to use the backend's name as a host name, like
fetch('http://backend:5000/hello')
When I make this call, though, I get a Javascript error
TypeError: NetworkError when attempting to fetch resource.
and if I look in the browser debug tools on the "network" tab, I see an error code NS_ERROR_UNKNOWN_HOST
.
Why doesn't the Docker service name work as a host name here?
When you have a browser-based application, the code is actually running in the browser. Even if you have a container serving its code, it's actually being executed on the host system (or a remote system) and not in Docker. The browser is the entity making the fetch()
call, and the browser itself isn't running in a container. That means the browser is trying to resolve the backend
host name, and producing that error.
If you're running this all on the same system, the "easy" way around this is to separately publish a port for the backend service. In the Compose file in the question, that's published on port 8002. If you change the fetch
call to
fetch('http://localhost:8002/hello')
it will access the backend through this published port.
However: this setup only works if the browser and the containers are running on the same machine. If you go to deploy the service somewhere else, this host name will need to be different too. This call also triggers the rules on Cross-Origin Resource Sharing (CORS), and the backend service needs to be aware of this.
A better approach is to run an HTTP reverse proxy. The proxy runs in a container, so it's "inside Docker" and can resolve the backend
host name. The browser uses a path-only URL to request the additional resource from the same host and port that served it, so it doesn't need to know any host name at all. This also resolves the production-deployment and CORS issues.
This involves one additional container in the Compose setup
version: '3.8'
services:
frontend: { ... }
backend: { ... }
proxy:
build: ./proxy
ports: ['8000:80']
The container could be based on Nginx, requiring only adding its configuration
# proxy/Dockerfile
FROM nginx:1.25
COPY default.conf /etc/nginx/conf.d
The proxy configuration can be very simple. Paths that begin with /api/
get forwarded to the backend
container; anything else gets forwarded to the frontend
.
# proxy/default.conf
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://frontend:80/;
}
location /api/ {
proxy_pass http://backend:5000/;
}
}
Now in the Javascript code you can change the fetch
call to
fetch('/api/hello');
Note that the URL does not contain an http
URL scheme or any host name or port; this path-only URL is resolved relative to the page's URL.
I've written a small demo application showing these different call patterns and what URLs work in which contexts.