System Design / Practical · Part 2

Zoo services with Docker

Bring our zoo to life with three ZooKeeper servers and five animal-service containers on your computer.

1. Add the five animal services

Complete Part 1: ZooKeeper servers and terminal commands first. Keep using that project folder so the ensemble and its saved data are reused. Exit any open ZooKeeper CLI sessions before continuing; this removes their temporary status entries.

Download the complete example and extract it. It contains compose.yaml and an animal-service folder with the shared Python client, its Dockerfile, and its dependencies. Keep that folder next to compose.yaml.

Download the complete Docker example (.zip) ↓

Open a terminal in the extracted zoo-services-docker folder. If you already have the earlier setup, replace its compose.yaml and add the animal-service folder in the same directory. The YAML alone now needs these extra files to build the animal containers.

Files in this download

compose.yaml
README.txt
animal-service/
  Dockerfile
  requirements.txt
  client.py
  • compose.yaml starts three ZooKeeper servers and five animal clients.
  • Dockerfile builds the shared Python client image.
  • requirements.txt installs Kazoo, the Python ZooKeeper library.
  • client.py registers each animal and lets Zebra watch the water limit.
  • README.txt contains the commands to run this lesson.

If you use a separate project folder instead, stop the first lesson with docker compose down from its original folder before starting this one, so the host ports are available.

You can also copy the Compose configuration below or download compose.yaml separately.

compose.yaml
services:
  zoo1:
    image: zookeeper:3.9.5
    hostname: zoo1
    restart: unless-stopped
    ports:
      - "127.0.0.1:2181:2181"
    environment:
      ZOO_MY_ID: "1"
      JVMFLAGS: "-Dzookeeper.extendedTypesEnabled=true"
      ZOO_SERVERS: "server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181"
    volumes:
      - zoo1-data:/data
      - zoo1-log:/datalog

  zoo2:
    image: zookeeper:3.9.5
    hostname: zoo2
    restart: unless-stopped
    ports:
      - "127.0.0.1:2182:2181"
    environment:
      ZOO_MY_ID: "2"
      JVMFLAGS: "-Dzookeeper.extendedTypesEnabled=true"
      ZOO_SERVERS: "server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181"
    volumes:
      - zoo2-data:/data
      - zoo2-log:/datalog

  zoo3:
    image: zookeeper:3.9.5
    hostname: zoo3
    restart: unless-stopped
    ports:
      - "127.0.0.1:2183:2181"
    environment:
      ZOO_MY_ID: "3"
      JVMFLAGS: "-Dzookeeper.extendedTypesEnabled=true"
      ZOO_SERVERS: "server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181"
    volumes:
      - zoo3-data:/data
      - zoo3-log:/datalog

  zebra:
    build: ./animal-service
    restart: unless-stopped
    environment:
      ANIMAL_NAME: zebra
      ZK_HOSTS: zoo1:2181,zoo2:2181,zoo3:2181
    depends_on:
      - zoo1
      - zoo2
      - zoo3

  giraffe:
    build: ./animal-service
    restart: unless-stopped
    environment:
      ANIMAL_NAME: giraffe
      ZK_HOSTS: zoo1:2181,zoo2:2181,zoo3:2181
    depends_on:
      - zoo1
      - zoo2
      - zoo3

  deer:
    build: ./animal-service
    restart: unless-stopped
    environment:
      ANIMAL_NAME: deer
      ZK_HOSTS: zoo1:2181,zoo2:2181,zoo3:2181
    depends_on:
      - zoo1
      - zoo2
      - zoo3

  bear:
    build: ./animal-service
    restart: unless-stopped
    environment:
      ANIMAL_NAME: bear
      ZK_HOSTS: zoo1:2181,zoo2:2181,zoo3:2181
    depends_on:
      - zoo1
      - zoo2
      - zoo3

  elephant:
    build: ./animal-service
    restart: unless-stopped
    environment:
      ANIMAL_NAME: elephant
      ZK_HOSTS: zoo1:2181,zoo2:2181,zoo3:2181
    depends_on:
      - zoo1
      - zoo2
      - zoo3

volumes:
  zoo1-data:
  zoo1-log:
  zoo2-data:
  zoo2-log:
  zoo3-data:
  zoo3-log:

The three ZooKeeper services form one ensemble. Each has a unique ZOO_MY_ID and the same membership list in ZOO_SERVERS. They elect one leader and two followers; zoo1 is not a fixed leader. A quorum needs two of the three servers.

Client connections use local ports 2181, 2182, and 2183. Inside Docker, the servers communicate using their service names. Named volumes retain each server’s data when containers are recreated. This is a local learning setup: all three servers share your computer.

Five more containers represent Zebra, Giraffe, Deer, Bear, and Elephant. Each runs the same Python client with a different ANIMAL_NAME. They are clients, not voting members of the ensemble. Each creates an ephemeral status entry such as /zoo/zebra-status. Kazoo, the Python ZooKeeper library, sends session heartbeats automatically.

Start and check the ensemble

Run these commands from the folder containing compose.yaml. The first validates the file; the second builds the animal client and starts all eight containers in the background. The first build needs internet access to download the base images and Python dependency.

Start the ensemble
docker compose config --quiet
docker compose up -d --build
docker compose ps

Check that all eight services are running. The animal clients retry their connection while the ensemble starts. Allow a few seconds for leader election, then check each server’s role:

Check leader and followers
docker compose exec zoo1 zkServer.sh status
docker compose exec zoo2 zkServer.sh status
docker compose exec zoo3 zkServer.sh status

You should see one Mode: leader and two Mode: follower results. If a server is still starting, wait and check again. Use docker compose logs to investigate startup errors.

See the animals register

View animal logs
docker compose logs --tail=30 zebra giraffe deer bear elephant

Look for a registration message for each animal. To inspect the znodes, open the ZooKeeper command-line client:

Open ZooKeeper client
docker compose exec zoo1 zkCli.sh -server zoo1:2181

At the ZooKeeper prompt, run:

List animal status entries
ls /zoo
get /zoo/zebra-status

You should see the five animal status entries and water-limit. The status data identifies Zebra as available.

Change the water limit and notify Zebra

In a second terminal in the same folder, follow Zebra’s logs:

Follow Zebra logs
docker compose logs -f zebra

Back at the ZooKeeper prompt, change the water limit:

Change water limit in ZooKeeper
set /zoo/water-limit "800 L"

Zebra prints ALARM: water limit changed: 800 L. Set a different value for another notification. Kazoo’s DataWatch reads the current data and registers another one-time watch automatically. The notification itself does not contain the new value. Press Ctrl+C to stop following logs; this leaves the container running.

See an ephemeral status disappear

In a normal terminal, stop Bear:

Stop Bear service
docker compose stop bear

Run ls /zoo again at the ZooKeeper prompt. bear-status disappears when Bear closes its session. If a client crashes without closing its session, ZooKeeper waits for session expiry instead. Start Bear again:

Start Bear service
docker compose start bear

Its status entry returns once it reconnects. Type quit to exit the ZooKeeper client.

When you finish

Stop the ensemble
docker compose down

This removes the containers and network while keeping the named data volumes.

Configuration follows the official ZooKeeper Docker image guide. See also Docker’s Compose installation guide and Kazoo’s watch documentation.

← Return to ZooKeeper, step by step