Dockerized Mock Environments
This page covers packaging mock API servers as Docker containers and wiring them together with Docker Compose — from a single WireMock service through to multi-service stacks integrated into a CI pipeline. It does not cover in-process interceptors or browser-layer mocking.
Prerequisites
- Docker Desktop 24+ or Docker Engine 24+ with the Compose v2 plugin (
docker compose version≥ 2.20) - A working knowledge of YAML and basic Docker concepts (images, volumes, networks)
- Stub mapping files in WireMock’s JSON format, or a willingness to generate them via record mode
- (Optional) Familiarity with WireMock standalone configuration if you intend to author complex request-matching rules
- (CI only) A pipeline runner that supports Docker-in-Docker or a Docker socket mount (GitHub Actions, GitLab CI, CircleCI all qualify)
Why containerise a mock server at all
Proxy-based mocking strategies intercept traffic at the TCP/HTTP layer rather than inside the application process. That means your application talks to a real socket, TLS handshakes happen, DNS resolves normally, and connection-level failure modes (reset packets, timeouts, refused connections) can be reproduced. Client-side tools like MSW setup excel at rapid UI iteration but cannot exercise this network path.
The diagram below shows where a containerised mock sits relative to the rest of a typical local-dev stack.
Phase 1 — Core setup: a single WireMock service
Start with the simplest configuration that actually works before adding complexity.
Directory layout
project-root/
├── docker-compose.yml
└── wiremock/
├── mappings/ # stub JSON files committed to source control
│ └── get-users.json
└── __files/ # static response body files (optional)
Stub mapping file
{
"mappings": [
{
"request": {
"method": "GET",
"url": "/api/users"
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"jsonBody": {
"users": [
{ "id": "u1", "name": "Alice Nguyen", "role": "admin" },
{ "id": "u2", "name": "Ben Carter", "role": "viewer" }
]
}
}
}
]
}
Core docker-compose.yml
version: "3.9"
services:
wiremock:
image: wiremock/wiremock:3.13.2
container_name: wiremock
# Run as the current user to avoid volume permission issues
user: "${UID:-1000}:${GID:-1000}"
ports:
- "8080:8080"
volumes:
- ./wiremock/mappings:/home/wiremock/mappings
- ./wiremock/__files:/home/wiremock/__files
command:
- "--verbose"
- "--port=8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
interval: 5s
timeout: 3s
retries: 6
start_period: 10s
networks:
default:
name: mock-net
Pin the image tag (3.13.2). Floating latest breaks reproducibility when WireMock ships a breaking change to its Admin API or default TLS behaviour.
Phase 2 — Configuration and wiring
Environment-variable-driven overrides
Real projects need different behaviour in development versus CI versus staging review environments. Use a .env file and an override compose file rather than editing the base docker-compose.yml.
.env (committed with safe defaults, .env.local gitignored for secrets):
WIREMOCK_TAG=3.13.2
WIREMOCK_PORT=8080
# Set to --record-mappings --proxy-all=https://api.example.com to enable record mode
WIREMOCK_EXTRA_ARGS=
docker-compose.override.yml (for local development only — also gitignored or conditionally applied):
version: "3.9"
services:
wiremock:
image: wiremock/wiremock:${WIREMOCK_TAG:-3.13.2}
ports:
- "${WIREMOCK_PORT:-8080}:8080"
command:
- "--verbose"
- "--port=8080"
- "${WIREMOCK_EXTRA_ARGS:-}"
Multi-service stack
When your application depends on more than one upstream (auth service, payments API, notification service), define a service per mock rather than stuffing all stubs into one WireMock instance. This keeps stub directories small, failure domains isolated, and port allocations explicit.
version: "3.9"
services:
app:
build: .
environment:
AUTH_API_URL: http://wiremock-auth:8080
PAYMENTS_API_URL: http://wiremock-payments:8080
depends_on:
wiremock-auth:
condition: service_healthy
wiremock-payments:
condition: service_healthy
networks:
- mock-net
wiremock-auth:
image: wiremock/wiremock:3.13.2
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./wiremock/auth/mappings:/home/wiremock/mappings
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
interval: 5s
timeout: 3s
retries: 6
start_period: 10s
networks:
- mock-net
wiremock-payments:
image: wiremock/wiremock:3.13.2
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./wiremock/payments/mappings:/home/wiremock/mappings
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
interval: 5s
timeout: 3s
retries: 6
start_period: 10s
networks:
- mock-net
networks:
mock-net:
driver: bridge
Services on the same named bridge network (mock-net) resolve each other by service name. The application container sets AUTH_API_URL=http://wiremock-auth:8080 — no host-port knowledge required, which removes a class of environment-specific bugs.
Persistent state vs ephemeral containers
Mock lifecycle management principles apply directly here. The right choice depends on the use case:
| Use case | Volume strategy | Rationale |
|---|---|---|
| CI test run | No named volume; bind-mount repo fixtures | Guarantees determinism; container teardown cleans state |
| Local iterative QA | Named volume for __files |
Persists recorded responses across restarts |
| Scenario-based testing | No volume; reset via Admin API between suites | Fastest; avoids filesystem I/O overhead |
For persistent scenarios, use a named volume and configure cleanup in your CI teardown hook:
volumes:
wiremock-state:
services:
wiremock:
image: wiremock/wiremock:3.13.2
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./wiremock/mappings:/home/wiremock/mappings
- wiremock-state:/home/wiremock/__files
Phase 3 — CI pipeline integration
The mock lifecycle management pattern for CI is: start → wait for healthy → test → reset → (repeat) → teardown. Never assume the container is ready after a fixed sleep; always use the healthcheck.
GitHub Actions example
name: Integration tests
on: [push, pull_request]
jobs:
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Export host UID/GID for volume permissions
run: echo "UID=$(id -u)" >> $GITHUB_ENV && echo "GID=$(id -g)" >> $GITHUB_ENV
- name: Start mock stack
run: docker compose up -d --wait
# --wait blocks until all healthchecks pass or the timeout is reached
- name: Run integration tests
run: npm test
- name: Tear down mock stack
if: always()
run: docker compose down --volumes
docker compose up -d --wait (Compose v2.1+) waits until every service with a healthcheck reports healthy before returning. This is more reliable than polling in a shell loop.
GitLab CI example
integration:
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- apk add --no-cache docker-compose-plugin curl
script:
- export UID=$(id -u) GID=$(id -g)
- docker compose up -d --wait
- npm test
after_script:
- docker compose down --volumes
Resetting state between test suites
For test suites that run in-process (e.g. Jest with a global setup/teardown), reset WireMock’s request journal and scenario state between suites without restarting the container:
# Reset all stubs, scenarios, and request log
curl -s -X POST http://localhost:8080/__admin/reset
# Or reset only the request journal (keeps stubs intact)
curl -s -X DELETE http://localhost:8080/__admin/requests
Calling /__admin/reset takes under 5 ms and avoids the 2–10 second overhead of container restart, which matters when running hundreds of test cases.
Verification steps
-
docker compose up -d --waitexits with code 0 and all services showhealthyindocker compose ps -
curl -s http://localhost:8080/__admin/health | jq .statusreturns"healthy" -
curl -s http://localhost:8080/api/users | jq .users[0].namereturns"Alice Nguyen" -
docker compose logs wiremockshows the matched stub ID and no"No mapping found"warnings for your test request - After
curl -X POST http://localhost:8080/__admin/reset,curl http://localhost:8080/__admin/requestsreturns{"requests":[]} -
docker compose down --volumes && docker compose up -d --waitproduces identical responses, confirming determinism
Troubleshooting
Error: No mapping found for GET /api/users
WireMock received the request but found no matching stub. Common causes:
- The stub JSON file is malformed — validate it with
python3 -m json.tool wiremock/mappings/get-users.json - The volume mount path is wrong — check
docker compose exec wiremock ls /home/wiremock/mappingsto confirm files are present - The
urlfield in the stub uses/api/usersbut the request hits/api/users?page=1— switch tourlPathPattern: "/api/users"and add anignoreExtraElementsbody matcher
Permission denied on volume mount at startup
WireMock’s official image runs as UID 1000 by default. If your host user is a different UID, the process cannot write to the mounted directory.
Fix: pass user: "${UID}:${GID}" in the service definition and export those variables before running Compose:
export UID=$(id -u) GID=$(id -g)
docker compose up -d --wait
service "wiremock" didn't reach healthy state after Xs
The healthcheck probe is failing. Inspect logs immediately after:
docker compose logs wiremock --tail 40
Common causes: the curl binary is not in the image (use wget -q -O- http://localhost:8080/__admin/health instead), the port is not yet bound, or an earlier flag is causing WireMock to exit on startup (check for flag typos in command:).
Port 8080 already in use on host
A local process (often another WireMock instance, a Spring Boot app, or a Jenkins agent) is already bound to 8080.
Fix: change the host-side port in docker-compose.yml — the container port stays 8080:
ports:
- "9090:8080" # host:container
Then update any localhost:8080 references in your application’s environment variables to localhost:9090.
docker compose up --wait hangs indefinitely
Compose --wait requires every service to have a healthcheck defined. If any service lacks one, Compose cannot determine when it is ready and blocks. Add a healthcheck to every service that your application depends_on, or upgrade to Compose v2.25+ where --wait-timeout gives you a hard ceiling.
When to advance
You are done with this setup when:
- All stubs are committed to source control, the container starts cleanly from a fresh checkout, and CI runs produce identical results across machines
- Stub mappings load automatically on container start with no manual API calls required
- The application’s environment variables point at the Docker service name (e.g.
http://wiremock:8080) rather thanlocalhost, so the same config works in both local and CI environments - You can onboard a new engineer by running
docker compose up -d --wait && npm testfrom a fresh clone
The natural next step is running WireMock in Docker Compose for more advanced scenarios: HTTPS termination, multi-profile stub loading, and response templating with Handlebars.
FAQ
Should I commit the wiremock-state Docker volume or keep it ephemeral?
Commit stub mapping JSON files under ./mappings to source control — those are your contract definitions. Keep __files (raw recorded response bodies) in a named volume or treat them as ephemeral. For CI, always start from a clean volume seeded by your repo’s fixtures to guarantee determinism across runs and machines.
How do I run WireMock in record mode inside Docker without network access issues?
Pass --record-mappings and --proxy-all=https://real-api.example.com to the WireMock entrypoint via the command: array. Map port 8080 to a host port and point your application at localhost:<host-port>. Ensure the container has egress to the real API’s host — if your Docker network has no internet access, use network_mode: bridge or add a DNS entry in your docker-compose.yml. Response shaping techniques covers how to post-process recorded responses before committing them.
Can I run multiple mock services on different ports in a single Compose stack?
Yes. Define each mock as a separate named service. Services on the same custom bridge network can address each other by service name (http://wiremock-auth:8080), so containers never need to know host port assignments. Only expose host ports that your local browser or external tooling actually needs.
What is the fastest way to reset WireMock state between test runs without restarting the container?
POST http://localhost:8080/__admin/reset — this clears the request journal and resets all scenarios to their initial state in under 5 ms. Pair it with a beforeAll or afterAll hook in your test framework. For journal-only resets (keep stubs active), use DELETE http://localhost:8080/__admin/requests.
Related
- Running WireMock in Docker Compose — advanced HTTPS, templating, and multi-profile stub loading
- WireMock Standalone Configuration — request matching rules, scenario state machines, and stub authoring
- Mock Service Worker (MSW) Setup — browser-layer and Node interception for frontend and unit test workflows
- Mock Lifecycle Management — start, reset, and teardown patterns that apply across all mock server types
- Proxy vs Inline Mocking Strategies — when network-boundary mocking is the right choice vs in-process interception
← Back to Tool-Specific Implementation & Setup