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.pycompose.yamlstarts three ZooKeeper servers and five animal clients.Dockerfilebuilds the shared Python client image.requirements.txtinstalls Kazoo, the Python ZooKeeper library.client.pyregisters each animal and lets Zebra watch the water limit.README.txtcontains 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.
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.
docker compose config --quiet
docker compose up -d --build
docker compose psCheck 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:
docker compose exec zoo1 zkServer.sh status
docker compose exec zoo2 zkServer.sh status
docker compose exec zoo3 zkServer.sh statusYou 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
docker compose logs --tail=30 zebra giraffe deer bear elephantLook for a registration message for each animal. To inspect the znodes, open the ZooKeeper command-line client:
docker compose exec zoo1 zkCli.sh -server zoo1:2181At the ZooKeeper prompt, run:
ls /zoo
get /zoo/zebra-statusYou 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:
docker compose logs -f zebraBack at the ZooKeeper prompt, change the water limit:
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:
docker compose stop bearRun 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:
docker compose start bearIts status entry returns once it reconnects. Type quit to exit the ZooKeeper client.
When you finish
docker compose downThis 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.