From 11c040106a0b40937125ffc9ee1d07ec2f4011bd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 07:57:03 +0000 Subject: [PATCH 1/3] feat: make the API and console ports configurable ACTOR_RUNTIME_API_PORT and ACTOR_RUNTIME_CONSOLE_PORT (defaults 3333 and 3000) move the ports the runtime listens on and every URL it hands out. Invalid or clashing values fail at startup. Refs #77 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ac6RaZAULDqhZMW7UhSeHD --- Dockerfile | 1 + README.md | 8 ++++++-- requirements/actor-driver.md | 2 +- requirements/console.md | 2 +- requirements/system.md | 15 +++++++++------ skills/actor-runtime/SKILL.md | 10 +++++++++- src/config.ts | 28 +++++++++++++++++++++++----- src/index.ts | 2 +- test/unit/config-ports.test.ts | 20 ++++++++++++++++++++ 9 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 test/unit/config-ports.test.ts diff --git a/Dockerfile b/Dockerfile index 9516f187..813db72b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -96,6 +96,7 @@ COPY --from=browser-viewer-payload /payload/version.txt /opt/apify-browser-viewe VOLUME ["/data"] ENV ACTOR_RUNTIME_DATA_DIR=/data +# Defaults; ACTOR_RUNTIME_API_PORT / ACTOR_RUNTIME_CONSOLE_PORT move them. EXPOSE 3333 3000 CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md index 973789f4..b1bc93b6 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ The console is at [http://localhost:3000](http://localhost:3000). The full walkt Skill: `apify runtime skill` prints it, and `apify runtime skill --install` installs it for your coding agent. +Port 3333 or 3000 already taken? `apify runtime start --api-port 4333 --console-port 4000` moves them. + If you are not logged in, any non-empty token works (`apify login --token local-dev-token`). In build and run logs, everything the runtime itself says opens with a blue `[actor-runtime]` prefix. @@ -188,8 +190,10 @@ podman run --rm -p 3333:3333 -p 3000:3000 \ - For rootful Podman, mount `/run/podman/podman.sock` and run with `sudo`. For rootless Docker, mount `$XDG_RUNTIME_DIR/docker.sock`. To mount the socket at another path, also set `-e DOCKER_HOST=unix:///that/path`. -- Podman 3.4 (Ubuntu 22.04's stock package) and newer are supported. Keep `-p 3333:3333` published on - all interfaces: under Podman 3.x and rootless Podman, Actors reach the API through it. +- To use other ports, set `-e ACTOR_RUNTIME_API_PORT=4333 -e ACTOR_RUNTIME_CONSOLE_PORT=4000` and publish + the same numbers on the host (`-p 4333:4333 -p 4000:4000`). +- Podman 3.4 (Ubuntu 22.04's stock package) and newer are supported. Keep the API port published on + all interfaces (`-p 3333:3333` by default): under Podman 3.x and rootless Podman, Actors reach the API through it. - Podman does not create a missing bind-mount directory, hence `mkdir -p data`. `apify runtime start` creates its data directory itself. - Short image names in an Actor's `FROM` line (`apify/actor-node:20`) resolve to Docker Hub, as on the diff --git a/requirements/actor-driver.md b/requirements/actor-driver.md index 4a09dc78..6d5a5eec 100644 --- a/requirements/actor-driver.md +++ b/requirements/actor-driver.md @@ -165,7 +165,7 @@ start`, ...) is refused by name, naming both the `CMD` fix and how to clear debu # Networking -- Every Actor container reaches the runtime's API at `http://apify-api:3333`, whatever the host's own +- Every Actor container reaches the runtime's API at `http://apify-api:` (default 3333, `system.md`), whatever the host's own networking, whichever supported engine runs the containers, and however the runtime itself was started (as a container or not). The runtime provides the `apify-local` network with the DNS alias `apify-api` for this; when it has to reach the same goal another way, it says so at startup. diff --git a/requirements/console.md b/requirements/console.md index 26f8cfe9..3671cf3d 100644 --- a/requirements/console.md +++ b/requirements/console.md @@ -1,7 +1,7 @@ # Frontend - Console frontend is a page that allows inspecting each user's objects across the whole runtime. -- Server-rendered HTML on its own fixed port (3000); the console reflects the same live state the API serves. +- Server-rendered HTML on its own port (default 3000, `system.md`); the console reflects the same live state the API serves. - Frontend shows for each object the owner (`userId`). - The console has no login of its own, so with multiple users it lists and shows every user's objects rather than scoping to one - the API's own endpoints stay strictly scoped to the calling token's user diff --git a/requirements/system.md b/requirements/system.md index 12fe589b..9ccd4587 100644 --- a/requirements/system.md +++ b/requirements/system.md @@ -19,25 +19,28 @@ - The system is isolated environment that is started by running the docker container. - The system user interface is accessible on localhost with specific ports for console frontend and API. - The user interacts with the system through the Apify cli. -- On startup the container prints a banner naming the API port (3333) and the console port (3000), +- On startup the container prints a banner naming the API port (default 3333) and the console port (default 3000), plus a warning if the host's Docker socket could not be reached - builds and runs then fail fast with a clear status message, while every other endpoint (storages, actor/build/run records, console) still works. -- Both ports are fixed and not configurable. -- Port 3333 also serves the per-run events websocket and standby Actors (`api.md`); no additional port is +- Both ports are configurable through the runtime's own environment: `ACTOR_RUNTIME_API_PORT` and + `ACTOR_RUNTIME_CONSOLE_PORT`. The runtime refuses to start when either is not a valid TCP port or both + are the same. Each is published on the same port number on the host (`-p N:N`); every URL the runtime + hands out (console links, standby URLs, browser view, the Actor containers' API URL) uses the configured ports. +- The API port also serves the per-run events websocket and standby Actors (`api.md`); no additional port is published for either. - **Debug mode is the one exception to "no other Actor container port is ever published"** (`actor-driver.md`'s "Debug mode" section): when debug mode is on for an Actor, that Actor's runs get a port published on the host, bound to `127.0.0.1` (`5678` Python / `9229` Node by default, per-Actor overridable) - the runtime's own two ports above are unaffected, and no port is published for an Actor that never turned debug mode on. -- Browser view (`actor-driver.md`) publishes no port on the host; the view is served on the console's port 3000. +- Browser view (`actor-driver.md`) publishes no port on the host; the view is served on the console's port. - Required `docker run` flags: mount the host's Docker-Engine-API socket read-write (`-v /var/run/docker.sock:/var/run/docker.sock`) so the runtime can build and run Actor containers, and mount a persistent data directory (`-v :/data`, e.g. `-v "$(pwd)/data:/data"`) so storages survive a restart and are easy to inspect from the host; the directory must exist before the - runtime starts. Publish both fixed ports - (`-p 3333:3333 -p 3000:3000`). The canonical start command is: + runtime starts. Publish both ports + (`-p 3333:3333 -p 3000:3000` for the defaults). The canonical start command is: ```bash docker build -t actor-runtime . diff --git a/skills/actor-runtime/SKILL.md b/skills/actor-runtime/SKILL.md index f5a7855c..7c7dddd8 100644 --- a/skills/actor-runtime/SKILL.md +++ b/skills/actor-runtime/SKILL.md @@ -7,7 +7,7 @@ description: Drive the local Apify Actor runtime - a self-contained local Apify These are the operating instructions for the Actor runtime the reader is talking to: a local Apify platform serving an Apify-compatible API on `http://localhost:3333` and a console UI on -`http://localhost:3000`. It emulates the subset of the Apify API needed to develop, run and debug +`http://localhost:3000` (the defaults - see "Other ports" below). It emulates the subset of the Apify API needed to develop, run and debug Actors locally, so no change needs a rebuild on the real platform to be tried out. It is not the Apify platform. Actors, builds, runs and storages created here exist only in this @@ -35,6 +35,14 @@ Any non-empty token authenticates - `apify login --token local-dev-token` is eno `APIFY_DISABLE_KEYRING=1` first in a sandbox with no OS keyring. To act as a second user, pass a different token on a single call: `apify api v2/datasets -H '{"authorization": "Bearer OTHER"}'`. +### Other ports + +When 3333 or 3000 is taken on this machine, start the runtime on other ports: +`apify runtime start --api-port 4333 --console-port 4000`. The CLI remembers them for later starts and +for `apify runtime connect`; with a hand-started container, set `-e ACTOR_RUNTIME_API_PORT=4333 +-e ACTOR_RUNTIME_CONSOLE_PORT=4000` and publish the same numbers (`-p 4333:4333 -p 4000:4000`). Every URL +in this document then uses those ports instead, `apify-api:3333` included. + ## The normal loop ```sh diff --git a/src/config.ts b/src/config.ts index 7e355b89..ce72006d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,24 @@ -/** Fixed, non-configurable ports (`system.md`): identical on every start, no env var overrides them. */ -export const API_PORT = 3333; -export const CONSOLE_PORT = 3000; +export const DEFAULT_API_PORT = 3333; +export const DEFAULT_CONSOLE_PORT = 3000; + +/** A port from the runtime's own environment (`system.md`). It is the port both listened on inside the + * container and published on the host (`-p N:N`): the URLs the runtime hands out, and the host-gateway + * route Actor containers may take to the API, assume the two are the same. */ +export function portFromEnv(name: string, fallback: number, env: NodeJS.ProcessEnv = process.env): number { + const raw = env[name]?.trim(); + if (!raw) return fallback; + const port = Number(raw); + if (!/^\d+$/.test(raw) || port < 1 || port > 65535) { + throw new Error(`${name} must be a TCP port number between 1 and 65535, got '${raw}'.`); + } + return port; +} + +export const API_PORT = portFromEnv('ACTOR_RUNTIME_API_PORT', DEFAULT_API_PORT); +export const CONSOLE_PORT = portFromEnv('ACTOR_RUNTIME_CONSOLE_PORT', DEFAULT_CONSOLE_PORT); +if (API_PORT === CONSOLE_PORT) { + throw new Error(`ACTOR_RUNTIME_API_PORT and ACTOR_RUNTIME_CONSOLE_PORT must differ, both are ${API_PORT}.`); +} /** The DNS alias every Actor container resolves the runtime's own API by, on the `apify-local` network. */ export const CONTAINER_API_ALIAS = 'apify-api'; @@ -9,10 +27,10 @@ export const CONTAINER_API_BASE_URL = `http://${CONTAINER_API_ALIAS}:${API_PORT} /** Base for the events-websocket URL every Actor container is given (`ACTOR_EVENTS_WEBSOCKET_URL` / * `APIFY_ACTOR_EVENTS_WS_URL`, `services/runs.ts: buildEnv`) - the same host:port as * `CONTAINER_API_BASE_URL`, just `ws://` instead of `http://`: the events endpoint upgrades on the - * existing API server (`api/events-ws.ts`), not a second port (`system.md`'s fixed-ports contract). */ + * existing API server (`api/events-ws.ts`), not a second port (`system.md`). */ export const CONTAINER_EVENTS_WS_BASE_URL = `ws://${CONTAINER_API_ALIAS}:${API_PORT}`; -/** Host-facing base URL for the local console UI (fixed port, `system.md`) - used only to build the +/** Host-facing base URL for the local console UI (`system.md`) - used only to build the * `consoleUrl` field storage DTOs return (the real platform's equivalent points at * `console.apify.com`; this points at the one console this runtime actually serves). The path appended * after this base uses the real platform's URL shape (e.g. `/storage/datasets/:id`), which the console diff --git a/src/index.ts b/src/index.ts index e4bff536..d5f030c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,7 +27,7 @@ async function main(): Promise { const apiServer = apiApp.listen(API_PORT); const consoleServer = consoleApp.listen(CONSOLE_PORT); - // Upgrades on the same API server/port - no second port (`system.md`'s fixed-ports contract); see + // Upgrades on the same API server/port - no second port (`system.md`); see // `api/events-ws.ts`'s own doc comment for why this attaches here rather than inside `createApiServer` // (Express never sees an `upgrade` event, so this needs the actual `http.Server` `listen()` returned). const eventsWebSocketServer = attachEventsWebSocket(apiServer, (req, socket, head) => diff --git a/test/unit/config-ports.test.ts b/test/unit/config-ports.test.ts new file mode 100644 index 00000000..9ee68b7b --- /dev/null +++ b/test/unit/config-ports.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; + +import { portFromEnv } from '../../src/config.js'; + +describe('portFromEnv', () => { + it('falls back to the default when the variable is unset or blank', () => { + expect(portFromEnv('ACTOR_RUNTIME_API_PORT', 3333, {})).toBe(3333); + expect(portFromEnv('ACTOR_RUNTIME_API_PORT', 3333, { ACTOR_RUNTIME_API_PORT: ' ' })).toBe(3333); + }); + + it('reads a valid port', () => { + expect(portFromEnv('ACTOR_RUNTIME_API_PORT', 3333, { ACTOR_RUNTIME_API_PORT: ' 4333 ' })).toBe(4333); + }); + + it.each(['0', '65536', 'abc', '80.5', '-1', '3e3'])('rejects %s', (value) => { + expect(() => portFromEnv('ACTOR_RUNTIME_API_PORT', 3333, { ACTOR_RUNTIME_API_PORT: value })).toThrow( + /ACTOR_RUNTIME_API_PORT must be a TCP port/, + ); + }); +}); From 46b0f11036c81f353ab97ef85ad92061bc21ba6c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 08:18:22 +0000 Subject: [PATCH 2/3] docs: tighten the configurable-ports requirements Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ac6RaZAULDqhZMW7UhSeHD --- requirements/actor-driver.md | 2 +- requirements/console.md | 2 +- requirements/system.md | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/requirements/actor-driver.md b/requirements/actor-driver.md index 6d5a5eec..4617a9a2 100644 --- a/requirements/actor-driver.md +++ b/requirements/actor-driver.md @@ -165,7 +165,7 @@ start`, ...) is refused by name, naming both the `CMD` fix and how to clear debu # Networking -- Every Actor container reaches the runtime's API at `http://apify-api:` (default 3333, `system.md`), whatever the host's own +- Every Actor container reaches the runtime's API at `http://apify-api:`, whatever the host's own networking, whichever supported engine runs the containers, and however the runtime itself was started (as a container or not). The runtime provides the `apify-local` network with the DNS alias `apify-api` for this; when it has to reach the same goal another way, it says so at startup. diff --git a/requirements/console.md b/requirements/console.md index 3671cf3d..be3ce93d 100644 --- a/requirements/console.md +++ b/requirements/console.md @@ -1,7 +1,7 @@ # Frontend - Console frontend is a page that allows inspecting each user's objects across the whole runtime. -- Server-rendered HTML on its own port (default 3000, `system.md`); the console reflects the same live state the API serves. +- Server-rendered HTML on its own port (`system.md`); the console reflects the same live state the API serves. - Frontend shows for each object the owner (`userId`). - The console has no login of its own, so with multiple users it lists and shows every user's objects rather than scoping to one - the API's own endpoints stay strictly scoped to the calling token's user diff --git a/requirements/system.md b/requirements/system.md index 9ccd4587..10bade9b 100644 --- a/requirements/system.md +++ b/requirements/system.md @@ -19,14 +19,13 @@ - The system is isolated environment that is started by running the docker container. - The system user interface is accessible on localhost with specific ports for console frontend and API. - The user interacts with the system through the Apify cli. -- On startup the container prints a banner naming the API port (default 3333) and the console port (default 3000), +- On startup the container prints a banner naming the API port and the console port, plus a warning if the host's Docker socket could not be reached - builds and runs then fail fast with a clear status message, while every other endpoint (storages, actor/build/run records, console) still works. -- Both ports are configurable through the runtime's own environment: `ACTOR_RUNTIME_API_PORT` and - `ACTOR_RUNTIME_CONSOLE_PORT`. The runtime refuses to start when either is not a valid TCP port or both - are the same. Each is published on the same port number on the host (`-p N:N`); every URL the runtime - hands out (console links, standby URLs, browser view, the Actor containers' API URL) uses the configured ports. +- The API port (default 3333) and console port (default 3000) are set by `ACTOR_RUNTIME_API_PORT` and + `ACTOR_RUNTIME_CONSOLE_PORT`, published under the same numbers on the host. Every URL the runtime shows + uses them. An invalid or clashing port stops the runtime at startup with an error. - The API port also serves the per-run events websocket and standby Actors (`api.md`); no additional port is published for either. - **Debug mode is the one exception to "no other Actor container port is ever published"** @@ -40,7 +39,7 @@ and mount a persistent data directory (`-v :/data`, e.g. `-v "$(pwd)/data:/data"`) so storages survive a restart and are easy to inspect from the host; the directory must exist before the runtime starts. Publish both ports - (`-p 3333:3333 -p 3000:3000` for the defaults). The canonical start command is: + (`-p 3333:3333 -p 3000:3000`). The canonical start command is: ```bash docker build -t actor-runtime . From 60da63aff0595c25412cae9f7e3ace612a47ba26 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 08:25:51 +0000 Subject: [PATCH 3/3] docs: drop the port validation note from the requirements Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ac6RaZAULDqhZMW7UhSeHD --- requirements/system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/system.md b/requirements/system.md index 10bade9b..c3b3a0e9 100644 --- a/requirements/system.md +++ b/requirements/system.md @@ -25,7 +25,7 @@ console) still works. - The API port (default 3333) and console port (default 3000) are set by `ACTOR_RUNTIME_API_PORT` and `ACTOR_RUNTIME_CONSOLE_PORT`, published under the same numbers on the host. Every URL the runtime shows - uses them. An invalid or clashing port stops the runtime at startup with an error. + uses them. - The API port also serves the per-run events websocket and standby Actors (`api.md`); no additional port is published for either. - **Debug mode is the one exception to "no other Actor container port is ever published"**