The Zoo is a simulated web environment. It's meant to be complex enough for true end-to-end testing (like the live web), while being reproducible (unlike the live web).
Web services are hosted on the .zoo domain accessible through a forward proxy.
Paper · Installation · Available Sites
The Zoo needs Node 22.9+, Docker Engine 25+ with Compose 2.24.4+, ~8 GB of memory for Docker and roughly 75 GB of disk for a checkout (npm start builds everything and pulls the heavy apps); npx the_zoo start needs ~15 GB (~35 GB with --with-heavy). npm run cli -- doctor checks these.
npm install
npm startTo upgrade a checkout, pull and run npm install && npm run stop && npm start. npm run reset is now npm run cli -- reset.
For targeted live tests, npm run test:focused -- tests/sites/misc-api.test.ts skips the suite-wide on-demand app warm-up. Use npm test for the full suite and for tests/integration/on-demand-start.test.ts, which needs the cold-start probe.
Without a checkout, the npm package runs the published images: npx the_zoo start (--with-heavy adds onestopshop.zoo and postmill.zoo).
For install/start issues, see here.
- Squid Proxy (Port 3128) - HTTP proxy for host browser access
- CoreDNS - DNS for all containers; resolves
.zoodomains from the composezoo.domainslabels - Caddy - Reverse proxy and static file server. Also handles SSL, with private keys committed for reproducibility.
- PostgreSQL, MySQL - Restore their golden state on every start, except after a crash
- Redis - Key-value store
- Stalwart - Mail server (GitHub)
- Hydra - OAuth2/OpenID Connect server (Ory Hydra)
- Apps - Compose services with a
zoo.domainslabel; custom images live insites/apps/ - Static Sites - Located in
sites/static/, served directly by Caddy - Zoo Sites - The published zoo-sites image serves 66 simulated sites, including
voltro.zoo,nimbrel.zoo, anddrennhill-dental.zoo. They share one on-demand container with in-memory state that resets on restart.EVAL_SEED=zoopins the difficulty draws; session identifiers remain random.
To update Zoo Sites, pin a published tag (by digest) in docker-compose.yaml, copy the domain mappings from that commit's docker/zoo-snippet.yaml, update core/zoo-sites-titles.json, add a Matomo site for each new domain (docs/analytics.md), run npm run generate-config, recreate zoo-sites and restart caddy and coredns.
npm run browse opens a configured Playwright Firefox (run npx playwright install firefox first; on Linux, npx playwright install --with-deps firefox). The zoo-playwright MCP server in .mcp.json needs its own: npx playwright-mcp install-browser firefox. It uses the published proxy port of the running Zoo instance; if several are running, set ZOO_BROWSER_INSTANCE to an instance ID or Compose project name before launching the MCP server. Run npm run cli -- status --json to see available instances. For a normal Firefox:
- Create a new profile (about:profiles) and copy
docs/firefox-profile/user.jsinto its folder (about:support shows it). It sets the proxy (localhost:3128) and.zoohandling. - Import
core/caddy/root.crtunder about:preferences#privacy → Certificates → View Certificates, and trust it for websites. - Restart Firefox.
Per-app logins for every persona are in docs/credentials/.
| Screenshot | Site | Description |
|---|---|---|
![]() |
analytics.zoo | Web analytics platform powered by Matomo |
![]() |
auth.zoo | Single sign-on for the zoo's apps |
![]() |
classifieds.zoo | Classified ads marketplace from VisualWebArena |
![]() |
example.zoo | Static example site |
![]() |
excalidraw.zoo | Virtual whiteboard for sketching diagrams |
![]() |
focalboard.zoo | Open source project management and kanban boards |
![]() |
gitea.zoo | Self-hosted Git service with web interface |
![]() |
home.zoo | Directory of every zoo site |
![]() |
miniflux.zoo | Minimalist feed reader with RSS/Atom support |
![]() |
mattermost.zoo | Team messaging and collaboration platform |
![]() |
misc.zoo | Miscellaneous utilities and test endpoints |
![]() |
northwind.zoo | Northwind sample database with phpMyAdmin interface |
![]() |
onestopshop.zoo | E-commerce shopping site from VisualWebArena |
![]() |
paste.zoo | Self-hosted pastebin |
![]() |
performance.zoo | Performance testing and monitoring tools |
![]() |
postmill.zoo | Reddit-like forum and link aggregator from VisualWebArena |
![]() |
snappymail.zoo | Modern webmail client with clean interface |
![]() |
wiki.zoo | Offline Wikipedia reader and knowledge base |
The 65 Zoo Sites domains are listed on home.zoo and in core/SITES.yaml.
If a build fails with wget: unable to resolve host address 'dl-cdn.alpinelinux.org', give Docker a DNS server in /etc/docker/daemon.json:
{
"dns": ["8.8.8.8", "1.1.1.1"]
}Then restart Docker:
sudo systemctl restart dockerIf a start fails with Pool overlaps with other one on this address space, another Docker network holds the Zoo's subnets: set ZOO_SUBNET, ZOO_PUBLIC_SUBNET and the ZOO_*_IP values in .env (see .env.example).


















