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.
Example topology
Section titled “Example topology”Consider a simple HTTP service that exposes:
GET /okon port 9000.
We want E2Engine to:
- run in its own container,
- expose an E2Engine-managed service boundary,
- forward requests to the real HTTP service,
- observe the interaction,
- evaluate the Test,
- 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.
Before you start
Section titled “Before you start”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:
docker pull ghcr.io/e2engine/cli:0.1.0You can use latest when you explicitly want the latest published image:
docker pull ghcr.io/e2engine/cli:latestSee Docker for details about the E2Engine image.
1. Create a Docker network
Section titled “1. Create a Docker network”Create a network shared by E2Engine and the services participating in the test:
docker network create e2engine-networkContainers attached to this network can address each other using their container names.
2. Start the real service
Section titled “2. Start the real service”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:
docker run -d \ --name ok-http \ --network e2engine-network \ ok-http:latestWithin e2engine-network, other containers can now reach the service at:
ok-http:9000No host port needs to be published for communication between containers on the same Docker network.
3. Define the Environment
Section titled “3. Define the Environment”Create an Environment specification:
kind: Environmentversion: 1.0.0name: direct-real-httpdescription: one real HTTP service that returns 200 OK for /okspec: services: - id: direct-real-http kind: http mode: real address: 127.0.0.1:8083 http_target: http://ok-http:9000There are two important addresses here:
address: 127.0.0.1:8083http_target: http://ok-http:9000They serve different purposes.
Service address
Section titled “Service address”address: 127.0.0.1:8083is 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.
Real service target
Section titled “Real service target”http_target: http://ok-http:9000identifies 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 serviceThis preserves the E2Engine service boundary while allowing the actual service to run in another container.
4. Define the Test
Section titled “4. Define the Test”Create a Test that sends a request through the Environment service boundary:
kind: Testversion: 1.0.0name: direct-real-httpdescription: direct-real-http service is called at least oncespec: 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: /okThe Test expects:
- an HTTP
200response, - and at least one matching call through the
direct-real-httpservice boundary.
5. Prepare persistent E2Engine data
Section titled “5. Prepare persistent E2Engine data”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:
mkdir -p .e2engineIt will be mounted as /data:
host container
./.e2engine ──────────────▶ /dataand configured with:
E2ENGINE_DATA_DIR=/dataWith the default database configuration, E2Engine stores its database at:
/data/e2engine.sqlitewhich persists on the host as:
./.e2engine/e2engine.sqlite6. Mount the specifications
Section titled “6. Mount the specifications”Assume the project contains:
project/├── .e2engine/└── e2engine/ ├── direct-real-http.env.yml └── direct-real-http.test.ymlMount the specification directory as /specs:
host ./e2engine ──────────▶ container /specsThe mount can be read-only because E2Engine only needs to read the specification files.
7. Create the Environment
Section titled “7. Create the Environment”Run E2Engine on the same Docker network as ok-http:
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.ymlE2Engine 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.08. Create the Test
Section titled “8. Create the Test”Create the Test using the same data directory:
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.ymlThe result is persisted in the same database:
created test, name: direct-real-http id: <test-id> version: 1.0.09. Run the Test
Section titled “9. Run the Test”Run the Test against the Environment:
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-httpThe 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.
10. Inspect the result
Section titled “10. Inspect the result”Use the same persistent data directory to retrieve the TestExecution:
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 yamlThe execution records the request, response, expectations, observed calls, deviations, and execution error information when applicable.
See Inspecting results for details.
Understanding localhost
Section titled “Understanding localhost”Container networking changes the meaning of:
localhost127.0.0.1Inside 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:9000would 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:9000This distinction is why an Environment specification used for host-based testing may need different target addresses when the same system is run in Docker.
E2Engine service boundaries
Section titled “E2Engine service boundaries”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:8083http_target: http://ok-http:9000the complete path is:
Test request │ ▼E2Engine service address127.0.0.1:8083 │ ▼E2Engine routing and observation │ ▼real service targetok-http:9000Because 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:8081and 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:8081Testing 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 serviceThe 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 omittedThe system under test is configured to call the E2Engine container rather than its dependencies directly:
FRAUD_SERVICE_URL=http://e2engine:8081ACCOUNT_SERVICE_ADDR=e2engine:8082NOTIFICATION_SERVICE_ADDR=e2engine:8083This 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 serviceIf 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:
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:8081listens on the E2Engine container’s network interface and can be reached from another container as:
e2engine:8081Do 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.
Custom configuration
Section titled “Custom configuration”A custom config.yml can be mounted into the container when execution settings need to differ from the defaults:
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-httpConfiguration values that support environment binding can also be passed directly:
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-demoSee 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:
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-demoFor scripts and CI pipelines, capture the TestSuiteExecution ID with quiet output and use check as the verification step:
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 1ficheck 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:
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 testexecutionsSee Running a test suite for TestSuite execution semantics.
Minimal container image
Section titled “Minimal container image”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:
shbashcurlcatare 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.
Troubleshooting
Section titled “Troubleshooting”When a containerized Test cannot reach a service, first verify the network topology.
Real service cannot be reached
Section titled “Real service cannot be reached”If an Environment contains:
http_target: http://127.0.0.1:9000but the service runs in another container, replace the loopback address with a name reachable through the shared Docker network:
http_target: http://ok-http:9000Real 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:50051Route the call through the E2Engine service boundary instead:
Payment API ──▶ e2engine:8082 ──▶ account:50051The Environment should define both the E2Engine boundary and the real target:
address: 0.0.0.0:8082grpc_target: account:50051E2Engine 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:8081For another container to reach the boundary, bind it to the container network interface:
address: 0.0.0.0:8081and call it through the E2Engine container name:
e2engine:8081Containers cannot resolve each other
Section titled “Containers cannot resolve each other”Verify that both containers use the same network:
docker network inspect e2engine-networkThe E2Engine container and participating services should appear on that network.
Resources disappear between commands
Section titled “Resources disappear between commands”Each docker run --rm invocation uses a new container.
Make sure every E2Engine command uses the same persistent data mount:
-v "$PWD/.e2engine:/data"-e E2ENGINE_DATA_DIR=/dataSpecification file cannot be found
Section titled “Specification file cannot be found”Paths passed to E2Engine are paths inside the container.
With:
-v "$PWD/e2engine:/specs:ro"use:
/specs/direct-real-http.env.ymlrather 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.protoand execution resolves that path as /proto/account.proto, mount the directory for every E2Engine invocation that may need it:
-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:
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 yamlCheck 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.
Clean up
Section titled “Clean up”Remove the example service:
docker rm -f ok-httpand remove the Docker network:
docker network rm e2engine-networkThe persisted E2Engine database remains in:
./.e2engineRemove that directory only when you no longer need the stored resources and execution history.
Next steps
Section titled “Next steps”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:
- Docker for the E2Engine container image.
- Environments for Environment and service definitions.
- Running a test for Test execution behavior.
- Inspecting results for execution results.
- CI/CD pipelines for running E2Engine in automated pipelines.

