Running WireMock in Docker Compose: Exact Configuration & Troubleshooting
WireMock launches fine interactively, then breaks the moment it runs inside Docker Compose: stubs vanish on restart, the container starts before mappings are readable, or a UID mismatch makes the volume unwritable. This page resolves each failure with exact configuration and targeted commands.
Context: why these failures happen
Three structural gaps cause almost every Docker-based WireMock issue:
- Ephemeral container state. Stubs registered at runtime via the Admin API live in memory. When the container restarts, they are gone — unless they are backed by bind-mounted JSON mapping files.
- UID/GID mismatch. The official
wiremock/wiremockimage runs as a non-root user (UID 1000). If Docker creates the mount directories as root, the container user cannot write logs or auto-detected mappings. - Startup ordering. Services that depend on WireMock start before
/__admin/healthresponds, causing connection-refused errors at test time. A properhealthcheck+depends_oncondition prevents this.
These issues are part of the broader challenge of managing mock server lifecycles in Docker, where startup sequencing and state persistence interact across the container boundary.
Solution
Step 1 — Create host directories before the stack starts
Docker auto-creates bind-mount directories as root when they are missing. Create them manually first so the non-root container user can write to them:
mkdir -p wiremock/{mappings,__files,logs}
Run this once in the project root. Add it to your team’s Makefile or setup.sh so every developer runs it before docker compose up.
Step 2 — Add the WireMock service to docker-compose.yml
Below is a complete, production-aligned service definition. Pin the image tag; latest is not a version.
services:
wiremock:
image: wiremock/wiremock:3.13.2
container_name: wiremock
restart: unless-stopped
user: "${UID:-1000}:${GID:-1000}"
ports:
- "0.0.0.0:${WIREMOCK_PORT:-8080}:8080"
- "0.0.0.0:${WIREMOCK_TLS_PORT:-8443}:8443"
command:
- "--root-dir=/home/wiremock"
- "--global-response-templating"
- "--enable-stub-cors"
- "--max-request-journal-size=1000"
volumes:
- ./wiremock/mappings:/home/wiremock/mappings
- ./wiremock/__files:/home/wiremock/__files
- ./wiremock/logs:/home/wiremock/logs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 8s
Key decisions embedded in this definition:
user: "${UID:-1000}:${GID:-1000}"— matches the container process to your host UID so bind-mount writes succeed. ExportUIDandGIDin your shell or.envfile.--root-dir=/home/wiremock— aligns the CLI flag with the volume mount target. If these diverge, WireMock loads no mappings and returns404for every stub.--max-request-journal-size=1000— caps heap growth during long QA runs. Without this, the journal grows unbounded until the JVM exhausts its heap.start_period: 8s— gives the JVM time to initialise before health-check failures count against the retry limit.
Step 3 — Wire dependent services to wait for readiness
Any service that calls WireMock must declare a depends_on condition so Compose waits until the health check passes:
services:
frontend:
image: node:22-alpine
depends_on:
wiremock:
condition: service_healthy
environment:
- VITE_API_BASE=http://wiremock:8080
Inside a Compose network, services reach each other by service name (wiremock), not localhost. The VITE_API_BASE environment variable lets you switch between mock and real API without changing source code — a pattern covered in depth under proxy vs inline mocking strategies.
Step 4 — Write stub mapping files
Place JSON mapping files in wiremock/mappings/. WireMock loads every *.json file in that directory on startup. A minimal mapping:
{
"request": {
"method": "GET",
"urlPattern": "/api/users/[0-9]+"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "user.json"
}
}
Place the response body at wiremock/__files/user.json. The __files directory is WireMock’s static file root; bodyFileName paths resolve relative to it.
Validate all mapping files before starting the stack to catch malformed JSON early:
find wiremock/mappings -name "*.json" -exec jq empty {} \; && echo "All mappings valid"
Step 5 — Configure your frontend dev server proxy
The network layer abstraction that WireMock provides is only useful if your frontend routes to it cleanly. Configure your dev server to proxy API calls to the container:
Vite (vite.config.ts):
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
}
Next.js (next.config.js):
/** @type {import('next').NextConfig} */
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:8080/api/:path*',
},
]
},
}
Webpack Dev Server (webpack.config.js):
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
secure: false,
},
},
},
}
Volume and network flow
The diagram below shows how host directories, the WireMock container, and frontend services interact at runtime.
Verification
Run these two commands after docker compose up -d. Both must succeed before you trust the stack:
# 1. Health endpoint responds 200
docker compose exec wiremock curl -sf http://localhost:8080/__admin/health
# 2. A stub mapping is loaded and returns the expected status
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/users/42
For the first command, expected output is a JSON object containing "status":"Running". For the second, the HTTP status code must match the status field in your mapping JSON.
To inspect all registered stubs and unmatched requests:
# All loaded stubs
curl -s http://localhost:8080/__admin/mappings | jq '.mappings | length'
# Unmatched requests (useful when a stub silently fails to match)
curl -s http://localhost:8080/__admin/requests/unmatched | jq '.requests[].request.url'
Gotchas and edge cases
-
Volume created as root before
mkdir -p. If Docker creates the mount directory before you do (because you randocker compose upwithout the setup step), the directory is owned by root and thewiremockuser inside the container cannot write to it. Fix:sudo chown -R ${UID}:${GID} wiremock/, then remove the container and restart. -
--root-dirand volume target must match exactly. The CLI flag--root-dir=/home/wiremocktells WireMock where to look formappings/and__files/subdirectories. If you change the volume target (e.g. to/opt/wiremock) without updating the flag, WireMock starts cleanly but loads zero stubs and returns404for every request. The request interception patterns layer never engages because no rules are loaded. -
--global-response-templatingchanges stub evaluation order. With this flag, every response body is processed as a Handlebars template even if it contains no template syntax. A response body containing{{for any other reason (e.g. a JSON body that accidentally includes double-braces) will cause a template-render error. Escape literal{{as{{{'{'}}{{'{'}}}}or disable global templating and opt in per-stub with"transformers": ["response-template"]instead.
FAQ
Why does the healthcheck pass but my stub still returns 404?
The health endpoint (/__admin/health) confirms the JVM is running, not that your mappings loaded correctly. Check /__admin/mappings to verify the stub count is non-zero. If it is zero, the most common cause is a mismatch between --root-dir and the volume mount path (see gotcha above) or a malformed JSON file that WireMock silently skipped.
Can I hot-reload stubs without restarting the container?
Yes. POST to /__admin/mappings/reset to clear all in-memory stubs and reload from disk, or POST a new mapping JSON to /__admin/mappings to add it at runtime. Changes to files in wiremock/mappings/ are not watched automatically — you must trigger the reset endpoint. This aligns with how mock lifecycle management handles state across restarts: disk is the source of truth, memory is the runtime projection.
How do I run WireMock on a dynamic port in CI to avoid conflicts?
Set WIREMOCK_PORT in your CI environment (e.g. WIREMOCK_PORT=9090) and reference it in the compose file as "${WIREMOCK_PORT:-8080}:8080". The container always binds internally on 8080; only the host port changes. Pass the same value to your test runner via environment variable so tests know where to connect.
Related
- Dockerized Mock Environments — parent cluster covering the full Docker-based mock stack
- Managing Mock Server Lifecycles in Docker — startup sequencing, graceful shutdown, and state teardown patterns
- WireMock Standalone Configuration — standalone binary setup and scenario state machine configuration
- Proxy vs Inline Mocking Strategies — when a containerised proxy mock like WireMock is the right choice over an in-process approach
← Back to Dockerized Mock Environments