• Docker ambassador pattern

    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.