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:

  1. 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.
  2. UID/GID mismatch. The official wiremock/wiremock image 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.
  3. Startup ordering. Services that depend on WireMock start before /__admin/health responds, causing connection-refused errors at test time. A proper healthcheck + depends_on condition 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. Export UID and GID in your shell or .env file.
  • --root-dir=/home/wiremock — aligns the CLI flag with the volume mount target. If these diverge, WireMock loads no mappings and returns 404 for 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.

Docker Compose WireMock Architecture Diagram showing host-side directories (wiremock/mappings, wiremock/__files, wiremock/logs) bind-mounted into the WireMock container at /home/wiremock, and a frontend service communicating with WireMock over the Compose internal network on port 8080, while the developer accesses the container via localhost:8080 on the host. HOST wiremock/mappings/ wiremock/__files/ wiremock/logs/ localhost:8080 (dev access) COMPOSE NETWORK WireMock wiremock/wiremock:3.13.2 /home/wiremock/mappings /home/wiremock/__files /home/wiremock/logs port 8080 (HTTP) port 8443 (HTTPS) /__admin/health Frontend Vite / Next.js / Webpack depends_on: service_healthy VITE_API_BASE= bind mount HTTP :8080

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 ran docker compose up without the setup step), the directory is owned by root and the wiremock user inside the container cannot write to it. Fix: sudo chown -R ${UID}:${GID} wiremock/, then remove the container and restart.

  • --root-dir and volume target must match exactly. The CLI flag --root-dir=/home/wiremock tells WireMock where to look for mappings/ 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 returns 404 for every request. The request interception patterns layer never engages because no rules are loaded.

  • --global-response-templating changes 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.


← Back to Dockerized Mock Environments