Skip to content

Commit 9cff3e7

Browse files
committed
fix: recover the local stack after a port clash
Docker Engine 29.2 leaves a container whose port could not be bound without its network. Every later start of that container has loopback only, even once the port is free. Only a new container recovers. For the Local course that meant: - Lab 0 step 2: after `Bind for 0.0.0.0:8080 failed`, running local/engine.sh again (what --check says to do) failed with `registry-1 exited (1)`, and stopping the other program did not help. - Lab 0 step 1: after stopping the other program, `up` exited 0 and the step's check listed six services, but the container had no published port. local/engine.sh now removes the engine's containers that are not running before `ork local start`. The engine's data is in volumes. A rerun gives the same bind error while the port is taken, and a working engine once it is free. Troubleshooting gives a recovery that works for both stacks, covers a doctor that cannot reach a container that lost its network, and has a row for a missing python/.venv on the CLI path. Lab 0 steps 1 and 2 point to it.
1 parent 2a5808f commit 9cff3e7

5 files changed

Lines changed: 93 additions & 7 deletions

File tree

‎labs/local/00-set-up.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ The first run downloads about 4.4 GB of images. It starts six services
4141
- `risingwave`: streaming SQL.
4242
- `risingwave-mcp`: RisingWave's MCP server, the agent's SQL tools.
4343

44+
If it fails with a port already in use, see
45+
[Troubleshooting](troubleshooting.md) before you run it again.
46+
4447
### Check
4548

4649
The six services are running. Before the step this prints nothing.
@@ -84,6 +87,9 @@ header:
8487
Run `local/engine.sh` again whenever the engine has been restarted. It is safe
8588
to run at any time.
8689

90+
If it stops with `port is already allocated`, another program has a port the
91+
engine needs, usually 8080: see [Troubleshooting](troubleshooting.md).
92+
8793
### Check
8894

8995
The engine is up, has a provider key, and can reach the MCP server. The script

‎labs/local/troubleshooting.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ Each failed line prints its fix.
1313

1414
| Symptom | Fix |
1515
|---|---|
16-
| `docker compose ... up` fails with a port already in use | Another program has one of the ports the stack publishes on `127.0.0.1`: 29092 (Kafka), 18081 (schema registry), 4566 and 5691 (RisingWave), 8000 (MCP). Stop that program. The ports are fixed: the broker tells its clients to come back to `127.0.0.1:29092`, and `local/write-env.sh` writes these ports into `.env`. |
17-
| `local/engine.sh` fails because port 8080 is taken (`Bind for 0.0.0.0:8080 failed: port is already allocated`) | Pick another port for the registry: `export ORCA_LOCAL_REGISTRY_PORT=18080`, run `local/engine.sh` again, then `local/write-env.sh` so `.env` has the new address. Export it in every terminal you run `local/engine.sh` from: a run without it goes back to 8080. |
16+
| `docker compose ... up` fails with a port already in use | Another program has one of the ports the stack publishes on `127.0.0.1`: 29092 (Kafka), 18081 (schema registry), 4566 and 5691 (RisingWave), 8000 (MCP). Stop that program, run `local/down.sh`, then run the `up` command again. Running it again without `local/down.sh` is not enough: Docker brings the container whose port was taken back without its network. The ports are fixed: the broker tells its clients to come back to `127.0.0.1:29092`, and `local/write-env.sh` writes these ports into `.env`. |
17+
| `local/engine.sh` fails because a port is taken (`Bind for 0.0.0.0:8080 failed: port is already allocated`) | Another program has a port the engine publishes on `127.0.0.1`: 8080 (the registry) or 18082. If it is a container, `docker ps` shows which: look for `:8080->` under PORTS. Stop that program and run `local/engine.sh` again. If the port is 8080 and you want to keep that program running, move the registry instead: `export ORCA_LOCAL_REGISTRY_PORT=18080`, run `local/engine.sh` again, then `local/write-env.sh` so `.env` has the new address. Export it in every terminal you run `local/engine.sh` from: a run without it goes back to 8080. |
1818
| `local/engine.sh`: `ANTHROPIC_API_KEY is not set in this shell` | `export ANTHROPIC_API_KEY=<your key>` in the terminal where you run the script. The engine reads the key only when it starts. |
1919
| `local/engine.sh`: `bootstrap refused: an organization already exists` | The engine's volumes exist but its keys in `.lab/ork` are gone. Start over: `local/down.sh --reset`, then Lab 0. |
20+
| CLI path: `.venv/bin/python: No such file or directory` | The Python path is not installed. If you installed the TypeScript path, use the `npm` command the lab gives beside the Python one. Otherwise install one of the two: Lab 0, "Before you start". |
2021
| Doctor: `Agent Engine HTTP 401` | The key in `.env` is not the running engine's key. Run `local/write-env.sh`. If it still fails, the engine's volumes and keys are out of step: `local/down.sh --reset`, then Lab 0. |
2122
| Doctor: `Kafka ... not found` | The topic does not exist yet: Lab 0, step 4. |
2223
| Doctor: `Schema Registry ... not found` | The schema is registered when you seed the topic: `python seed.py` or `npm run seed`. |
23-
| Doctor: `Kafka`, `Schema Registry`, or `MCP server` cannot be reached, or `RisingWave through MCP` fails | The streaming stack is not up: `docker compose -f local/compose.yaml up -d --wait`. |
24+
| Doctor: `Kafka`, `Schema Registry`, or `MCP server` cannot be reached, or `RisingWave through MCP` fails | The streaming stack is not up: `docker compose -f local/compose.yaml up -d --wait`. If it is up and the doctor still says so, a container lost its network when its port was taken: run `local/down.sh`, then start both stacks again. |
2425
| Doctor: `Agent Engine Connection error` | The engine is not up: `local/engine.sh`. Start the streaming stack first. |
2526
| `seed`: `already holds 246 events` | The topic is seeded. Nothing to do. |
2627
| `table or source not found: security.login_events` | Create the source: Lab 2, step 1. |

‎local/engine.sh‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
# 1. ork local --data-dir <this checkout>/.lab/ork start --with-gateway
1212
# The Agent Engine, with the AI Gateway. The gateway makes every MCP call on
1313
# your agent's behalf, so MCP tools need it. The data directory is given as a
14-
# full path: ork v0.6.0 does not resolve a relative one.
14+
# full path: ork v0.6.0 does not resolve a relative one. First, the engine's
15+
# containers that are not running are removed, so that it starts from fresh
16+
# ones. Its data is in volumes.
1517
#
1618
# 2. The link. The gateway refuses private MCP hosts unless they are on its
1719
# allowlist, and `ork local start` writes that allowlist empty every time.
@@ -44,6 +46,14 @@ start_engine() {
4446
[ -n "$(mcp_container)" ] ||
4547
die "The streaming stack is not running. Start it first:
4648
docker compose -f local/compose.yaml up -d --wait"
49+
# Replace what is not running. A container whose port could not be bound
50+
# (another program had it) stays cut off from its network: Docker starts it
51+
# with loopback only from then on, even once the port is free (seen with
52+
# Docker Engine 29.2). The engine's data is in volumes, so nothing is lost.
53+
local name
54+
while IFS= read -r name; do
55+
[ -z "$name" ] || docker rm "$name" >/dev/null
56+
done < <(engine_stopped_containers)
4757
ork local --data-dir "$ORK_DIR" start --with-gateway
4858
}
4959

‎local/lib.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ engine_container() { # engine_container <service>
4444
--filter "label=com.docker.compose.service=$1" --format '{{.Names}}' | head -n 1
4545
}
4646

47+
# The Agent Engine's containers that exist but are not running.
48+
engine_stopped_containers() {
49+
docker ps -a --filter "label=com.docker.compose.project.working_dir=$ORK_DIR" \
50+
--filter status=created --filter status=exited --filter status=dead --format '{{.Names}}'
51+
}
52+
4753
mcp_container() {
4854
docker ps --filter "label=com.docker.compose.project=$STREAMING_PROJECT" \
4955
--filter "label=com.docker.compose.service=risingwave-mcp" --format '{{.Names}}' | head -n 1

‎local/tests/run.sh‎

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,53 @@ set -euo pipefail
1010
TESTS=$(cd "$(dirname "$0")" && pwd)
1111
LOCAL=$(dirname "$TESTS")
1212
WORK=$(mktemp -d "${TMPDIR:-/tmp}/hello-local-tests.XXXXXX")
13+
WORK=$(cd "$WORK" && pwd) # as the scripts see it: a TMPDIR ending in "/" leaves a "//"
1314
trap 'rm -rf "$WORK"' EXIT
1415

15-
# A `docker` that knows which host port the registry is published on, and
16-
# remembers what it was asked.
16+
# A `docker` that knows which host port the registry is published on, which
17+
# containers exist, and remembers what it was asked.
1718
mkdir -p "$WORK/bin"
1819
cat >"$WORK/bin/docker" <<'EOF'
1920
#!/usr/bin/env bash
2021
[ -n "${FAKE_DOCKER_DIR:-}" ] || exit 1
2122
printf '%s\n' "$*" >>"$FAKE_DOCKER_DIR/calls"
2223
case "$1" in
23-
ps) [ -f "$FAKE_DOCKER_DIR/registry-port" ] && echo registry-1 ;;
24+
ps)
25+
case " $* " in
26+
*" -a "*)
27+
# Every container, running or not: the "<status> <name>" lines of the
28+
# fixture, when the question is narrowed to the engine's project. A
29+
# status filter keeps the lines with that status.
30+
if [[ " $* " != *" label=com.docker.compose.project.working_dir=${FAKE_DOCKER_DIR%/*}/.lab/ork "* ]]; then
31+
echo someone-elses-container
32+
elif [ -f "$FAKE_DOCKER_DIR/containers" ]; then
33+
while read -r status name; do
34+
[[ " $* " == *" status="* && " $* " != *" status=$status "* ]] || echo "$name"
35+
done <"$FAKE_DOCKER_DIR/containers"
36+
fi
37+
;;
38+
*) [ -f "$FAKE_DOCKER_DIR/registry-port" ] && echo registry-1 ;;
39+
esac
40+
;;
2441
port)
2542
[ ! -f "$FAKE_DOCKER_DIR/port-fails" ] || exit 1
2643
[ -f "$FAKE_DOCKER_DIR/registry-port" ] && echo "127.0.0.1:$(cat "$FAKE_DOCKER_DIR/registry-port")"
2744
;;
45+
rm) [ $# -ge 2 ] || exit 1 ;; # like docker, it wants at least one container
2846
compose) ;;
2947
*) exit 1 ;;
3048
esac
3149
EOF
3250
chmod +x "$WORK/bin/docker"
51+
# An `ork` that remembers the call and stops the script there: what comes after
52+
# a start needs a running stack.
53+
cat >"$WORK/bin/ork" <<'EOF'
54+
#!/usr/bin/env bash
55+
[ -n "${FAKE_DOCKER_DIR:-}" ] || exit 1
56+
printf 'ork %s\n' "$*" >>"$FAKE_DOCKER_DIR/calls"
57+
exit 1
58+
EOF
59+
chmod +x "$WORK/bin/ork"
3360
export PATH="$WORK/bin:$PATH"
3461
# Nothing from the developer's own shell may leak into the tests.
3562
unset ORCA_LOCAL_REGISTRY_PORT TUTORIAL_STACK PARTICIPANT ORCA_MODEL ORCA_API_KEY
@@ -63,6 +90,7 @@ run() { # run <script> [args...]
6390
env_is() { [ "$(sed -n "s/^$1=//p" "$R/.env")" = "$2" ]; }
6491
out_has() { grep -qF -- "$1" "$R/out"; }
6592
err_has() { grep -qF -- "$1" "$R/err"; }
93+
not() { ! "$@"; }
6694

6795
check() { # check <description> <command...>
6896
if "${@:2}"; then
@@ -166,6 +194,41 @@ test_write_env_refreshes_a_local_env_and_keeps_your_choices() {
166194
check "keeps your model" env_is ORCA_MODEL claude-haiku-4-5
167195
}
168196

197+
# ----------------------------------------------------- starting the engine --
198+
199+
start_engine() { # local/engine.sh, as far as `ork local start`
200+
ANTHROPIC_API_KEY=not-a-real-key run engine.sh
201+
}
202+
203+
started() { grep -qE '^ork local .* start --with-gateway$' "$FAKE_DOCKER_DIR/calls"; }
204+
removed() { grep -qE "^rm( .*)? $1( |\$)" "$FAKE_DOCKER_DIR/calls"; }
205+
removed_before_the_start() { # removed_before_the_start <container>
206+
sed '/^ork /,$d' "$FAKE_DOCKER_DIR/calls" | grep -qE "^rm( .*)? $1( |\$)"
207+
}
208+
209+
test_engine_replaces_its_containers_that_are_not_running() {
210+
# A container whose port could not be bound comes back without its network,
211+
# even once the port is free (Docker Engine 29.2). A start must not reuse it.
212+
fresh_repo engine-stopped
213+
engine_started
214+
printf '%s\n' 'created ork-registry-1' 'exited ork-migrate-1' 'running ork-harness-1' >"$FAKE_DOCKER_DIR/containers"
215+
start_engine
216+
check "removes a container that never started, before the engine starts" removed_before_the_start ork-registry-1
217+
check "removes a container that has stopped, before the engine starts" removed_before_the_start ork-migrate-1
218+
check "leaves a running container alone" not removed ork-harness-1
219+
check "leaves other projects' containers alone" not removed someone-elses-container
220+
check "then starts the engine" started
221+
}
222+
223+
test_engine_starts_when_nothing_has_stopped() {
224+
fresh_repo engine-running
225+
engine_started
226+
printf '%s\n' 'running ork-registry-1' >"$FAKE_DOCKER_DIR/containers"
227+
start_engine
228+
check "asks docker to remove nothing" not grep -q '^rm' "$FAKE_DOCKER_DIR/calls"
229+
check "and starts the engine" started
230+
}
231+
169232
# ------------------------------------------------------- the gateway patch --
170233

171234
gateway_yaml() { # the two lines of `ork local`'s gateway.yaml that matter here

0 commit comments

Comments
 (0)