Skip to content

Running E2Engine with Docker

Running E2Engine in Docker uses the same Environment, Test, TestSuite, and execution model as the locally installed CLI.

The main difference is networking. There are two directions of traffic to consider:

  • E2Engine must be able to reach real services through Docker network names;
  • the system under test must be able to reach E2Engine-managed service boundaries for mocked and observed real dependencies.

When E2Engine and the system under test run in containers, service addresses must therefore reflect the Docker network topology rather than the host machine’s network.

This guide starts with a minimal real-service example and then shows a distributed topology where a system under test calls mocked services and a real service through E2Engine.

Consider a simple HTTP service that exposes:

GET /ok

on port 9000.

We want E2Engine to:

  1. run in its own container,
  2. expose an E2Engine-managed service boundary,
  3. forward requests to the real HTTP service,
  4. observe the interaction,
  5. evaluate the Test,
  6. persist the TestExecution result.

The resulting topology is:

Docker network
┌───────────────────────────────────────────────┐
│ │
│ E2Engine container │
│ │
│ Test request │
│ │ │
│ ▼ │
│ 127.0.0.1:8083 │
│ │ │
│ │ E2Engine service boundary │
│ │ │
│ ▼ │
│ http://ok-http:9000 ──────────────┐ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ ok-http │ │
│ │ :9000 │ │
│ └─────────────┘ │
│ │
└───────────────────────────────────────────────┘

Both containers participate in the same Docker network.

You need:

  • Docker installed and running,
  • the E2Engine image,
  • an Environment specification,
  • a Test specification,
  • and a containerized system or service to test.

Pull a specific E2Engine version for reproducible execution:

Terminal window
docker pull ghcr.io/e2engine/cli:0.1.0

You can use latest when you explicitly want the latest published image:

Terminal window
docker pull ghcr.io/e2engine/cli:latest

See Docker for details about the E2Engine image.

Create a network shared by E2Engine and the services participating in the test:

Terminal window
docker network create e2engine-network

Containers attached to this network can address each other using their container names.

For this example, assume an HTTP service listening on port 9000:

package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/ok", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
if err := http.ListenAndServe(":9000", nil); err != nil {
log.Fatal(err)
}
}

Run its image on the E2Engine network with the name ok-http:

Terminal window
docker run -d \
--name ok-http \
--network e2engine-network \
ok-http:latest

Within e2engine-network, other containers can now reach the service at:

ok-http:9000

No host port needs to be published for communication between containers on the same Docker network.

Create an Environment specification:

kind: Environment
version: 1.0.0
name: direct-real-http
description: one real HTTP service that returns 200 OK for /ok
spec:
services:
- id: direct-real-http
kind: http
mode: real
address: 127.0.0.1:8083
http_target: http://ok-http:9000

There are two important addresses here:

address: 127.0.0.1:8083
http_target: http://ok-http:9000

They serve different purposes.

address: 127.0.0.1:8083

is the E2Engine-managed service boundary.

The Test sends its request to this address inside the E2Engine container.

E2Engine can therefore observe the request before routing it to the real service.

http_target: http://ok-http:9000

identifies the real service.

Because ok-http is another container on the same Docker network, Docker DNS resolves the container name to its network address.

The request path is therefore:

Test
│
▼
127.0.0.1:8083
│
▼
E2Engine router
│
▼
http://ok-http:9000
│
▼
real HTTP service

This preserves the E2Engine service boundary while allowing the actual service to run in another container.

Create a Test that sends a request through the Environment service boundary:

kind: Test
version: 1.0.0
name: direct-real-http
description: direct-real-http service is called at least once
spec:
request:
http:
method: GET
url: http://127.0.0.1:8083/ok
expect:
http:
status: 200
calls:
- service_id: direct-real-http
http:
method: GET
path: /ok

The Test expects:

  • an HTTP 200 response,
  • and at least one matching call through the direct-real-http service boundary.

Each docker run --rm invocation creates a temporary container.

Persist the E2Engine database on the host so that resources created by one invocation remain available to later commands.

Create a directory:

Terminal window
mkdir -p .e2engine

It will be mounted as /data:

host container
./.e2engine ──────────────▶ /data

and configured with:

E2ENGINE_DATA_DIR=/data

With the default database configuration, E2Engine stores its database at:

/data/e2engine.sqlite

which persists on the host as:

./.e2engine/e2engine.sqlite

Assume the project contains:

project/
├── .e2engine/
└── e2engine/
├── direct-real-http.env.yml
└── direct-real-http.test.yml

Mount the specification directory as /specs:

host ./e2engine ──────────▶ container /specs

The mount can be read-only because E2Engine only needs to read the specification files.

Run E2Engine on the same Docker network as ok-http:

Terminal window
docker run --rm \
--network e2engine-network \
-v "$PWD/.e2engine:/data" \
-v "$PWD/e2engine:/specs:ro" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
create environment /specs/direct-real-http.env.yml

E2Engine creates the Environment and persists it in the mounted database.

The output includes its name, ID, and version:

created environment, name: direct-real-http id: <environment-id> version: 1.0.0

Create the Test using the same data directory:

Terminal window
docker run --rm \
--network e2engine-network \
-v "$PWD/.e2engine:/data" \
-v "$PWD/e2engine:/specs:ro" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
create test /specs/direct-real-http.test.yml

The result is persisted in the same database:

created test, name: direct-real-http id: <test-id> version: 1.0.0

Run the Test against the Environment:

Terminal window
docker run --rm \
--name e2engine \
--network e2engine-network \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
run test direct-real-http direct-real-http

The first reference is the Test and the second is the Environment:

e2engine run test <test-ref> <env-ref>

The command creates a TestExecution:

created test execution with id: <test-execution-id>

With the default direct transport, the command waits for the Test execution to complete before returning.

Use the same persistent data directory to retrieve the TestExecution:

Terminal window
docker run --rm \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
get testexecution <test-execution-id> -o yaml

The execution records the request, response, expectations, observed calls, deviations, and execution error information when applicable.

See Inspecting results for details.

Container networking changes the meaning of:

localhost
127.0.0.1

Inside a container, these addresses refer to that container itself.

They do not refer to another container.

For example, this Environment target:

http_target: http://127.0.0.1:9000

would tell E2Engine to connect to port 9000 inside the E2Engine container.

It would not reach the ok-http container.

Because the real service is another container on the shared network, use its Docker network name instead:

http_target: http://ok-http:9000

This distinction is why an Environment specification used for host-based testing may need different target addresses when the same system is run in Docker.

Real service targets are only one side of the network topology.

An Environment service address defines where E2Engine exposes the service boundary through which traffic is observed.

For the example:

address: 127.0.0.1:8083
http_target: http://ok-http:9000

the complete path is:

Test request
│
▼
E2Engine service address
127.0.0.1:8083
│
▼
E2Engine routing and observation
│
▼
real service target
ok-http:9000

Because the Test itself is executed by E2Engine, the loopback service address works for this direct request.

A different topology may require an E2Engine service boundary to be reachable by another container.

This is common when the system under test makes calls to services represented by E2Engine, particularly mocked services.

In that case, the service boundary must listen on an address reachable through the container network, for example:

address: 0.0.0.0:8081

and the system under test can address the E2Engine container through its Docker network name.

If the E2Engine container is named e2engine, that service can be reached from another container as:

e2engine:8081

Testing systems with mocked and real dependencies

Section titled “Testing systems with mocked and real dependencies”

A distributed-system test commonly has the system under test call several dependencies. Some may be mocked by E2Engine while others are real services whose calls still need to be observed.

The E2Engine demo uses this topology:

Docker network
┌─────────────────┐
│ Payment API │
└────────┬────────┘
│
all dependency calls
go through E2Engine
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
e2engine:8081 e2engine:8082 e2engine:8083
Fraud Account Notification
mocked real boundary mocked
│
▼
account:50051
real service

The corresponding Environment can use container-reachable service boundaries:

spec:
services:
- id: fraud
kind: http
mode: mocked
address: 0.0.0.0:8081
- id: account
kind: grpc
mode: real
address: 0.0.0.0:8082
grpc_target: account:50051
proto:
external:
file: ./proto/account.proto
service: account.v1.AccountService
- id: notification
kind: grpc
mode: mocked
address: 0.0.0.0:8083
# protobuf contract and fixtures omitted

The system under test is configured to call the E2Engine container rather than its dependencies directly:

FRAUD_SERVICE_URL=http://e2engine:8081
ACCOUNT_SERVICE_ADDR=e2engine:8082
NOTIFICATION_SERVICE_ADDR=e2engine:8083

This distinction is essential for real services as well as mocked ones.

For the real Account service, the traffic path is:

Payment API
│
▼
e2engine:8082
│
▼
E2Engine gRPC boundary
│
▼
account:50051
│
▼
real Account service

If the Payment API called account:50051 directly, the application call could succeed, but E2Engine would not observe it. A Test expectation such as calls.account.count: 1 would then report zero observed calls.

For mocked services there is no downstream target. E2Engine itself handles the request using the configured fixture while recording the interaction.

Making E2Engine reachable from the system under test

Section titled “Making E2Engine reachable from the system under test”

When another container calls E2Engine-managed services, the E2Engine execution container needs a stable Docker network name. Give it the name e2engine:

Terminal window
docker run --rm \
--name e2engine \
--network e2engine-network \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
run test <test-ref> <environment-ref>

A service configured with:

address: 0.0.0.0:8081

listens on the E2Engine container’s network interface and can be reached from another container as:

e2engine:8081

Do not use 127.0.0.1 for a service boundary that must be called by another container. Binding to loopback makes the service reachable only from inside the E2Engine container.

Similarly, host.docker.internal is not the correct target when the mocked service is running inside the E2Engine container. Containers on the shared user-defined network should communicate through their Docker network names.

The stable name is needed for the execution invocation that hosts the active E2Engine service boundaries. Resource-creation commands may use short-lived unnamed containers because they only persist definitions in the shared data directory.

A custom config.yml can be mounted into the container when execution settings need to differ from the defaults:

Terminal window
docker run --rm \
--network e2engine-network \
-v "$PWD/e2engine/config.yml:/config/config.yml:ro" \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_CONFIG_PATH=/config/config.yml \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
run test direct-real-http direct-real-http

Configuration values that support environment binding can also be passed directly:

Terminal window
docker run --rm \
--network e2engine-network \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
-e E2ENGINE_RUNNER_WORKERS_POOL_SIZE=4 \
ghcr.io/e2engine/cli:0.1.0 \
run testsuite smoke payment-demo

See Configuration for available settings.

Running TestSuites and checking the result

Section titled “Running TestSuites and checking the result”

TestSuites use the same Docker setup.

Once an Environment, Tests, and TestSuite have been created in the persistent E2Engine database, run the suite from a named E2Engine container when the system under test needs to reach E2Engine-managed services:

Terminal window
docker run --rm \
--name e2engine \
--network e2engine-network \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
run testsuite smoke payment-demo

For scripts and CI pipelines, capture the TestSuiteExecution ID with quiet output and use check as the verification step:

Terminal window
tid=$(docker run --rm \
--name e2engine \
--network e2engine-network \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
run ts smoke payment-demo -q)
if ! docker run --rm \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
check tse "$tid" -q; then
echo "Test suite failed."
docker run --rm \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
get tse "$tid"
exit 1
fi

check tse exits with status 0 for a passed TestSuiteExecution and a non-zero status when verification fails, making it suitable for CI.

You can list the persisted executions afterward:

Terminal window
docker run --rm \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
get testsuiteexecutions
docker run --rm \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
get testexecutions

See Running a test suite for TestSuite execution semantics.

The E2Engine image is built from scratch.

It contains the statically built E2Engine executable and does not contain a shell, package manager, or general-purpose operating-system utilities.

Commands such as:

sh
bash
curl
cat

are therefore not available inside the E2Engine container.

If additional diagnostic tools are needed, run them from the host or from a separate utility container attached to the same Docker network.

When a containerized Test cannot reach a service, first verify the network topology.

If an Environment contains:

http_target: http://127.0.0.1:9000

but the service runs in another container, replace the loopback address with a name reachable through the shared Docker network:

http_target: http://ok-http:9000

Real service call succeeds but E2Engine records zero calls

Section titled “Real service call succeeds but E2Engine records zero calls”

Check whether the system under test is calling the real service directly.

For a real service that E2Engine must observe, this bypasses E2Engine:

Payment API ──▶ account:50051

Route the call through the E2Engine service boundary instead:

Payment API ──▶ e2engine:8082 ──▶ account:50051

The Environment should define both the E2Engine boundary and the real target:

address: 0.0.0.0:8082
grpc_target: account:50051

E2Engine service returns connection refused from another container

Section titled “E2Engine service returns connection refused from another container”

If Docker DNS resolves e2engine but the connection to an E2Engine-managed service is refused, check the service boundary binding.

This binds only inside the E2Engine container:

address: 127.0.0.1:8081

For another container to reach the boundary, bind it to the container network interface:

address: 0.0.0.0:8081

and call it through the E2Engine container name:

e2engine:8081

Verify that both containers use the same network:

Terminal window
docker network inspect e2engine-network

The E2Engine container and participating services should appear on that network.

Each docker run --rm invocation uses a new container.

Make sure every E2Engine command uses the same persistent data mount:

Terminal window
-v "$PWD/.e2engine:/data"
-e E2ENGINE_DATA_DIR=/data

Paths passed to E2Engine are paths inside the container.

With:

Terminal window
-v "$PWD/e2engine:/specs:ro"

use:

/specs/direct-real-http.env.yml

rather than the corresponding host path.

External files referenced by a specification must also exist inside the container at the path E2Engine resolves during execution.

For example, if an Environment references:

proto:
external:
file: ./proto/account.proto

and execution resolves that path as /proto/account.proto, mount the directory for every E2Engine invocation that may need it:

Terminal window
-v "$PWD/proto:/proto:ro"

This is especially important when resource creation and execution happen in separate docker run --rm invocations.

Test result differs from expected behavior

Section titled “Test result differs from expected behavior”

Inspect the persisted TestExecution first:

Terminal window
docker run --rm \
-v "$PWD/.e2engine:/data" \
-e E2ENGINE_DATA_DIR=/data \
ghcr.io/e2engine/cli:0.1.0 \
get testexecution <test-execution-id> -o yaml

Check its response, observed calls, deviations, and execution error.

If additional runtime information is needed, use E2Engine logs to inspect service mounting, route registration, and HTTP or gRPC routing.

See Logging for details.

Remove the example service:

Terminal window
docker rm -f ok-http

and remove the Docker network:

Terminal window
docker network rm e2engine-network

The persisted E2Engine database remains in:

./.e2engine

Remove that directory only when you no longer need the stored resources and execution history.

This guide demonstrated the main Docker-specific concerns: persistent E2Engine data, mounted specifications and external files, shared networks, container DNS, real service targets, E2Engine-managed service boundaries, routing dependency calls through E2Engine, and CI-friendly execution verification.

See: