Docker

June 24, 2026 · View on GitHub

Image Variants

MockServer provides multiple Docker image variants for different use cases:

graph TB
    subgraph "Production Images"
        MAIN["docker/Dockerfile
Main (nonroot)
gcr.io/distroless/java17:nonroot"]
        ROOT["docker/root/Dockerfile
Root
gcr.io/distroless/java17"]
        GRAAL["docker/graaljs/Dockerfile
GraalJS
gcr.io/distroless/java17:nonroot"]
        SNAP["docker/snapshot/Dockerfile
Snapshot (debug)
gcr.io/distroless/java17:debug-nonroot"]
        RSNAP["docker/root-snapshot/Dockerfile
Root Snapshot
gcr.io/distroless/java17"]
        LOCAL["docker/local/Dockerfile
Local Build
gcr.io/distroless/java17:nonroot"]
        WEBHOOK["docker/webhook/Dockerfile
Admission Webhook
gcr.io/distroless/java17:nonroot"]
        CLUSTERED["docker/clustered/Dockerfile
Clustered (Infinispan)
gcr.io/distroless/java17:nonroot"]
    end

    subgraph "Build Images"
        MVN["docker_build/maven/Dockerfile
Maven CI
Ubuntu 24.04 + JDK 17 + Maven 3.9"]
        PERF["docker_build/performance/Dockerfile
Performance
grafana/k6"]
    end

Production Images

VariantDockerfileBase ImageUserPurpose
Maindocker/Dockerfilegcr.io/distroless/java17:nonrootnonrootDefault production image
Rootdocker/root/Dockerfilegcr.io/distroless/java17rootWhen root access is needed
GraalJSdocker/graaljs/Dockerfilegcr.io/distroless/java17:nonrootnonrootIncludes GraalJS for JS templating
Snapshotdocker/snapshot/Dockerfilegcr.io/distroless/java17:debug-nonrootnonrootTesting pre-release builds
Root Snapshotdocker/root-snapshot/Dockerfilegcr.io/distroless/java17rootTesting pre-release (root)
Localdocker/local/Dockerfilegcr.io/distroless/java17:nonrootnonrootBuilding from local JAR
Webhookdocker/webhook/Dockerfilegcr.io/distroless/java17:nonrootnonrootKubernetes admission webhook for sidecar injection
Clustereddocker/clustered/Dockerfilegcr.io/distroless/java17:nonrootnonrootInfinispan state backend for multi-node clustering

Docker Registries

Images are published to two registries:

RegistryImageNotes
Docker Hubmockserver/mockserverPrimary registry (main MockServer image)
Docker Hubmockserver/mockserver-webhookAdmission webhook image
AWS ECR Publicpublic.ecr.aws/mockserver/mockserverAvoids Docker Hub rate limits for AWS-based CI/CD
AWS ECR Publicpublic.ecr.aws/mockserver/mockserver-webhookWebhook image on ECR

Both registries receive the same tags on every push. On each merge to master, the legacy Buildkite pipeline (.buildkite/scripts/steps/java-docker-push-snapshot.sh) pushes the :snapshot, :mockserver-snapshot, and -graaljs snapshot variants (plus :snapshot / :mockserver-snapshot for the webhook image). During releases, the release pipeline (scripts/release/components/docker.sh) pushes :latest, :X.Y.Z, :mockserver-X.Y.Z, -graaljs, clustered-*, and webhook release variants. The :latest tag is pushed only by the release pipeline, not by the per-merge snapshot step. The :latest tag always points to the most recent official release, not the development branch.

Release images are cosign-signed by digest after push (see below). Snapshot images are not signed.

The -clustered image variant (clustered-X.Y.Z, clustered-mockserver-X.Y.Z, clustered-latest) is published alongside the base and GraalJS images at release time. It bundles the mockserver-state-infinispan module and its transitive dependencies (Infinispan, JGroups, etc.) plus netty-tcnative-boringssl-static for native TLS. The build is error-isolated: a clustered image push failure does not abort the release since the main images have already been published.

Verifying Image Signatures

Release images are cosign-signed by digest after push using the project's signing key (stored in AWS Secrets Manager mockserver-release/cosign-key). Signing uses the same key infrastructure as the Helm chart signing in scripts/release/components/helm.sh. The release Docker step runs on the release queue (the only queue granted read_release_secrets, which includes the cosign key) and auto-installs the pinned cosign binary into .tmp/ if it is not already on the agent.

To verify a release image:

# Install cosign: https://docs.sigstore.dev/cosign/system_config/installation/

# Verify by digest (most reliable — binds to exact manifest content)
cosign verify \
  --key https://www.mock-server.com/mockserver-cosign.pub \
  mockserver/mockserver@sha256:<digest>

# Or verify the tag (resolves to digest internally)
cosign verify \
  --key https://www.mock-server.com/mockserver-cosign.pub \
  mockserver/mockserver:7.2.0

The public key corresponding to mockserver-release/cosign-key is published at https://www.mock-server.com/mockserver-cosign.pub (source: jekyll-www.mock-server.com/mockserver-cosign.pub; an identical copy is at helm/mockserver/cosign.pub). It can also be re-derived from the private key with cosign public-key --key cosign.key. The same key signs the Helm chart.

Signing is non-fatal in the release pipeline: if the key is absent (or the cosign binary cannot be downloaded), images are published unsigned and the release continues. The cosign binary itself is no longer a prerequisite — the release step downloads and checksum-verifies it on demand.

IAM note: the signing step is gated by aws secretsmanager describe-secret mockserver-release/cosign-key, so the release-queue role needs secretsmanager:DescribeSecret on that secret in addition to GetSecretValue — otherwise the probe fails and signing is silently skipped (this caused the 7.2.0 chart/images to publish unsigned until the grant was added to read_release_secrets).

Base Image CVE Baseline

Image scanners (Trivy, Grype, the ArtifactHub Helm security report) will always show a residual set of CVEs against the distroless base image, not against MockServer code or its Maven dependencies. This is expected and is not a release blocker.

Why these appear: every production image runs on gcr.io/distroless/java17 (digest-pinned). That base ships the JRE plus the minimal set of Debian OS libraries the JRE links against — libc6, libexpat1 (XML), zlib1g, libuuid1, libpng16 / liblcms2-2 (AWT imaging), libbz2-1.0. Scanners report any open Debian advisory against those packages. They are part of the base layer; MockServer neither installs nor controls them.

Why a Java/JRE version bump does not clear them: the CVEs are against the OS packages in the Debian layer, independent of the JRE major version. Changing the build toolchain JDK (e.g. building the release on JDK 17 rather than JDK 11) does not alter the runtime base image's OS package set at all.

Why they often cannot be remediated at build time: most carry Fixed in: - (Fixable: 0) in the report, meaning Debian has not yet published a patched package. When no upstream fix exists, there is nothing to pull in — re-pinning to the newest distroless digest removes nothing. Such advisories clear only once Debian ships patched packages and the distroless base is rebuilt and we adopt the new digest.

How the digest stays current: digest re-pinning is automated — Dependabot's docker ecosystem (see .github/dependabot.yml) opens a bump PR whenever the upstream digest of a pinned base image moves. So when distroless rebuilds with fixed OS packages, the update arrives as a routine dependency PR; no manual re-pin or release-time step is required. Only the Dockerfile directories registered in dependabot.yml are auto-bumped — when adding a new Dockerfile directory, register it there too or its base image will not be tracked.

What to check when triaging a base-image CVE report:

  1. Confirm the flagged package is an OS library from the distroless base (libc6, libexpat1, zlib1g, libuuid1, libpng16, liblcms2-2, libbz2-1.0, …) rather than a bundled Maven artifact — only the latter is actionable in our build.
  2. Check the Fixed in column. - means no upstream fix exists yet → expected baseline, no action. A concrete version means distroless has likely already rebuilt → ensure the Dependabot digest-bump PR has merged (or merge it).
  3. Assess reachability — these libraries are largely inert for MockServer's HTTP/proxy hot paths (e.g. no untrusted-XML-through-expat path), which is why an unpatched base CVE is rarely a practical risk.

Docker HEALTHCHECK

All production MockServer server Dockerfiles include a built-in HEALTHCHECK instruction that runs a lightweight Java class (org.mockserver.cli.HealthCheck) to verify MockServer is serving requests. The health check calls PUT /mockserver/status internally — no shell, curl, or external tools required. The one exception is the admission-webhook image (docker/webhook/Dockerfile), which deliberately has no HEALTHCHECK — it is a short-lived Kubernetes sidecar-injection webhook rather than a long-running server, and its liveness/readiness is governed by Kubernetes probes against the webhook endpoint.

HEALTHCHECK --interval=10s --timeout=5s --start-period=120s --retries=3 \
  CMD ["java", "-cp", "/mockserver-netty-jar-with-dependencies.jar", "org.mockserver.cli.HealthCheck"]

The health check reads SERVER_PORT / MOCKSERVER_SERVER_PORT to determine the correct port (defaults to 1080).

Main Dockerfile Build Process

flowchart TD
    subgraph "Build Stage (selectable)"
        DL["'download' stage
Downloads JAR from Sonatype"]
        CP["'copy' stage
Uses local JAR"]
    end

    DL -->|default| INT[Intermediate Stage]
    CP -->|ARG source=copy| INT

    INT --> RT["Runtime Stage
distroless/java17:nonroot"]

    RT --> EXPOSE["EXPOSE 1080"]
    RT --> ENTRY["ENTRYPOINT java -cp mockserver-netty-jar-with-dependencies.jar
org.mockserver.cli.Main"]

The main Dockerfile supports two source modes via the source build ARG:

  • download (default): Downloads mockserver-netty-jar-with-dependencies.jar from Sonatype and netty-tcnative-boringssl-static from Maven Central
  • copy: Copies a locally-built JAR from the Docker context; downloads netty-tcnative-boringssl-static from Maven Central

Both modes download netty-tcnative-boringssl-static from Maven Central (repo1.maven.org) for TLS performance.

Exposed port: 1080

MCP endpoint: When mcpEnabled=true (via system property or mockserver.properties), the MCP (Model Context Protocol) endpoint is available at /mockserver/mcp on the same port. AI agents can connect using HTTP+SSE transport.

Entry point: java -Dfile.encoding=UTF-8 -XX:MaxRAMPercentage=75.0 -cp /mockserver-netty-jar-with-dependencies.jar:/libs/* -Dmockserver.propertyFile=/config/mockserver.properties org.mockserver.cli.Main

Heap cap: -XX:MaxRAMPercentage=75.0 limits the JVM heap to 75% of the container's memory limit so the in-memory request/expectation ring buffers size off a bounded heap rather than total node memory. The Helm chart delivers any app.jvmOptions value via the JAVA_TOOL_OPTIONS environment variable; the JVM prepends JAVA_TOOL_OPTIONS flags before the command-line args, so the ENTRYPOINT's -XX:MaxRAMPercentage=75.0 is evaluated last and wins over any competing MaxRAMPercentage in jvmOptions. An explicit -Xmx in jvmOptions (or JAVA_TOOL_OPTIONS) does disable MaxRAMPercentage — once -Xmx is present the flag is ignored. Both docker/Dockerfile and docker/clustered/Dockerfile include this flag.

Building Behind a Corporate TLS-Inspecting Proxy

Outcome: to build the image variants locally behind a corporate TLS proxy, point MOCKSERVER_LOCAL_CA_BUNDLE at your corporate root CA before building — CI and published images are byte-identical because the mechanism is a no-op when the variable is unset.

export MOCKSERVER_LOCAL_CA_BUNDLE=/path/to/corporate-root-ca.pem
# then build any variant as usual, e.g.:
docker build docker/            # base image (downloads from Maven Central via the proxy)

How it works: each variant's alpine download stage (docker/, docker/root/, docker/snapshot/, docker/root-snapshot/, docker/clustered/, docker/graaljs/) COPYs a ca-bundle.pem from the build context. When that file is non-empty, the stage trusts it before apk add and before the wget jar downloads from repo1.maven.org, so TLS interception does not break the build. When it is empty (the CI/published-image case), an [ -s ] guard skips all trust changes, so the build is identical to a no-CA build.

The release/CI scripts and the container-integration-test harness stage this file automatically via the shared docker/ensure-ca-bundle.sh helper: it copies MOCKSERVER_LOCAL_CA_BUNDLE (or the NODE_EXTRA_CA_CERTS / AWS_CA_BUNDLE fallbacks) into the context when set, otherwise writes an empty placeholder. All ca-bundle.pem files are gitignored. The single-stage docker/local and docker/webhook images do not download anything and so do not use this mechanism.

The same download stages also harden Maven Central downloads against transient DNS/connection blips by appending GNU-wget retry directives (tries, timeout, waitretry, retry_on_host_error, retry_connrefused) to /etc/wgetrc. BusyBox wget ignores /etc/wgetrc, so this is a safe no-op on images that fall back to it.

Build Images

ImageDockerfileBasePurpose
mockserver/mockserver:mavendocker_build/maven/DockerfileUbuntu 24.04CI builds — JDK 17, Maven 3.9.16
mockserver/mockserver:performancedocker_build/performance/Dockerfilegrafana/k6Load testing with k6

Docker Compose Examples

Three reference configurations demonstrate different MockServer setup approaches:

By Volume Mount

docker/docker-compose/configure_by_volume_mount/

Mounts a mockserver.properties file and initializerJson.json into the container.

By Command Arguments

docker/docker-compose/configure_by_command/

Passes configuration via command-line arguments to the MockServer CLI.

By Environment Properties

docker/docker-compose/configure_by_environment_properties/

Uses environment variables (MOCKSERVER_*) for configuration.

Multi-Architecture Build

Production images are built for both linux/amd64 and linux/arm64 using Buildkite with QEMU emulation on x86_64 agents:

# Built and pushed by the release pipeline's Docker step
# (scripts/release/components/docker.sh, via release-runner.sh docker)

See CI/CD for full pipeline details.

Local Docker Operations

# Build from local JAR
docker/local/local_docker_build.sh

# Run locally built image
docker/local/local_docker_run.sh

# Run with cAdvisor monitoring
docker/local/local_docker_cadvisor_run.sh

# Launch interactive Maven container
scripts/local_docker_launch.sh

Container Integration Tests

The container_integration_tests/ directory contains 24 automated tests (16 Docker Compose + 8 Helm), plus non-blocking smoke tests for image variants:

graph TD
    TESTS[integration_tests.sh]

    subgraph "Docker Compose Tests (16)"
        DC1[Without server port]
        DC2[Default properties file]
        DC3[Custom properties file]
        DC4[Server port by command]
        DC5[Env var long name]
        DC6[Env var short name]
        DC7[Remote host/port]
        DC8[Persisted expectations]
        DC9[Expectation initialiser]
        DC10[Forward with override]
        DC11[mTLS]
        DC12[JVM options]
        DC13[Libs classpath]
        DC14[Graceful shutdown]
        DC15[Metrics]
        DC16[WAR Tomcat]
    end

    subgraph "Helm Tests (8)"
        H1[Default Helm values]
        H2[Helm with local Docker image]
        H3[Helm with custom port]
        H4[Helm with remote host/port]
        H5[Helm with inline config]
        H6[Helm ConfigMap injection]
        H7[Helm MockServer config chart]
        H8[Clustered state convergence]
    end

    TESTS --> DC1
    TESTS --> DC2
    TESTS --> DC3
    TESTS --> DC4
    TESTS --> DC5
    TESTS --> DC6
    TESTS --> DC7
    TESTS --> DC8
    TESTS --> DC9
    TESTS --> DC10
    TESTS --> DC11
    TESTS --> DC12
    TESTS --> DC13
    TESTS --> DC14
    TESTS --> DC15
    TESTS --> DC16
    TESTS --> H1
    TESTS --> H2
    TESTS --> H3
    TESTS --> H4
    TESTS --> H5
    TESTS --> H6
    TESTS --> H7
    TESTS --> H8

Each test:

  1. Starts MockServer (via Docker Compose or Helm/k3d)
  2. Creates expectations via the REST API
  3. Validates responses using a curl-based client container
  4. Tears down the environment

Helper Scripts

ScriptPurpose
integration_tests.shMain orchestrator: builds images, runs all tests, prints summary
docker-compose.shDocker Compose helpers: start-up, tear-down, docker-exec, container-logs
helm-deploy.shk3d cluster lifecycle: start-up-k8s, tear-down-k8s, Helm install/uninstall
logging.shColoured terminal output, runCommand, retryCommand, logTestResult

Environment Variable Controls

VariablePurpose
SKIP_JAVA_BUILDSkip mvnw package step
SKIP_DOCKER_BUILD_MOCKSERVERSkip building MockServer Docker image
SKIP_DOCKER_REBUILD_CLIENTSkip rebuilding the curl client image
SKIP_ALL_TESTSSkip all tests (build only)
SKIP_DOCKER_TESTSSkip Docker Compose tests
SKIP_HELM_TESTSSkip Helm/k3d tests

See Testing for full details on running container integration tests.

Maven CI Image

Building Locally

The Maven CI image supports an optional corporate CA certificate for environments behind a TLS inspection proxy:

# Copy your corporate root CA certificate (optional, for TLS proxy environments)
cp /path/to/your/corporate-root-ca.pem docker_build/maven/corporate-root-ca.pem

# Build the image (native architecture)
docker build -t mockserver/mockserver:maven docker_build/maven/

Without a corporate CA cert, create an empty corporate-root-ca.pem file (or copy the .pem.example placeholder). The Dockerfile detects the empty file and skips certificate injection.

Cross-Architecture Build (amd64 on Apple Silicon)

Buildkite agents run on amd64 EC2 instances. When building on Apple Silicon, cross-compile to amd64 before pushing:

docker buildx build \
    --builder desktop-linux \
    --platform linux/amd64 \
    --load \
    -t mockserver/mockserver:maven \
    docker_build/maven/

Important: Use the desktop-linux buildx builder, not docker-container builders (e.g. multiplatform). The docker-container driver runs in its own container and does not inherit the host's TLS certificate trust store, causing x509: certificate signed by unknown authority errors behind corporate TLS proxies.

Verify the architecture before pushing:

docker inspect mockserver/mockserver:maven --format '{{.Architecture}}'
# Should print: amd64

Corporate CA Certificate

The Dockerfile supports injecting a corporate root CA certificate at build time:

  • Placeholder: docker_build/maven/corporate-root-ca.pem.example (empty, committed to git)
  • Real cert: docker_build/maven/corporate-root-ca.pem (gitignored, local only)
  • If the cert file has content, it is added to the OS trust store (update-ca-certificates) and the Java truststore (keytool)
  • In CI (Buildkite), the empty placeholder is used — no corporate CA is needed

Automated Build

The Maven CI image is built and pushed to Docker Hub by the Buildkite pipeline .buildkite/docker-push-maven.yml:

  • Trigger: Manual (via Buildkite UI or API)
  • Auth: Docker Hub credentials from AWS Secrets Manager (mockserver-build/dockerhub)
  • Tag: mockserver/mockserver:maven

See CI/CD for details.