Article summary
If you have a containerized application composed of multiple services, it can become a burden to string together in-network communication. Fortunately, Docker Compose solves this networking problem by making it easy for your containers to communicate without knowing one another’s context. In a couple of short steps, I’ll show you how to quickly get a custom network running in your application and inject container references into your project’s environment for quick setup.
Networking Basics
You should already have your individual projects set up with Docker files. In the root of your project, add a docker-compose.yml
file that looks similar to this:
version: "3.7"
services:
api-external:
build:
context: .
dockerfile: ./MyApp.APIExternal/Dockerfile
ports:
- "5000:80"
api-internal:
build:
context: .
dockerfile: ./MyApp.APIInternal/Dockerfile
ports:
- "6000:80"
db:
image: postgres
ports:
- "8001:5432"
web:
build:
context: .
dockerfile: ./MyApp.Web/Dockerfile
ports:
- "9220:80"
In this example, I’ve set up a web project with two APIs (internal and external) and a Postgres database. The web project requires the use of the external API, whereas the internal API relies on the database.
When we run this compose file, a default network called myapp_default
is created. Each container (api-external, api-internal, db, and web) joins the myapp_default
network and can now look up the hostnames of the others.
To show that, let’s make some changes to the docker-compose.yml
file. We’d like web
to get the IP of api-external
, so we’ll pass an environment variable using Docker’s environment
key. It will match an AppSettings variable defined in the web’s appsettings.json
under AppSettings:APIBaseUrl
.
web:
build:
context: .
dockerfile: ./MyApp.Web/Dockerfile
ports:
- "9220:80"
environment:
- AppSettings:APIBaseURL=http://api-external
Inside the container, the APIBaseURL
will look like http://api-external
. But from the host machine, the url will resolve to http://{DOCKER_IP}
. Now we can have full communication between containers.
Put it all together for the other services, and it looks something like this:
version: "3.7"
services:
api-external:
build:
context: .
dockerfile: ./MyApp.APIExternal/Dockerfile
ports:
- "5000:80"
environment:
- AppSettings:APIBaseURL=http://api-internal
api-internal:
build:
context: .
dockerfile: ./MyApp.APIInternal/Dockerfile
ports:
- "6000:80"
environment:
- AppSettings:DBConnectionString=postgres://db:5432
db:
image: postgres
ports:
- "8001:5432"
web:
build:
context: .
dockerfile: ./MyApp.Web/Dockerfile
ports:
- "9220:80"
environment:
- AppSettings:APIBaseURL=http://api-external
Custom Networks
This works really well on its own, but lets say we don’t want web
to know anything about api-internal
or db
. In that case, we’ll need to put these services on separate networks by defining them using the network
key. Here we’ll define two networks: frontend and backend.
networks:
frontend:
backend:
We can now have a service be connected to a defined network using the networks
key:
web:
networks:
- frontend
build:
context: .
dockerfile: ./MyApp.Web/Dockerfile
ports:
- "9220:80"
environment:
- AppSettings:APIBaseURL=http://api-external
Specifying these two new custom networks now allows us to create specific connections between services. We’ll also add the depends_on
key to ensure that each service builds in the correct order. Put it all together, and it looks like this:
version: "3.7"
services:
api-external:
depends_on:
- api-internal
networks:
- frontend
- backend
build:
context: .
dockerfile: ./MyApp.APIExternal/Dockerfile
ports:
- "5000:80"
environment:
- AppSettings:APIBaseURL=http://api-internal
api-internal:
depends_on:
- db
networks:
- backend
build:
context: .
dockerfile: ./MyApp.APIInternal/Dockerfile
ports:
- "6000:80"
environment:
- AppSettings:DBConnectionString=postgres://db:5432
db:
networks:
- backend
image: postgres
ports:
- "8001:5432"
web:
depends_on:
- api-external
networks:
- frontend
build:
context: .
dockerfile: ./MyApp.Web/Dockerfile
ports:
- "9220:80"
environment:
- AppSettings:APIBaseURL=http://api-external
networks:
frontend:
backend:
The web
service now only knows of other services on its shared network. If we try to pass the reference of, lets say, api-internal
through the environment variables of web
, it will be unable to resolve.
web:
networks:
- frontend
build:
context: .
dockerfile: ./MyApp.Web/Dockerfile
ports:
- "9220:80"
environment:
# This will fail!
- AppSettings:APIBaseURL=http://api-internal
Now we have all the tools we need to quickly define networks for easy communication between our projects and services.
There is obviously much more you can do with Docker Compose networks, including using external networks and custom drivers. To read more about that, I highly recommend checking out the documentation from Docker.