Note (2026): The ambassador pattern is outdated. Docker 1.9+ (2015) introduced user-defined networks, which solve this problem more elegantly. Containers on the same network see each other by service name without
links. This article is preserved as a historical artifact; the problem described here is no longer current.Modern solution:
services: fpm: networks: [app] nginx: networks: [app] networks: app:
When designing an information system, clarity is very important. There is nothing better than an elegantly built application with no unnecessary details, including unnecessary links between components.
Fortunately, Docker lets us isolate processes from each other and organize communication between them by means of predeclared ports and shared directories. In most cases, only a one-way connection between containers is needed for the whole thing to succeed, for example: a web server uses php-fpm, or php-fpm uses a database. In this simple case, no problems arise; establishing such a connection between several containers using Docker Compose is very easy:
# ~/docker-apps/testapp/test.yml
fpm:
build: ./build/fpm
nginx:
build: ./build/nginx
links:
- fpm
We start it. As you can see, everything works:
docker-compose -f test.yml up -d
Creating testapp_fpm_1...
Creating testapp_nginx_1...
docker-compose ps
Name Command State Ports
-------------------------------------------------------------------------
testapp_fpm_1 /usr/sbin/php5-fpm -F Up 9000/tcp
testapp_nginx_1 /usr/sbin/nginx Up 80/tcp
From the testapp_nginx_1 container, testapp_fpm_1 can be addressed through the hosts written by Docker itself. Let us look at /etc/hosts:
docker exec testapp_nginx_1 cat /etc/hosts
...
172.17.0.5 testapp_fpm_1 2bae5bf71e09
172.17.0.5 fpm 2bae5bf71e09 testapp_fpm_1
172.17.0.5 fpm_1 2bae5bf71e09 testapp_fpm_1
In principle, a situation is possible where both linked processes must know about each other. For example, if Selenium is running: it needs access to the web server in order to execute test tasks; but the web server also needs access to Selenium itself in order to start those tasks through a BDD framework. For clarity of the example relative to the previous one, let us suppose that we need access from nginx to php-fpm and back.
# ~/docker-apps/testapp/test2.yml
fpm:
build: ./build/fpm
links:
- nginx
nginx:
build: ./build/nginx
links:
- fpm
Do we start it?
docker-compose -f test2.yml up -d
Circular import between fpm and nginx
Fortunately, there is a solution. It is impossible to add cross-links only at the startup stage, but no one can forbid adding them after startup. Ambassador comes to the rescue: an image of a proxy process whose only task is creating cross-links between containers.
# ~/docker-apps/testapp/test3.yml
fpm:
build: ./build/fpm
links:
- ambassador:nginx
nginx:
build: ./build/nginx
links:
- ambassador:fpm
ambassador:
image: cpuguy83/docker-grand-ambassador
volumes:
- '/var/run/docker.sock:/var/run/docker.sock'
command: '-name testapp_fpm_1 -name testapp_nginx_1'
We start it:
docker-compose -f test3.yml up -d
Creating testapp_ambassador_1...
Creating testapp_nginx_1...
Creating testapp_fpm_1...
What happened?
docker exec astgo_nginx_1 cat /etc/hosts
...
172.17.0.11 ambassador_1 f3437e869aff testapp_ambassador_1
172.17.0.11 testapp_ambassador_1 f3437e869aff
172.17.0.11 fpm f3437e869aff testapp_ambassador_1
As you can see, Docker added the standard records for ambassador, and then ambassador added a record pointing to the fpm container. It is worth noting: not directly, but through that same ambassador.