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.

Dockerized mock environment architecture Traffic flow from the application under test through a Docker bridge network to the WireMock container, with an optional record path to the real upstream API. DOCKER COMPOSE STACK App Under Test localhost:3000 Docker bridge mock-net WireMock wiremock:8080 stub response Real upstream API (record mode only) proxy-all Named volume ./mappings + __files

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 --wait exits with code 0 and all services show healthy in docker compose ps
  • curl -s http://localhost:8080/__admin/health | jq .status returns "healthy"
  • curl -s http://localhost:8080/api/users | jq .users[0].name returns "Alice Nguyen"
  • docker compose logs wiremock shows 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/requests returns {"requests":[]}
  • docker compose down --volumes && docker compose up -d --wait produces 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/mappings to confirm files are present
  • The url field in the stub uses /api/users but the request hits /api/users?page=1 — switch to urlPathPattern: "/api/users" and add an ignoreExtraElements body 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 than localhost, 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 test from 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.


← Back to Tool-Specific Implementation & Setup