Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,8 @@ DESCRIPTION
runtime image, 'apify runtime start' runs it, and 'apify runtime status' says
whether it is up.

The runtime publishes two ports on localhost:
The runtime publishes two ports on localhost, by default (move them with
'apify runtime start --api-port <port> --console-port <port>'):

3333 API http://localhost:3333 (Apify API compatible endpoint)
3000 Console http://localhost:3000 (web UI)
Expand All @@ -489,7 +490,7 @@ DESCRIPTION

These environment variables point the CLI (and the Apify SDKs and API clients
that honour them) at the runtime for one shell only, and take precedence over
the connection wherever they are set:
the connection wherever they are set (shown for the default ports):

export APIFY_CLIENT_BASE_URL=http://localhost:3333
export APIFY_CONSOLE_URL=http://localhost:3000
Expand Down Expand Up @@ -557,18 +558,25 @@ DESCRIPTION
Docker or Podman.
Installs the runtime first when needed (like 'apify runtime install'). The
runtime API listens on http://localhost:3333 and the console on
http://localhost:3000. Run 'apify runtime -h' for the environment variables
that point the CLI at it.
http://localhost:3000 unless moved with --api-port and --console-port, which
are remembered for later starts and for 'apify runtime connect'. Run 'apify
runtime -h' for the environment variables that point the CLI at it.

USAGE
$ apify runtime start [--data-dir <value>] [-d]
$ apify runtime start [--api-port <value>]
[--console-port <value>] [--data-dir <value>] [-d]

FLAGS
--data-dir=<value> Host directory mounted as the runtime
data directory (storages, builds and run records).
Defaults to ~/.apify/actor-runtime/data.
-d, --detach Run the runtime container in the
background. Stop it with 'apify runtime stop'.
--api-port=<value> Host port for the runtime API.
Defaults to the port of the previous start, else 3333.
--console-port=<value> Host port for the runtime
console. Defaults to the port of the previous start,
else 3000.
--data-dir=<value> Host directory mounted as the
runtime data directory (storages, builds and run
records). Defaults to ~/.apify/actor-runtime/data.
-d, --detach Run the runtime container
in the background. Stop it with 'apify runtime stop'.
```

##### `apify runtime stop`
Expand Down
4 changes: 2 additions & 2 deletions src/commands/runtime/_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ export class RuntimeIndexCommand extends ApifyCommand<typeof RuntimeIndexCommand
'',
`'apify runtime install' checks that the engine is available and pulls the runtime image, 'apify runtime start' runs it, and 'apify runtime status' says whether it is up.`,
'',
'The runtime publishes two ports on localhost:',
`The runtime publishes two ports on localhost, by default (move them with 'apify runtime start --api-port <port> --console-port <port>'):`,
'',
` ${String(ACTOR_RUNTIME_API_PORT).padEnd(5)} API ${ACTOR_RUNTIME_API_URL} (Apify API compatible endpoint)`,
` ${String(ACTOR_RUNTIME_CONSOLE_PORT).padEnd(5)} Console ${ACTOR_RUNTIME_CONSOLE_URL} (web UI)`,
'',
`Run 'apify runtime connect' to send every Apify CLI command to the runtime instead of the Apify cloud, and 'apify runtime disconnect' to go back. The connection is remembered across terminals and does not touch your login.`,
'',
'These environment variables point the CLI (and the Apify SDKs and API clients that honour them) at the runtime for one shell only, and take precedence over the connection wherever they are set:',
'These environment variables point the CLI (and the Apify SDKs and API clients that honour them) at the runtime for one shell only, and take precedence over the connection wherever they are set (shown for the default ports):',
'',
...runtimeEnvExportLines().map((line) => ` ${line}`),
'',
Expand Down
15 changes: 10 additions & 5 deletions src/commands/runtime/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import chalk from 'chalk';
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { simpleLog, success, warning } from '../../lib/outputs.js';
import {
ACTOR_RUNTIME_API_URL,
ACTOR_RUNTIME_CONSOLE_URL,
findRunningRuntimeEngine,
runtimeApiUrl,
runtimeConsoleUrl,
runtimeSkillHintLines,
} from '../../lib/runtime/docker.js';
import { overridingRuntimeEnvVars, setConnectedToActorRuntime } from '../../lib/runtime/target.js';
import {
configuredRuntimePorts,
overridingRuntimeEnvVars,
setConnectedToActorRuntime,
} from '../../lib/runtime/target.js';

export class RuntimeConnectCommand extends ApifyCommand<typeof RuntimeConnectCommand> {
static override name = 'connect' as const;
Expand All @@ -33,12 +37,13 @@ export class RuntimeConnectCommand extends ApifyCommand<typeof RuntimeConnectCom

async run() {
setConnectedToActorRuntime(true);
const ports = configuredRuntimePorts();

success({ message: 'The Apify CLI now targets the local Actor runtime.' });
simpleLog({
message: [
` API: ${ACTOR_RUNTIME_API_URL}`,
` Console: ${ACTOR_RUNTIME_CONSOLE_URL}`,
` API: ${runtimeApiUrl(ports.api)}`,
` Console: ${runtimeConsoleUrl(ports.console)}`,
'',
`Run ${chalk.white.bold('apify runtime disconnect')} to target the Apify platform again.`,
'',
Expand Down
48 changes: 42 additions & 6 deletions src/commands/runtime/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,25 @@ import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { Flags } from '../../lib/command-framework/flags.js';
import { GLOBAL_CONFIGS_FOLDER, INTERRUPT_SIGNALS } from '../../lib/consts.js';
import { error, info, run } from '../../lib/outputs.js';
import { updateActorRuntimeConfig } from '../../lib/runtime/config.js';
import {
ACTOR_RUNTIME_API_PORT,
ACTOR_RUNTIME_API_URL,
ACTOR_RUNTIME_CONSOLE_PORT,
ACTOR_RUNTIME_CONSOLE_URL,
ACTOR_RUNTIME_CONTAINER_NAME,
buildRuntimeRunArgs,
findRunningRuntimeEngine,
resolveEngineSocketPath,
runtimeApiUrl,
runtimeConsoleUrl,
runtimeEnvExportLines,
runtimeSkillHintLines,
} from '../../lib/runtime/docker.js';
import { ensureActorRuntimeImage, installedActorRuntimeImage } from '../../lib/runtime/ensure.js';
import { configuredRuntimePorts } from '../../lib/runtime/target.js';

const isValidPort = (port: number) => Number.isInteger(port) && port >= 1 && port <= 65535;

const defaultDataDir = () => join(GLOBAL_CONFIGS_FOLDER(), 'actor-runtime', 'data');

Expand All @@ -29,8 +37,9 @@ export class RuntimeStartCommand extends ApifyCommand<typeof RuntimeStartCommand
static override description =
`Starts the Actor runtime, a local Apify platform running as a container on Docker or Podman.\n` +
`Installs the runtime first when needed (like 'apify runtime install'). The runtime API listens on ` +
`${ACTOR_RUNTIME_API_URL} and the console on ${ACTOR_RUNTIME_CONSOLE_URL}. Run 'apify runtime -h' for the ` +
`environment variables that point the CLI at it.`;
`${ACTOR_RUNTIME_API_URL} and the console on ${ACTOR_RUNTIME_CONSOLE_URL} unless moved with --api-port and ` +
`--console-port, which are remembered for later starts and for 'apify runtime connect'. Run 'apify runtime -h' ` +
`for the environment variables that point the CLI at it.`;

static override group = 'Local Actor Development';

Expand All @@ -47,6 +56,10 @@ export class RuntimeStartCommand extends ApifyCommand<typeof RuntimeStartCommand
description: 'Start with runtime data stored in a custom directory.',
command: 'apify runtime start --data-dir ./data',
},
{
description: 'Start on other ports when 3333 or 3000 is already taken.',
command: 'apify runtime start --api-port 4333 --console-port 4000',
},
];

static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-runtime-start';
Expand All @@ -61,6 +74,12 @@ export class RuntimeStartCommand extends ApifyCommand<typeof RuntimeStartCommand
description: `Run the runtime container in the background. Stop it with 'apify runtime stop'.`,
default: false,
}),
'api-port': Flags.integer({
description: `Host port for the runtime API. Defaults to the port of the previous start, else ${ACTOR_RUNTIME_API_PORT}.`,
}),
'console-port': Flags.integer({
description: `Host port for the runtime console. Defaults to the port of the previous start, else ${ACTOR_RUNTIME_CONSOLE_PORT}.`,
}),
};

async run() {
Expand All @@ -72,30 +91,47 @@ export class RuntimeStartCommand extends ApifyCommand<typeof RuntimeStartCommand
return;
}

const previous = configuredRuntimePorts();
const ports = {
api: this.flags.apiPort ?? previous.api,
console: this.flags.consolePort ?? previous.console,
};
if (!isValidPort(ports.api) || !isValidPort(ports.console)) {
error({ message: 'Ports must be integers between 1 and 65535.' });
process.exitCode = 1;
return;
}
if (ports.api === ports.console) {
error({ message: `The API and console need different ports, both are ${ports.api}.` });
process.exitCode = 1;
return;
}

const image = installedActorRuntimeImage();
const engine = await ensureActorRuntimeImage({ image });
if (!engine) return;

const hostSocketPath = await resolveEngineSocketPath(engine);
const dataDir = resolve(this.flags.dataDir ?? defaultDataDir());
await mkdir(dataDir, { recursive: true });
updateActorRuntimeConfig({ apiPort: ports.api, consolePort: ports.console });

info({
message: [
`Starting the Actor runtime (data directory: ${dataDir})...`,
'',
` API: ${ACTOR_RUNTIME_API_URL}`,
` Console: ${ACTOR_RUNTIME_CONSOLE_URL}`,
` API: ${runtimeApiUrl(ports.api)}`,
` Console: ${runtimeConsoleUrl(ports.console)}`,
'',
'Point the Apify CLI at the runtime with:',
...runtimeEnvExportLines().map((line) => chalk.white.bold(` ${line}`)),
...runtimeEnvExportLines(ports).map((line) => chalk.white.bold(` ${line}`)),
'',
...runtimeSkillHintLines(),
].join('\n'),
});

// Spawned without a shell so interrupt signals reach the engine's 'run' directly instead of dying in 'sh -c'.
const args = buildRuntimeRunArgs({ image, dataDir, detach: this.flags.detach, hostSocketPath });
const args = buildRuntimeRunArgs({ image, dataDir, detach: this.flags.detach, hostSocketPath, ports });
run({ message: `${engine} ${args.join(' ')}` });

const child = execa(engine, args, { stdio: 'inherit' });
Expand Down
23 changes: 14 additions & 9 deletions src/commands/runtime/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,30 @@ import chalk from 'chalk';
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { simpleLog } from '../../lib/outputs.js';
import {
ACTOR_RUNTIME_API_PORT,
ACTOR_RUNTIME_CONSOLE_PORT,
ACTOR_RUNTIME_CONTAINER_NAME,
findRunningRuntimeEngine,
inspectRuntimeContainer,
type PublishedPort,
type RuntimePorts,
runtimeSkillHintLines,
} from '../../lib/runtime/docker.js';
import { installedActorRuntimeImage } from '../../lib/runtime/ensure.js';
import { isConnectedToActorRuntime, overridingRuntimeEnvVars, resolveApiBaseUrl } from '../../lib/runtime/target.js';
import {
configuredRuntimePorts,
isConnectedToActorRuntime,
overridingRuntimeEnvVars,
resolveApiBaseUrl,
} from '../../lib/runtime/target.js';
import { printJsonToStdout } from '../../lib/utils.js';

function portRole(containerPort: number): string {
if (containerPort === ACTOR_RUNTIME_API_PORT) return 'API';
if (containerPort === ACTOR_RUNTIME_CONSOLE_PORT) return 'Console';
function portRole(containerPort: number, ports: RuntimePorts): string {
if (containerPort === ports.api) return 'API';
if (containerPort === ports.console) return 'Console';
return '';
}

function portLine({ containerPort, protocol, hostAddress }: PublishedPort): string {
const role = portRole(containerPort);
function portLine({ containerPort, protocol, hostAddress }: PublishedPort, ports: RuntimePorts): string {
const role = portRole(containerPort, ports);
return ` ${`${containerPort}/${protocol}`.padEnd(10)} -> ${hostAddress}${role ? ` (${role})` : ''}`;
}

Expand Down Expand Up @@ -101,7 +105,8 @@ export class RuntimeStatusCommand extends ApifyCommand<typeof RuntimeStatusComma
}

lines.push('', container?.ports.length ? 'Published ports:' : 'Published ports: none');
lines.push(...(container?.ports ?? []).map(portLine));
const ports = configuredRuntimePorts();
lines.push(...(container?.ports ?? []).map((port) => portLine(port, ports)));
}

lines.push('', 'Apify CLI target:');
Expand Down
3 changes: 3 additions & 0 deletions src/lib/runtime/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export interface ActorRuntimeConfig {
image?: string;
/** Whether `apify runtime connect` pointed the CLI at the runtime. */
connected?: boolean;
/** The ports the last `apify runtime start` published. */
apiPort?: number;
consolePort?: number;
}

export function readActorRuntimeConfig(): ActorRuntimeConfig {
Expand Down
46 changes: 40 additions & 6 deletions src/lib/runtime/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,33 @@ export const DOCKER_ENGINE_INSTALL_URL = 'https://docs.docker.com/engine/install
/** Official Podman documentation: installation on every platform. */
export const PODMAN_INSTALL_URL = 'https://podman.io/docs/installation';

/** Default ports; 'apify runtime start --api-port/--console-port' moves them. */
export const ACTOR_RUNTIME_API_PORT = 3333;

export const ACTOR_RUNTIME_CONSOLE_PORT = 3000;

export const ACTOR_RUNTIME_API_URL = `http://localhost:${ACTOR_RUNTIME_API_PORT}`;
/** The runtime's own environment variables that move its ports; the same number is published on the host. */
export const RUNTIME_API_PORT_ENV_VAR = 'ACTOR_RUNTIME_API_PORT';

export const ACTOR_RUNTIME_CONSOLE_URL = `http://localhost:${ACTOR_RUNTIME_CONSOLE_PORT}`;
export const RUNTIME_CONSOLE_PORT_ENV_VAR = 'ACTOR_RUNTIME_CONSOLE_PORT';

export interface RuntimePorts {
api: number;
console: number;
}

export const DEFAULT_RUNTIME_PORTS: RuntimePorts = {
api: ACTOR_RUNTIME_API_PORT,
console: ACTOR_RUNTIME_CONSOLE_PORT,
};

export const runtimeApiUrl = (port: number) => `http://localhost:${port}`;

export const runtimeConsoleUrl = (port: number) => `http://localhost:${port}`;

export const ACTOR_RUNTIME_API_URL = runtimeApiUrl(ACTOR_RUNTIME_API_PORT);

export const ACTOR_RUNTIME_CONSOLE_URL = runtimeConsoleUrl(ACTOR_RUNTIME_CONSOLE_PORT);

/**
* Environment variables that point the Apify CLI (and the Apify SDKs/clients that honour them)
Expand All @@ -47,8 +67,11 @@ export const RUNTIME_SOCKET_PATH = '/var/run/docker.sock';
/** Where the runtime container expects its data directory (storages, builds and run records). */
export const RUNTIME_DATA_PATH = '/data';

export function runtimeEnvExportLines(): string[] {
return Object.entries(ACTOR_RUNTIME_ENV_VARS).map(([name, value]) => `export ${name}=${value}`);
export function runtimeEnvExportLines(ports: RuntimePorts = DEFAULT_RUNTIME_PORTS): string[] {
return [
`export APIFY_CLIENT_BASE_URL=${runtimeApiUrl(ports.api)}`,
`export APIFY_CONSOLE_URL=${runtimeConsoleUrl(ports.console)}`,
];
}

/** The engine the user asked for via `APIFY_CONTAINER_ENGINE`, or undefined for "whichever is installed". */
Expand Down Expand Up @@ -298,6 +321,7 @@ export interface RuntimeRunArgsOptions {
dataDir: string;
detach: boolean;
hostSocketPath: string;
ports?: RuntimePorts;
platform?: NodeJS.Platform;
}

Expand All @@ -306,6 +330,7 @@ export function buildRuntimeRunArgs({
dataDir,
detach,
hostSocketPath,
ports = DEFAULT_RUNTIME_PORTS,
platform = process.platform,
}: RuntimeRunArgsOptions): string[] {
// --init makes signals (Ctrl+C) reach the runtime process even though it runs as the container's PID 1.
Expand All @@ -315,11 +340,20 @@ export function buildRuntimeRunArgs({
args.push('--detach');
}

// Same number inside and out: the runtime builds its URLs, and Actors may reach its API through the
// host, from the port it listens on. Defaults are left implicit so older runtime images keep working.
if (ports.api !== ACTOR_RUNTIME_API_PORT) {
args.push('-e', `${RUNTIME_API_PORT_ENV_VAR}=${ports.api}`);
}
if (ports.console !== ACTOR_RUNTIME_CONSOLE_PORT) {
args.push('-e', `${RUNTIME_CONSOLE_PORT_ENV_VAR}=${ports.console}`);
}

args.push(
'-p',
`${ACTOR_RUNTIME_API_PORT}:${ACTOR_RUNTIME_API_PORT}`,
`${ports.api}:${ports.api}`,
'-p',
`${ACTOR_RUNTIME_CONSOLE_PORT}:${ACTOR_RUNTIME_CONSOLE_PORT}`,
`${ports.console}:${ports.console}`,
'-v',
socketMountArg(hostSocketPath, platform),
'-v',
Expand Down
6 changes: 3 additions & 3 deletions src/lib/runtime/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { execa } from 'execa';

import { APIFY_CLIENT_DEFAULT_HEADERS } from '../consts.js';
import { userHomeDir } from '../utils.js';
import { ACTOR_RUNTIME_API_URL, findRunningRuntimeEngine, imageExistsLocally, type ContainerEngine } from './docker.js';
import { findRunningRuntimeEngine, imageExistsLocally, runtimeApiUrl, type ContainerEngine } from './docker.js';
import { installedActorRuntimeImage } from './ensure.js';
import { resolveApiBaseUrl } from './target.js';
import { configuredRuntimePorts, resolveApiBaseUrl } from './target.js';

/** Matches the `name` in the file's own frontmatter. */
export const RUNTIME_SKILL_NAME = 'apify-actor-runtime';
Expand Down Expand Up @@ -70,7 +70,7 @@ async function readSkillFromImage(engine: ContainerEngine, image: string): Promi
/** Cheapest-first: HTTP, then the installed image. Deliberately no copy bundled with the CLI - one could
* only ever describe the image the CLI was released against, not the one present. */
export async function resolveRuntimeSkill(): Promise<ResolvedSkill | null> {
const baseUrl = resolveApiBaseUrl() ?? ACTOR_RUNTIME_API_URL;
const baseUrl = resolveApiBaseUrl() ?? runtimeApiUrl(configuredRuntimePorts().api);

const overHttp = await fetchSkillFromRuntime(baseUrl);
if (overHttp) return { content: overHttp, source: { kind: 'runtime', baseUrl } };
Expand Down
Loading