Docker Deployment¶
Deploy Rift using Docker for quick setup and consistent environments.
Quick Start¶
# Pull the image from Docker Hub
docker pull zainalpour/rift-proxy:latest
# Run with default settings
docker run -p 2525:2525 zainalpour/rift-proxy:latest
Image Flavors¶
Every release publishes two flavors of the same rift binary. They are interchangeable — same admin API, same imposter behaviour, same ports, same environment variables.
| Tag | Base | Use it when |
|---|---|---|
latest, vX.Y.Z (e.g. v0.17.0) |
debian:bookworm-slim |
The default. |
latest-static, vX.Y.Z-static |
scratch |
You want the smallest CVE surface — typically ephemeral CI/test environments. |
Both are multi-arch (linux/amd64, linux/arm64). Version tags keep the leading v of the release
tag. Both run as the unprivileged user rift (uid 1000) with /data as the working directory, and
set MB_PORT=2525, MB_LOGLEVEL=info and RIFT_METRICS_PORT=9090. They leave MB_HOST unset, so
the admin API and /metrics listen on every interface, IPv6 and IPv4 ([::] dual-stack). The
entrypoint is rift, so arguments after the image name are rift flags or a subcommand.
The images declare EXPOSE 2525 9090 4545-4550 8080-8090 as documentation only — publish the ports
your imposters actually use with -p.
The linter ships as its own image, zainalpour/rift-lint (latest, vX.Y.Z); its entrypoint is
rift-lint and its working directory is /imposters.
docker pull zainalpour/rift-proxy:latest-static
docker run -p 2525:2525 zainalpour/rift-proxy:latest-static
The -static flavor is a statically-linked musl build on FROM scratch. It contains the rift
binary, a CA certificate bundle, and a passwd entry — nothing else. No package manager ever runs in
it, so an image scanner finds no OS packages to report: there is no base distro to keep patching
just because rift is running in your test suite.
Two consequences worth knowing before you switch:
-
No shell. There is no
/bin/sh, sodocker exec ... sh, string-formcommand:overrides, and shell-form healthchecks do not work. Use exec form (["rift", "healthcheck"]) and pass flags directly. The health probe is built into the binary precisely for this reason — seerift healthcheck.The images' built-in
HEALTHCHECKrunsrift healthcheck(every 30s, 3s timeout, 3 retries, 5s start period), which probes/healthon the host and port the container's ownMB_HOST/MB_PORTsay. Moving the port with-e MB_PORT=…is therefore followed automatically. Moving it with a--portargument or an--rcfileis not — the probe never sees the server's command line — so override the healthcheck to tell the probe the same thing, or it will report unhealthy forever (issue #1133): -
No mimalloc. The musl binaries are built without the mimalloc allocator. Scripting is present. If you are benchmarking allocation-heavy workloads, use the default flavor on
amd64(thearm64default image is built without mimalloc too).
HTTPS upstream proxying works in both: the CA bundle is copied into the static image, because rift's TLS client loads the OS trust store at runtime. For a private CA, see Reaching an Origin Behind a Private CA.
Health checks and --api-key¶
Set the key with MB_APIKEY, and the built-in HEALTHCHECK needs no change (issue #1154): the
probe is a separate process, but it runs with the container's environment, so it reads MB_APIKEY
and presents it to the admin API's /health. It sends the key only to the address derived from
MB_HOST/MB_PORT, never to an explicit --url.
The other two spellings do not reach it on their own, for the reason above — the probe never sees the server's command line:
--api-keyas adocker runargument is invisible to the probe, so the container reports unhealthy. Switch to-e MB_APIKEY=…. Do not fix it by adding--api-keyto the healthchecktest:line — that puts the key indocker inspect.- An rcfile
apiKeyreaches the probe only if the healthcheck is given the same file, as in the--rcfileoverride above:["CMD", "rift", "--rcfile", "/etc/rift/rc.json", "healthcheck"].
Stopping the container¶
docker stop is prompt: rift handles SIGTERM itself, including as the container's PID 1, so no
init process is needed (issue #1155). It stops accepting, gives in-flight admin connections a
bounded grace of about three seconds at most, closes imposter connections, leaves any --datadir
state in place, and exits 0. Before this, rift had no signal handler — the kernel discards an
unhandled SIGTERM to PID 1 — so docker stop waited out its full timeout and killed it, exit
137; running under an init process was the workaround and is no longer needed.
Verifying an Image¶
Published images carry an SBOM and max-mode provenance, and are signed with cosign keyless — the signing identity is the release workflow itself, so there is no public key to distribute.
# Verify the signature and its provenance
cosign verify \
--certificate-identity-regexp 'https://github.com/achird-labs/rift/.github/workflows/.+' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
zainalpour/rift-proxy:latest-static
# Inspect the SBOM / provenance attestations
docker buildx imagetools inspect zainalpour/rift-proxy:latest-static \
--format '{{ json .SBOM }}'
docker buildx imagetools inspect zainalpour/rift-proxy:latest-static \
--format '{{ json .Provenance }}'
Basic Configuration¶
With Environment Variables¶
docker run -d \
--name rift \
-p 2525:2525 \
-p 9090:9090 \
-e MB_PORT=2525 \
-e MB_ALLOW_INJECTION=true \
-e MB_LOGLEVEL=info \
zainalpour/rift-proxy:latest
With Configuration File¶
docker run -d \
--name rift \
-p 2525:2525 \
-p 4545:4545 \
-v $(pwd)/imposters.json:/imposters.json:ro \
zainalpour/rift-proxy:latest \
--configfile /imposters.json
Docker Compose¶
Basic Setup¶
# docker-compose.yml
services:
rift:
image: zainalpour/rift-proxy:latest
container_name: rift
ports:
- "2525:2525" # Admin API
- "4545:4545" # Imposter port
- "9090:9090" # Metrics
environment:
- MB_PORT=2525
- MB_ALLOW_INJECTION=true
- MB_LOGLEVEL=info
volumes:
- ./imposters.json:/imposters.json:ro
command: ["--configfile", "/imposters.json"]
healthcheck:
test: ["CMD", "rift", "healthcheck"]
interval: 10s
timeout: 5s
retries: 3
With Multiple Ports¶
services:
rift:
image: zainalpour/rift-proxy:latest
ports:
- "2525:2525" # Admin
- "4545:4545" # User Service
- "4546:4546" # Order Service
- "4547:4547" # Payment Service
- "9090:9090" # Metrics
volumes:
- ./imposters.json:/imposters.json:ro
command: ["--configfile", "/imposters.json"]
With TLS¶
Mount the certificate and key, and name them as the default for HTTPS imposters that carry no
cert/key of their own:
services:
rift:
image: zainalpour/rift-proxy:latest
ports:
- "2525:2525"
- "4545:4545"
volumes:
- ./imposters.json:/imposters.json:ro
- ./certs:/certs:ro
command:
- "--configfile"
- "/imposters.json"
- "--default-tls-cert"
- "/certs/server.pem"
- "--default-tls-key"
- "/certs/server-key.pem"
See TLS/HTTPS for per-imposter certificates, mutual TLS and the self-signed fallback.
Intercepting HTTPS from another container¶
To mock an external HTTPS host your application hard-codes, run the
intercept listener beside it and send the app's
HTTPS through it. Declare the listener and its rules in the config file, so it is correct the moment
rift is healthy:
{
"imposters": [
{ "port": 4545, "protocol": "http",
"stubs": [{ "responses": [{ "is": { "statusCode": 200, "body": "{\"featureX\":\"ON\"}" } }] }] }
],
"intercept": {
"host": "0.0.0.0",
"port": 8080,
"caCertPath": "/ca/ca-cert.pem",
"caKeyPath": "/ca/ca-key.pem",
"rules": [
{ "host": "cdn.example.com", "action": { "forward": { "port": 4545 } } }
]
}
}
The CA for a config-file listener comes from the block: caCertPath / caKeyPath (files, as here),
caCertPem / caKeyPem (inline), or caCertPemEnv / caKeyPemEnv (the names of two environment
variables holding the PEMs — e.g. set by CI or an orchestrator that injects secrets as variables;
Docker/Compose secrets are mounted as files under /run/secrets, so use the path pair for those). The RIFT_INTERCEPT_* variables and --intercept-* flags
belong to the other way of starting a listener -- --intercept-port with no block, rules added over
the admin API afterwards. Setting any of them alongside a block is a startup error, so do not put
RIFT_INTERCEPT_CA_CERT_PEM (or any RIFT_INTERCEPT_*) in the environment of a container that loads
a block. Make the CA once with rift intercept-ca generate --out-dir ./ca (see
intercept-ca) and mount it:
services:
rift:
image: zainalpour/rift-proxy:latest
volumes:
- ./mock.json:/config/mock.json:ro
- ./ca:/ca:ro # ca-cert.pem + ca-key.pem from `intercept-ca generate`
command: ["--configfile", "/config/mock.json"]
# No `ports:` for 8080 -- only the app, on the compose network, needs the listener.
healthcheck:
test: ["CMD", "rift", "healthcheck"]
interval: 5s
timeout: 3s
retries: 10
app:
build: .
depends_on:
rift:
condition: service_healthy
environment:
HTTPS_PROXY: http://rift:8080
NO_PROXY: localhost,127.0.0.1
SSL_CERT_FILE: /certs/ca.pem # curl, Go, OpenSSL clients
REQUESTS_CA_BUNDLE: /certs/ca.pem # Python requests
NODE_EXTRA_CA_CERTS: /certs/ca.pem # Node
volumes:
- ./ca/ca-cert.pem:/certs/ca.pem:ro
A JVM app reads its truststore once at startup, so give it a JKS built ahead of time:
rift intercept-ca export --cert ./ca/ca-cert.pem --format jks --out truststore.jks --merge-system-cas /etc/ssl/certs/ca-certificates.crt
(password from RIFT_TRUSTSTORE_PASSWORD), then -Djavax.net.ssl.trustStore=…. Which variable adds
to the default roots and which replaces them is spelled out under
Trusting the CA from the SUT;
it is not repeated here.
Three things that fail quietly in containers:
- The bind host.
POST /interceptdefaults to127.0.0.1, which another container cannot reach. Name"host": "0.0.0.0"in the config block (as above); a bare--intercept-portbinds on the admin server's address (every interface when--hostis unset), and warns unless--intercept-authis set. Keep the listener off published ports unless something outside the network needs it. - Key file permissions. The image runs as uid 1000, and
intercept-ca generatewrites the key0600as you, so on a Linux bind mount rift cannot read it and fails at startup. For a CI or demo CA only,chmod 644 ca/ca-key.pem(or run the container with auser:/ group that matches the file's owner); never loosen a real key. See the note ingenerate-certs.shfor the trap. forwardreaches the rift container. A rule'sforwarddefaults to127.0.0.1:{port}inside the rift container. To reach an imposter elsewhere, name it:{"forward": {"host": "mock-svc", "port": 4600, "scheme": "https"}}-- see Forward to one of your imposters.
Changing a rule needs only an edited file and POST /admin/reload; the listener itself (host, port,
CA) is fixed at boot. The CI-verified, runnable version of this setup is the
HTTPS intercept demo.
Integration Testing Setup¶
Rift with Your Application¶
services:
# Your application
app:
build: .
environment:
- USER_SERVICE_URL=http://rift:4545
- ORDER_SERVICE_URL=http://rift:4546
depends_on:
rift:
condition: service_healthy
# Mock server
rift:
image: zainalpour/rift-proxy:latest
ports:
- "2525:2525"
volumes:
- ./test/mocks:/mocks:ro
command: ["--configfile", "/mocks/imposters.json"]
healthcheck:
test: ["CMD", "rift", "healthcheck"]
interval: 5s
timeout: 3s
retries: 10
Test Runner Integration¶
services:
rift:
image: zainalpour/rift-proxy:latest
ports:
- "2525:2525"
- "4545:4545"
healthcheck:
test: ["CMD", "rift", "healthcheck"]
interval: 5s
timeout: 3s
retries: 10
tests:
build:
context: .
dockerfile: Dockerfile.test
environment:
- MOCK_SERVER_URL=http://rift:4545
- MOCK_ADMIN_URL=http://rift:2525
depends_on:
rift:
condition: service_healthy
command: ["npm", "test"]
Production Configuration¶
Resource Limits¶
services:
rift:
image: zainalpour/rift-proxy:latest
deploy:
resources:
limits:
cpus: '2'
memory: 512M
reservations:
cpus: '0.5'
memory: 128M
Logging¶
services:
rift:
image: zainalpour/rift-proxy:latest
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
environment:
- MB_LOGLEVEL=warn
Restart Policy¶
Building Custom Image¶
Dockerfile¶
FROM zainalpour/rift-proxy:latest
# Copy configuration
COPY imposters.json /config/imposters.json
# Set environment
ENV MB_PORT=2525
ENV MB_ALLOW_INJECTION=true
# Run with config
CMD ["--configfile", "/config/imposters.json"]
Build and Run¶
Feature flags for a custom build¶
The published images are assembled from the release binaries. To build an image from source
instead, use crates/rift-http-proxy/Dockerfile from the repository root
(docker build -f crates/rift-http-proxy/Dockerfile .); its runtime stage is debian:trixie-slim,
and it builds with ARG FEATURES=javascript by default (on top of the crate's default
features). If you're building a slimmer image (or embedding Rift as a cdylib instead of running the
container), see the Cargo feature table in FFI (C-ABI)
and the Embedding & SPI overview — the same features gate both the
binary and the rift-ffi cdylib, and cargo build --no-default-features (plus an explicit
--features list) drops the scripting engines you don't need.
Common Operations¶
View Logs¶
Drive the Admin API¶
The images ship the rift binary and nothing else — no curl, and in the -static flavor no shell
either — so run these from the host against the published admin port rather than via docker exec.
# Create imposter
curl -X POST http://localhost:2525/imposters \
-H "Content-Type: application/json" \
-d '{"port": 4545, "protocol": "http", "stubs": []}'
# List imposters
curl http://localhost:2525/imposters
The one thing worth running inside the container is the health probe, which is built in:
Restart¶
Clean Up¶
Troubleshooting¶
Container Won't Start¶
# Check logs
docker logs rift
# Verify config (rift-lint ships as its own image; the server image does not include it)
docker run --rm -v $(pwd):/imposters zainalpour/rift-lint imposters.json
Port Already in Use¶
# Find process using port
lsof -i :2525
# Use different host port
docker run -p 3525:2525 zainalpour/rift-proxy:latest