Managing Mock Server Lifecycles in Docker
Containerised mock servers boot faster than real backends and produce deterministic responses, but they introduce a new class of failure: your application container starts before the mock engine has loaded its route definitions. The result is ECONNREFUSED errors, false-negative CI failures, and flaky integration tests that pass on rerun — the classic sign of a race condition rather than a real bug.
This page resolves that race by showing the exact Docker Compose configuration and entrypoint patterns that synchronise mock server readiness with dependent services, and that keep request logs intact when containers shut down.
Context: why this race condition happens
Mock lifecycle management distinguishes three phases for any mock server: initialisation (loading fixtures and route definitions), serving (accepting and responding to requests), and teardown (flushing state and releasing ports). Docker Compose, by default, only models the container start event — it has no built-in concept of “route definitions are loaded and the server is ready”.
The depends_on directive in older Compose files waits for a container to reach the running state, which happens the moment the process starts, not when it finishes its own initialisation. WireMock and similar servers may take several seconds to scan and register fixtures from disk, during which any inbound request returns a 404 or connection error.
Three configuration gaps together cause the problem:
- No
healthcheckdefined on the mock service — Docker has no signal to wait for. depends_onwithoutcondition: service_healthy— dependent containers start immediately.- No entrypoint validation — the container starts even if fixture files are missing or corrupted.
The diagram below shows the unsafe default timing versus the controlled startup sequence this page implements.
Solution
Step 1 — Write a fixture-validating entrypoint script
The container entrypoint script runs before the mock engine starts. Use it to abort early if fixtures are missing and to hold open the container process until the engine signals readiness on its health endpoint. This gives Docker’s health check a reliable surface to probe.
#!/bin/bash
# entrypoint.sh
set -e
FIXTURE_DIR="${FIXTURE_DIR:-/mocks/fixtures}"
HEALTH_URL="${HEALTH_URL:-http://localhost:3000/__health}"
MAX_WAIT="${MAX_WAIT:-60}"
# 1. Validate fixture directory exists and is non-empty
if [ ! -d "$FIXTURE_DIR" ]; then
echo "ERROR: Fixture directory '$FIXTURE_DIR' is missing. Mount it as a volume." >&2
exit 1
fi
if [ -z "$(ls -A "$FIXTURE_DIR" 2>/dev/null)" ]; then
echo "ERROR: Fixture directory '$FIXTURE_DIR' is empty. Add fixture files before starting." >&2
exit 1
fi
echo "Fixtures validated. Starting mock engine..."
# 2. Start mock server in background, keep PID for wait
mock-server --config /etc/mock/config.yaml --fixtures "$FIXTURE_DIR" &
MOCK_PID=$!
# 3. Poll readiness endpoint with a hard timeout
ELAPSED=0
until curl -sf "$HEALTH_URL" > /dev/null 2>&1; do
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
echo "ERROR: Mock server did not become healthy within ${MAX_WAIT}s." >&2
kill "$MOCK_PID" 2>/dev/null
exit 1
fi
echo "Waiting for mock server readiness... (${ELAPSED}s elapsed)"
sleep 2
ELAPSED=$((ELAPSED + 2))
done
echo "Mock server is healthy. Container ready."
wait "$MOCK_PID"
Set the FIXTURE_DIR, HEALTH_URL, and MAX_WAIT values as Docker environment variables so the same image works across local dev and CI without rebuilding.
Step 2 — Configure Docker health checks and dependency ordering
Add an explicit healthcheck to the mock service and use condition: service_healthy in every dependent service’s depends_on block. This wires Docker’s internal readiness model to your entrypoint’s signalling.
# docker-compose.yml
services:
api-mock:
image: wiremock/wiremock:3.13.2
entrypoint: ["/bin/bash", "/entrypoint.sh"]
environment:
FIXTURE_DIR: /mocks/fixtures
HEALTH_URL: http://localhost:8080/__admin/health
MAX_WAIT: "60"
volumes:
- ./mocks/fixtures:/mocks/fixtures:ro
- ./entrypoint.sh:/entrypoint.sh:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 15s
networks:
- dev-sim
frontend-app:
build: ./client
depends_on:
api-mock:
condition: service_healthy
environment:
API_BASE_URL: http://api-mock:8080
networks:
- dev-sim
e2e-tests:
build: ./tests
depends_on:
api-mock:
condition: service_healthy
frontend-app:
condition: service_started
networks:
- dev-sim
networks:
dev-sim:
driver: bridge
Key points in this config:
start_period: 15sgives WireMock time to scan fixtures before the health check retry counter starts. Retries that fire during the start period do not count as failures.retries: 10withinterval: 5smeans Docker waits up to 50 seconds after the start period before marking the service unhealthy.- The
dev-simbridge network isolates mock traffic so mock port numbers never collide with host services.
The proxy vs inline mocking strategies page covers when to put the mock on a separate network segment versus wiring it directly through the host network — relevant if you need the mock to intercept traffic from processes outside Docker.
Step 3 — Graceful teardown and request log persistence
When Docker stops a container it sends SIGTERM. If no stop_signal is configured, some images default to SIGKILL, which bypasses any in-process shutdown hook and corrupts in-memory request logs. WireMock keeps its request journal in memory by default; it is lost on SIGKILL.
The response shaping techniques cluster explains why request journals matter: replaying captured traffic lets you generate realistic dynamic responses from real-world payloads. If the journal is lost at teardown, that replay option disappears.
services:
api-mock:
image: wiremock/wiremock:3.13.2
stop_signal: SIGTERM
stop_grace_period: 30s
volumes:
- ./mocks/fixtures:/mocks/fixtures:ro
- ./mock-logs:/var/log/mock-requests
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 15s
To capture the journal before the process exits, add a preStop hook in the entrypoint’s SIGTERM trap. Because WireMock exposes its journal via GET /__admin/requests, you can write it to the mounted log volume from inside the container:
#!/bin/bash
# entrypoint.sh — add signal trap before starting the mock engine
flush_journal() {
echo "SIGTERM received: flushing request journal..."
curl -sf http://localhost:8080/__admin/requests \
> /var/log/mock-requests/journal-$(date +%Y%m%dT%H%M%S).json || true
echo "Journal flushed."
}
trap 'flush_journal; kill "$MOCK_PID" 2>/dev/null; wait "$MOCK_PID"' TERM
# ... rest of entrypoint continues as above
With stop_grace_period: 30s, Docker waits 30 seconds after sending SIGTERM before issuing SIGKILL. The journal flush typically completes in under two seconds, leaving ample headroom.
Verification
Run the following commands after starting the stack to confirm the lifecycle controls are active:
# 1. Confirm the mock service reaches healthy status
docker compose ps api-mock
# Expected: STATUS = healthy
# 2. Query the WireMock admin API to verify routes loaded
curl -s http://localhost:8080/__admin/mappings | jq '.total'
# Expected: a positive integer matching your fixture count
# 3. Trigger a teardown and confirm the journal file is written
docker compose stop api-mock
ls -lh ./mock-logs/
# Expected: a journal-*.json file with recent timestamp and non-zero size
# 4. Inspect journal content
cat ./mock-logs/journal-*.json | jq '.requests | length'
# Expected: number of requests served during the session
If docker compose ps shows starting instead of healthy after the start period, the health check is failing. Check docker compose logs api-mock for fixture validation errors or port binding failures.
Gotchas and edge cases
-
start_perioddoes not pause retries — it discards failures. Docker still runs health check probes during the start period; it just does not decrement the retry counter when they fail. If your mock engine takes longer thanstart_periodto become healthy, increasestart_periodrather thanretries. Raisingretriesalone extends the window but starts counting failures from container start. -
Volume mount ordering on macOS. On Docker Desktop for Mac, bind-mount volume propagation can lag by a few hundred milliseconds after the container starts. If your entrypoint script checks the fixture directory immediately on startup, add a brief
sleep 1before the directory check — or switch to a named volume, which Docker initialises synchronously. -
WireMock’s
/__admin/healthreturns 200 before scenario states load. If you use WireMock stateful scenario sequences, the admin health endpoint reports healthy as soon as the HTTP listener is up, not after scenarios are registered. Add an additional probe that queriesGET /__admin/scenariosand asserts the expected scenario count before allowing dependent containers to start.
Related
- Running WireMock in Docker Compose — concrete WireMock-specific Compose setup with stub directory conventions
- When to Use Proxy vs Inline Mocking — deciding whether the mock server lives inside or outside the Docker network
- Abstracting Network Layers for Frontend Apps — environment-variable-driven URL switching so the same frontend image points at the mock in dev and the real API in production
← Back to Mock Lifecycle Management