Self-hosted E-Worker command runner with files, artifacts, previews, and terminals.
10K+
Remote Runner is a Docker image for E-Worker. You run it as a container, then approved E-Worker AI tools can use that container as a separate Linux place for command-line work.
Common uses:
Copy a project into the container.
Run scripts, tests, builds, and code checks.
Install project tools into mounted folders.
Collect logs, generated files, and ZIP artifacts.
Keep that work away from the main E-Worker app process.
E-Worker connects to the runner over HTTP or HTTPS.
Approved agent tools can run commands, sync files, collect logs, and return artifacts.
A secret API key is required by default; treat it like a password.
Commands run as the container user and can modify the folders you mount into the container.
File, artifact, and workspace-sync APIs are scoped to the configured workspace root and reject paths that pass through workspace symlinks.
Task commands get a scrubbed environment by default, so runner service variables such as API keys are not inherited by command processes.
This is useful Docker container separation, but it is not a complete security sandbox by itself.
The image includes sudo and an optional remote-runner user (10001:10001) for stricter deployments that run tasks as a non-root user.
eworkerinc/remote-runner:0.6.3eworkerinc/remote-runner-env:2026.06.02Use the runner service image in Docker Compose and deployment commands. The runner service image is intentionally small: it contains the Remote Runner API service and container entrypoint. It is built on top of the reusable tools image.
The reusable tools image is the large layer. It contains the SDKs, browsers, fonts, browser helper scripts, and build utilities that agents commonly need. It changes when those installed tools change or when the image is rebuilt for OS security updates. This keeps normal runner service updates much smaller after the first pull.
Versioning policy:
eworkerinc/remote-runner.0.6.3.latest tracks the current stable tag.remote-runner-env tag is pinned separately because tool updates and service updates do not always happen together.Chat, assistant, and agent workflows can use the runner only when Remote Runner tools are enabled and allowed for that workflow. Agent Console can use it when the selected agent has Remote Runner access in its policy.
From the repository:
cd E-Worker-Services/Remote-Runner
# Normal runner code change: rebuild only the small service image.
Scripts/Build-Docker-Images.sh --runner-only
# Toolchain change: rebuild the large tools image, then the service image.
Scripts/Build-Docker-Images.sh
# Security refresh: pull the latest base image, disable Docker cache, upgrade OS packages, then rebuild both images.
# Use a new dated environment tag when publishing.
Scripts/Build-Docker-Images.sh --security-refresh \
--env-image "eworkerinc/remote-runner-env:$(date +%Y.%m.%d)" \
--runner-image eworkerinc/remote-runner:NEXT_VERSION
Use Scripts/Build-Docker-Images.sh --push when publishing the built tags. A public security refresh should normally use a new dated remote-runner-env tag and a new runner service version tag, then publish the runner service image on top of the refreshed environment image.
Recommended cadence: run a security refresh at least weekly, and sooner when a relevant base image, browser, runtime, or OS package vulnerability is announced.
The runner gives work a separate Docker container filesystem and process boundary. The default compose example lets tasks run as root inside the container, so approved agents can install missing OS packages and project tools.
That is useful isolation, but it is not the same as a hardened sandbox or a separate virtual machine. Commands run with the permissions of the container user. They can read and write the folders you mount, such as /workspace and /data.
For stronger isolation, use a dedicated VM or locked-down container host, private network, VPN or gateway, narrow volume mounts, firewall rules, and resource limits. Do not expose the runner directly to the public Internet.
Use separate runner containers for separate trust boundaries. One shared runner means shared mounted folders, task history, and command execution context.
The reusable tools image is a developer-capable generic environment. It includes the tools agents commonly need when they write helper code, inspect projects, run tests, or build artifacts:
pwsh) inherited from the Microsoft .NET SDK base image; the exact version follows the base image used when remote-runner-env was rebuiltpip, venv, pipx, and Python headers for common build tasksnpmgit, curl, wget, jq, ripgrep, fd, build-essential, pkg-config, make, patch, rsync, zip, unzip, sqlite3, SSH client, and common shell/network utilitiesplaywright-chromium executable wrapper, ffmpeg, ImageMagick, and common web fonts for browser testing and debuggingUsers and agents can still install missing software inside the running runner container or download project-specific tools into mounted folders such as /workspace/.tools or /data/tools. Those folders are already on PATH, along with /data/tools/npm-global/bin and /data/tools/go/bin.
For highly repeatable work, build a custom image from this one and preinstall your team-specific CLIs, SDKs, or private package configuration. Installing OS packages inside a running container is supported by the default compose profile, but those changes live in the container filesystem and can be lost when the container is recreated.
The default image does not expose Docker or Podman inside the container.
This starts one runner container with persistent /data and /workspace folders. The example sets the API key to change-me; replace it with your own long secret.
services:
remote-runner:
image: eworkerinc/remote-runner:0.6.3
restart: unless-stopped
environment:
EW_REMOTE_RUNNER_LISTEN_ADDR: 0.0.0.0:62480
EW_REMOTE_RUNNER_DATA_DIR: /data
EW_REMOTE_RUNNER_WORKSPACE_ROOT: /workspace
EW_REMOTE_RUNNER_CORS_ORIGINS: https://app.eworker.ca
EW_REMOTE_RUNNER_PREVIEW_BIND_ADDR: "127.0.0.1"
EW_REMOTE_RUNNER_PREVIEW_PORTS: "13000-13099"
EW_REMOTE_RUNNER_PREVIEW_HOST_PORTS: "13000-13099"
EW_REMOTE_RUNNER_API_KEY: change-me
EW_REMOTE_RUNNER_API_KEY_FILE: /data/remote-runner-api-key
EW_REMOTE_RUNNER_MAX_CONCURRENT: "10"
EW_REMOTE_RUNNER_MAX_TERMINAL_SESSIONS: "20"
EW_REMOTE_RUNNER_DEFAULT_TIMEOUT_MS: "600000"
EW_REMOTE_RUNNER_SYNC_WAIT_TIMEOUT_MS: "600000"
EW_REMOTE_RUNNER_RUN_AS_USER: "false"
ports:
# Runner API, local-only default. Use 0.0.0.0 only behind private network, VPN, or gateway.
- "127.0.0.1:62480:62480"
# Local-only development preview ports for servers started inside the runner.
- "127.0.0.1:13000-13099:13000-13099"
volumes:
- ./Data:/data
- ./Workspace:/workspace
shm_size: 1g
pids_limit: 1024
mem_limit: 4g
cpus: 4
Start it:
docker compose up -d
If you remove EW_REMOTE_RUNNER_API_KEY, the entrypoint generates a key for you. Read it from the data folder or container logs:
cat ./Data/remote-runner-api-key
docker logs <container-name>
The default compose example keeps software installation simple: tasks run root-capable inside the container, and sudo is installed for agents that use common Linux setup commands.
The compose example includes browser-friendly CPU, memory, process, and /dev/shm limits. Raise them for larger builds or lower them for smaller shared hosts.
For narrow prebuilt workloads that should not install OS packages, add the optional hardened override:
docker compose -f docker-compose.yml -f docker-compose.hardened.yml up -d
The hardened override starts the container as UID/GID 10001, drops Linux capabilities, and enables no-new-privileges. Use it only when the runner already has everything it needs. Make sure mounted folders are writable by UID/GID 10001.
The default compose example publishes one local-only preview range:
127.0.0.1:13000-13099 on the Docker host13000-13099 inside the runner containerAgents use this range for long-running dev servers while builds, installs, and tests stay inside the isolated runner. The runner also reports the configured mapping in /v1/info as previewPorts, so E-Worker tools can discover custom ranges instead of guessing.
Start dev servers inside the container on 0.0.0.0, then open the matching host URL:
# Vite
npm run dev -- --host 0.0.0.0 --port 13000
# Next.js
npm run dev -- --hostname 0.0.0.0 --port 13000
# Python static server
python -m http.server 13000 --bind 0.0.0.0
Open the matching host URL, for example http://127.0.0.1:13000 for container port 13000, or http://127.0.0.1:13001 for container port 13001.
Keep preview ports local-only unless you intentionally expose them behind a private network, VPN, or gateway. If you change the Docker port mapping, update EW_REMOTE_RUNNER_PREVIEW_PORTS and EW_REMOTE_RUNNER_PREVIEW_HOST_PORTS so /v1/info remains accurate.
Docker binds the full host preview range when the container starts. If a host port is already allocated, keep EW_REMOTE_RUNNER_PREVIEW_PORTS=13000-13099 for container ports and move only the host range, for example EW_REMOTE_RUNNER_PREVIEW_HOST_PORTS=13100-13199. This lets agents keep using the same internal ports while each runner gets its own host range.
To disable preview ports, remove the preview ports line from docker-compose.yml and set EW_REMOTE_RUNNER_PREVIEW_PORTS=none.
For multiple named runners, give each runner a unique host preview range. Leave the container preview range at the default 13000-13099 unless you intentionally want agents to use different internal ports:
Scripts/Create-Docker-Runner.sh \
--workspace-id acme-workspace \
--runner-name debug-linux \
--host-port 62501 \
--preview-host-ports 13100-13199
The runner does not start an SSH server. To open a shell, run Docker commands on the Docker host. From another machine, SSH to the Docker host first, then use Docker exec.
From the compose folder:
docker compose exec -w /workspace remote-runner bash
docker compose exec -w /workspace remote-runner sh
The published runner image includes PowerShell (pwsh) from the Microsoft .NET SDK base image. Its exact version follows the mcr.microsoft.com/dotnet/sdk:10.0 image used when remote-runner-env was rebuilt.
docker compose exec -w /workspace remote-runner pwsh
Using the default container name:
docker exec -it -w /workspace ew-remote-runner bash
docker exec -it -w /workspace ew-remote-runner sh
docker exec -it -w /workspace ew-remote-runner pwsh
Find the actual container name:
docker compose ps
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"
E-Worker can also open browser-based terminal sessions for saved Remote Runner records. Use AI Ecosystem -> Remote Runners -> Open Terminal, or open a terminal from the workspace Remote Runners tree/folder viewer. These sessions use short-lived WebSocket tokens and are capped by EW_REMOTE_RUNNER_MAX_TERMINAL_SESSIONS (default 20).
Run a browser smoke test inside the container:
browser-debug-smoke
Start a Chromium session with Chrome DevTools Protocol enabled:
debug-chrome https://example.com
debug-chrome listens on container port 9222, uses headless mode automatically when no DISPLAY is available, and writes screenshots/traces under BROWSER_ARTIFACTS_DIR (/workspace/.artifacts/browser by default).
Do not expose Chrome DevTools/CDP to the public Internet. For local debugging, use the optional compose override from this repository:
docker compose \
-f docker-compose.yml \
-f docker-compose.browser-debug.yml \
up -d
export EW_REMOTE_RUNNER_API_KEY="change-me"
curl -sS http://127.0.0.1:62480/healthz
curl -sS \
-H "Authorization: Bearer $EW_REMOTE_RUNNER_API_KEY" \
http://127.0.0.1:62480/v1/info
curl -sS \
-H "Authorization: Bearer $EW_REMOTE_RUNNER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode":"sync","shell":"sh","command":"python --version && node --version && go version && pwd"}' \
http://127.0.0.1:62480/v1/tasks
Use one image as a template for many named runner containers:
cd E-Worker-Services/Remote-Runner
chmod +x Scripts/Create-Docker-Runner.sh
Scripts/Create-Docker-Runner.sh \
--workspace-id acme-workspace \
--runner-name debug-linux \
--host-port 62501 \
--image-tag eworkerinc/remote-runner:0.6.3
The helper writes:
Instances/<workspace-id>/<runner-name>/.envInstances/<workspace-id>/<runner-name>/api-key.txtInstances/<workspace-id>/<runner-name>/Data/Instances/<workspace-id>/<runner-name>/Workspace/Remote Runner supports HTTPS. Use it when browser-based E-Worker is served over HTTPS and calls the runner directly.
Create a local test certificate:
mkdir -p certs
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout certs/service.key -out certs/service.crt \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
Then add these lines under the existing environment and volumes sections:
# under environment:
EW_REMOTE_RUNNER_TLS_CERT_FILE: /certs/service.crt
EW_REMOTE_RUNNER_TLS_KEY_FILE: /certs/service.key
EW_REMOTE_RUNNER_CORS_ORIGINS: https://app.eworker.ca
# under volumes:
- ./certs:/certs:ro
Only set the two TLS file variables after ./certs/service.crt and ./certs/service.key exist on the host and the folder is mounted as /certs. If you do not need HTTPS yet, remove both TLS file variables and use the HTTP URL.
Restart the container and verify it:
curl -k https://127.0.0.1:62480/healthz
Use -k only while testing a self-signed certificate. For LAN access, create a certificate that includes the real host name or IP address, then use that HTTPS URL in E-Worker.
CORS allows E-Worker Studio by default. Set it to none for private desktop or server-to-server flows where the browser will not call the runner directly. The examples in this section use .env syntax:
EW_REMOTE_RUNNER_CORS_ORIGINS=https://app.eworker.ca
Disable browser CORS:
EW_REMOTE_RUNNER_CORS_ORIGINS=none
Allow E-Worker Studio plus a customer-managed browser origin with a comma-separated list:
EW_REMOTE_RUNNER_CORS_ORIGINS=https://app.eworker.ca,https://studio.example.com
Use * only for trusted local testing:
EW_REMOTE_RUNNER_CORS_ORIGINS=*
If you edit docker-compose.yml directly, use YAML mapping syntax under environment. Use :, not =, and quote * because it has special meaning in YAML:
environment:
# allow E-Worker Studio
EW_REMOTE_RUNNER_CORS_ORIGINS: "https://app.eworker.ca"
# or allow multiple origins
# EW_REMOTE_RUNNER_CORS_ORIGINS: "https://app.eworker.ca,https://studio.example.com"
# or, for trusted local testing only
# EW_REMOTE_RUNNER_CORS_ORIGINS: "*"
Origins must match the browser origin exactly. Include the scheme and port when needed. Do not add paths or a trailing slash.
If you use the repo docker-compose.yml, put the value in the .env file next to the compose file, then recreate the container:
docker compose up -d --force-recreate
For named runners, pass CORS when creating the runner:
Scripts/Create-Docker-Runner.sh \
--workspace-id acme-workspace \
--runner-name secure-linux \
--host-port 62502 \
--cors-origins https://app.eworker.ca
Check a browser preflight:
curl -k -i -X OPTIONS https://127.0.0.1:62480/v1/info \
-H "Origin: https://app.eworker.ca" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization"
In E-Worker AI Ecosystem, add a Remote Runner record:
http://127.0.0.1:62480, or https://127.0.0.1:62480 when HTTPS is enabledEW_REMOTE_RUNNER_API_KEY or Data/remote-runner-api-keyAgent Console can then use approved tools for:
Users can also open interactive terminal sessions from AI Ecosystem or the workspace Remote Runners browser. The browser requests a short-lived terminal token with the configured API key, then connects to the runner over WebSocket. Open more than one terminal when needed and choose an available shell such as bash, sh, or pwsh.
Content type
Image
Digest
sha256:da2506852…
Size
1.4 GB
Last updated
4 months ago
docker pull eworkerinc/remote-runner