diff --git a/README.md b/README.md index d5da41b..7e88562 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,19 @@ npx skills add gitea@livebook:images/skills.git ``` -只安装指定 skill: +按语言选择安装(中文版 `runtime-image` / 英文版 `runtime-image-en`,同一能力的两个语言版本,选一个即可): ```bash npx skills add gitea@livebook:images/skills.git --skill runtime-image +npx skills add gitea@livebook:images/skills.git --skill runtime-image-en ``` ## Skills 列表 -| Skill | 说明 | -| --- | --- | -| [runtime-image](skills/runtime-image/SKILL.md) | 探测项目技术栈,按 runtime-base 镜像矩阵选型并生成/修正 Dockerfile | +| Skill | 语言 | 说明 | +| --- | --- | --- | +| [runtime-image](skills/runtime-image/SKILL.md) | 中文 | 探测项目技术栈,按 runtime-base 镜像矩阵选型并生成/修正 Dockerfile | +| [runtime-image-en](skills/runtime-image-en/SKILL.md) | English | English version of runtime-image: detect the tech stack, pick a base image from the runtime-base matrix, generate/audit the Dockerfile | ## 目录结构 @@ -28,10 +30,12 @@ npx skills add gitea@livebook:images/skills.git --skill runtime-image ``` skills/ -└── runtime-image/ - ├── SKILL.md # skill 主体(必需) - ├── references/ # 补充参考文档 - └── scripts/ # 辅助脚本 +├── runtime-image/ # 中文版 +│ ├── SKILL.md # skill 主体(必需) +│ ├── references/ # 补充参考文档 +│ └── scripts/ # 辅助脚本 +└── runtime-image-en/ # English version + └── ... ``` ## License diff --git a/skills/runtime-image-en/SKILL.md b/skills/runtime-image-en/SKILL.md new file mode 100644 index 0000000..79d4bb2 --- /dev/null +++ b/skills/runtime-image-en/SKILL.md @@ -0,0 +1,193 @@ +--- +name: runtime-image-en +description: Detects a project's tech stack and picks the correct base runtime image from the runtime-base image matrix, generating or fixing the project Dockerfile; can also audit an existing Dockerfile and repair it. Use whenever the user asks to "write a Dockerfile / containerize this project / pick a base image / which FROM image / package this into an image / deploy this project as a container / review my Dockerfile / optimize / audit / slim the image", or when symptoms appear such as container won't start / exec format error / image too large / pip compile failures / latest tag behavior changed, or when runtime-node / runtime-pm2 / runtime-base / livebook:8418 / code.innev.cn / registry mirror are mentioned — even if the user never says "runtime image". Boundaries: building/pushing app images belongs to project CI or xgent-image-push; modifying the runtime images themselves (adding images, changing versions) belongs to the runtime-base repo, not this skill. English version of the runtime-image skill. +--- + +# runtime-image — Pick a runtime image by project language and environment + +Based on the runtime-base repo's image matrix (10 images: 7 Alpine-based Linux + 3 Wine), complete "detect stack → select image → write Dockerfile → verify on a real machine" inside the project. Two working modes: + +- **Create mode**: the project has no Dockerfile; select from scratch and generate one +- **Audit-and-fix mode**: the project already has a Dockerfile (or the user asks to "look at this Dockerfile"); audit first, then fix + +Core idea (the reasoning chain every constraint hangs on): **the runtime series is only responsible for running already-compiled code** — building/compiling is done by the project's own CI; images contain no compilers/SDKs (the only exception is the Node family: install+build happens at container start, with the node-gyp toolchain included by design). Therefore a project Dockerfile is always a single stage: `FROM runtime-` → `COPY artifacts` → `CMD`. + +## Overall flow + +``` +Pre-step registry detection (script auto-detects internal/external network) +→ Step 0 detect tech stack (also detect existing Dockerfile → switch to audit-and-fix mode if found) +→ Step 1 selection table → Step 2 pin the tag +→ Step 3 write/modify Dockerfile → Step 4 verify on a real machine → Step 5 report +``` + +## Pre-step: registry detection (do this FIRST on every task, never skip) + +There are two registry sources, **decided by detection, never by guesswork**: + +| Source | Prefix | When | Condition | +|---|---|---|---| +| Internal | `livebook:8418/images` | machines on the LAN; fastest pulls | ① daemon.json already lists `insecure-registries: ["livebook:8418"]` (plain HTTP; docker refuses to pull without it) ② `http://livebook:8418/v2/` reachable (200 or 401 both count) | +| External | `code.innev.cn/images` | external machines / distribution to colleagues | fallback when either internal condition fails | + +Run `scripts/detect-registry.sh`; the last stdout line is the image prefix. Every Dockerfile's `FROM` is then written as `/runtime-:`. If both routes fail (exit 1) → die: stop and report troubleshooting directions (internal machine: confirm same subnet + registration + docker restart; external machine: confirm `https://code.innev.cn` is reachable). Do not hardcode either one. + +→ Acceptance: the image prefix used in this task is backed by a detection conclusion (internal/external), recorded in the Step 5 report. + +## Step 0: detect the tech stack (never skip) + +Never pick an image from the user's one-liner — confirm with project files first, then listen to the user's deployment requirements. + +| What you detect | Verdict | +|---|---| +| `package.json` (no build artifacts, not a pure static site) | Node backend → then ask: single process, or multi-process/cluster needed? | +| `package.json` + pure frontend (react/vue/vite static output) | **Not for this series** — CI produces static files hosted by the project's own nginx image | +| `requirements.txt` / `pyproject.toml` | Python | +| `go.mod` / `Cargo.toml` | Go/Rust → static binary → `runtime-base` | +| `pom.xml` / `build.gradle` | Java → jar | +| `*.csproj` / `*.sln` | .NET → **must ask one more question**: modern .NET (publish output) or Framework 4.x? They use completely different images | +| `CMakeLists` / `Makefile` (C/C++) | C/C++ → **must confirm musl dynamic linking** (constraint ⑤) | +| MQL4 / MT4 terminal requirement | `runtime-mt4` | +| Scripts / ops tooling | `runtime-base` | + +**Also detect existing Dockerfiles**: glob all `Dockerfile*` in the project (including subdirectories and `.dockerignore`). If any exist, switch to "audit-and-fix mode" (after Step 2); otherwise continue in create mode. Audit each Dockerfile when there are several. + +→ Acceptance: the verdict has file evidence (paths); anything uncertain is asked about before selection, never guessed; there is an explicit conclusion on whether a Dockerfile already exists. + +## Step 1: selection table + +| Project type | FROM | What the project provides | +|---|---|---| +| Node backend (single process) | `runtime-node` | source code (**without** node_modules); install+build at startup | +| Node backend (multi-process) | `runtime-pm2` | same + pm2 cluster (defaults to max cores) | +| Python | `runtime-python` | source + `venv/` (dependencies installed by CI) | +| Go / Rust static binary | `runtime-base` | compiled binary | +| C/C++ (musl dynamic linking) | `runtime-cpp` | compiled binary | +| Java jar | `runtime-java` | `target/*.jar` | +| Modern .NET publish output | `runtime-dotnet` | `dotnet publish` output directory | +| .NET Framework 4.x | `runtime-wine-dotnet` | `bin/Release/` directory | +| MT4 terminal | `runtime-mt4` | no COPY; mount a data directory at runtime | +| Scripts / tools | `runtime-base` | script files | + +Full image name: `/runtime-:` — the prefix comes from the pre-step detection (internal `livebook:8418/images` / external `code.innev.cn/images`), and can be explicitly overridden by `INNEV_IMAGE_PREFIX` (CI scenarios). For matrix details (preinstalled content / environment variables / Wine notes) read `references/runtime-matrix.md` on demand. + +die: the detected stack is outside the matrix (e.g. Ruby/PHP) → stop and report "no matching runtime image", giving the two options (project builds its own full image / add a new image in the runtime-base repo). **Do not force a near-fit image.** + +→ Acceptance: the chosen image has an explicit row in the table, and every special precondition of that row has been confirmed (musl vs glibc / modern .NET vs 4.x / single vs multi-process). + +## Step 2: pin the tag + +Three tag classes: `latest` (rolling rebuild with upstream every Monday) / `short-sha` / `version tag` (e.g. v0.5.0). + +- Production/delivery projects → pin a `version tag` or `short-sha` (warn: if the user insists on latest, remind them of the rolling-rebuild risk; do not block) +- Local trial runs → latest is acceptable + +→ Acceptance: the FROM tag in the Dockerfile is the result of an explicit choice, not something typed in passing. + +## Audit-and-fix mode (take this branch when Step 0 finds an existing Dockerfile) + +Finish Steps 1 and 2 first to establish "what it should look like" (correct image + tag), then audit the current state item by item. **Read-only audit: change no files until the audit report is out.** + +### Audit checklist (each item carries a hard-constraint number; severity defined below) + +| # | Check | Violates | Severity | +|---|---|---|---| +| A1 | Multi-stage build / SDK installed / compiling inside the image (`RUN mvn package`, `dotnet build`, `pip install` from source) | ① | must-fix | +| A2 | Node family COPYs node_modules / missing `.dockerignore` exclusion / COPYs dist artifacts as a generic pattern (the Node model is install+build at startup) | ② | must-fix | +| A3 | Token passed via `ARG` / secrets COPYed into layers / `.npmrc` with a token landing in image layers | ③ | must-fix (security, handle first) | +| A4 | `FROM ...:latest` in a production/delivery project | ④ | should-fix | +| A5 | C/C++ artifacts built in a glibc environment (debian/ubuntu CI) FROM an Alpine-based image | ⑤ | must-fix | +| A6 | Go/Rust binary FROM a full `golang`/`rust` image or another language-bearing image | ⑥ | should-fix (switch to `runtime-base`; size often drops by an order of magnitude) | +| A7 | Frontend static output served by a node image with `serve`/`http-server` | ⑦ | should-fix (switch to nginx hosting) | +| A8 | Stack/FROM mismatch (Framework 4.x on `runtime-dotnet`, multi-process need running single-process on `runtime-node`, etc.) | table | must-fix | +| A9 | Missing `CMD`/`ENTRYPOINT`, `EXPOSE`; entry path doesn't match the CI artifact path | — | note | +| A10 | FROM prefix contradicts the pre-step detection (external machine FROM `livebook:8418` can't pull; internal machine FROM `code.innev.cn` takes a pointless detour) | ⑧ | must-fix | + +Severity definitions: **must-fix** = will break (won't start / wrong behavior) or leaks secrets; **should-fix** = runs but size/stability/maintainability suffers; **note** = blemish. If only "should-fix/note" items are found and the user asked for an audit only, change no files — the report is the deliverable. + +### Audit report format (fixed) + +- One-line conclusion: currently FROM what → should be FROM what (if identical, say plainly "selection is correct") +- Problem table: severity | line number | problem | basis (constraint number) | fix +- Items with no problems also get a "passed" line (go through A1–A9 one by one; reporting only the bad and never the good is forbidden — omission is as harmful as false positives) + +Lay out the report; once the user confirms fixes → go to Step 3 and rewrite per the template (show the diff), then Step 4 verification. If the user says "audit only, don't touch" → the report is the deliverable. + +## Step 3: write/modify the Dockerfile + +Default action: **lay out the selection conclusion plus the full Dockerfile text for user confirmation before writing to disk** (show a diff when modifying an existing Dockerfile). + +Template (generic pattern; python/java/dotnet/cpp/base/wine are isomorphic): + +```dockerfile +FROM livebook:8418/images/runtime-: +COPY +CMD [...] +``` + +Node family pattern (note the difference from the generic pattern — COPY source, not artifacts): + +```dockerfile +FROM livebook:8418/images/runtime-node: +COPY . . # .dockerignore must exclude node_modules +``` + +Node-family runtime parameters are injected via environment variables at `docker run` time (`APP_ENTRY`/`BUILD_CMD`/`NPM_REGISTRY`/`PM2_APP_NAME`/`PM2_INSTANCES`, etc.; full table in references) — **never baked into image layers**. If an existing Dockerfile uses multi-stage builds / installs SDKs / passes tokens via ARG, call it out per the red lines and rewrite to a single stage. + +→ Acceptance: Dockerfile is single-stage, has no compile steps, contains no secrets; Node family comes with a `.dockerignore` excluding node_modules. + +## Step 4: verify on a real machine + +Done means actually building and smoke-running a container, not "the Dockerfile syntax is fine": + +1. `docker build -t :test .` succeeds +2. `docker run` starts a container and the process stays alive (Node family install+builds on first start — slow startup is normal; watch the logs until listening) +3. If it has a port, verify the port is reachable / the health check passes + +If the environment doesn't allow build verification (host hasn't registered `livebook:8418` as an insecure registry, no docker) → explicitly declare "not verified" and list "left for manual follow-up" in the report. + +## Step 5: report + +Fixed structure: +- Mode: create / audit-and-fix +- Registry: internal / external (detection conclusion, with script output as evidence) +- Detection conclusion: language/stack + evidence file paths +- Selection: `runtime-:` + one-line rationale +- [Audit-and-fix mode] audit results: must-fix N / should-fix N / notes N (with the problem table); whether the user authorized fixes +- What was written: Dockerfile (new/modified diff), `.dockerignore` +- What was skipped/not done: e.g. multi-process need unconfirmed, CI build step missing, user asked for audit only +- How far verification went: build/run results — **if not done, say not done** +- Left for manual follow-up: daemon.json registration, CI artifact steps, secret configuration + +## Hard constraints + +- ① **No compilation in runtime images**. Why: images carry no compilers/SDKs (dotnet has no SDK, java has no JDK/Maven, python has no musl compile chain); installing one defeats the "minimal runtime base" purpose. Symptom: build-time `mvn/dotnet SDK not found`, pip source-compile failures. Sole exception: the Node family install+builds at startup (node-gyp toolchain included — by design, not a loophole). +- ② **Node family never COPYs node_modules**. Why: the entrypoint runs `pnpm install` at startup (`--frozen-lockfile` with a lockfile); COPYing it in causes overwrite conflicts and image bloat; native modules must compile inside the container to match the runtime environment. +- ③ **Tokens never via ARG, never in image layers**. Why: ARG values persist in `docker history`; `NPM_TOKEN` via env shows up in `docker inspect` output. Correct posture: `NPM_TOKEN_FILE` pointing at a secret file (docker secret / k8s mounted volume); the token is written only into the in-container `~/.npmrc`. +- ④ **No latest in production**. Why: `latest` rolls with upstream every Monday 02:30 — the content under the same tag changes; "it ran last time" doesn't mean it runs next time. Pin a version tag or short sha. +- ⑤ **C/C++ only with musl dynamic linking on `runtime-cpp`**. Why: Alpine is musl-based; glibc artifacts won't run — symptom `exec format error` or `not found` (binary exists but the loader is missing). For glibc artifacts, switch base or compile statically. +- ⑥ **No dedicated Go/Rust image**. A static native binary needs no language environment at runtime — `FROM runtime-base` is enough; don't go looking for `runtime-go`. +- ⑦ **Frontend doesn't use this series**. React/Vue etc. are built by CI into static files and hosted by the project's own nginx image. +- ⑧ **Registry source is decided by pre-step detection — never guessed, never hardcoded**. Why: internal `livebook:8418` is LAN plain HTTP, unreachable from outside; external `code.innev.cn` is a detour from inside. Hardcode either route and pulling breaks when the network environment changes. Run `scripts/detect-registry.sh` first on every task; for scenarios needing a fixed source (CI), set `INNEV_IMAGE_PREFIX` explicitly and say so in the report. + +## Troubleshooting table + +| Symptom | Cause | What to do | +|---|---|---| +| `exec format error` | glibc binary in a musl image / architecture mismatch | switch the build stage to musl or static compilation; check buildx architecture | +| Pull fails with `http: server gave HTTP response to HTTPS client` | host hasn't registered `insecure-registries: ["livebook:8418"]` | edit `/etc/docker/daemon.json` and restart docker (manual); or switch to the external prefix per pre-step detection | +| External machine FROM `livebook:8418` times out / connection refused | internal prefix is only reachable on the LAN | change FROM to `code.innev.cn/images/...` (re-run pre-step detection to confirm) | +| External pull returns 401 unauthorized | code.innev.cn registry requires auth | `docker login code.innev.cn` (manual, with the Gitea account) | +| pip install compile failure | runtime-python has no compile chain | compile wheels/venv in the build stage (CI), then COPY | +| .NET program won't start on runtime-dotnet | Framework 4.x program on the modern .NET image | switch to `runtime-wine-dotnet`, start with `xvfb-run -a wine` | +| Token visible in `docker inspect` | NPM_TOKEN passed via env | switch to NPM_TOKEN_FILE secret mount | +| Worked last week, broken this week | used latest; Monday rolling rebuild | pin a version tag / short sha | +| Node container starts very slowly | normal: install+build at startup | watch logs until listening; don't kill it as hung | + +## Red lines (never violated under any circumstance) + +- Never select by guesswork — if Step 0 finds no evidence, ask; never force a near-fit image +- Never add compile stages/SDKs to a project Dockerfile (except the Node family's startup build) +- Never write any token/secret into a Dockerfile or image layer; a token the user pasted must not land in any file — and remind them to rotate it +- Never silently drop anything — skipped requirements (multi-process/private registry/health check) must appear in the report +- Before changing an existing Dockerfile, present the conclusion and the diff first; write only after confirmation diff --git a/skills/runtime-image-en/references/runtime-matrix.md b/skills/runtime-image-en/references/runtime-matrix.md new file mode 100644 index 0000000..dc948ee --- /dev/null +++ b/skills/runtime-image-en/references/runtime-matrix.md @@ -0,0 +1,61 @@ +# runtime-base image matrix details (excerpted from the runtime-base repo README, 2026-09 edition) + +> Read on demand: consult this file when Step 1 leaves you unsure about preinstalled content or environment variables. +> Full image name: `$INNEV_IMAGE_PREFIX/runtime-:`, default prefix `livebook:8418/images/`. + +## Linux side (all Alpine, linux/amd64 + linux/arm64 dual-arch) + +| Image | Base | Preinstalled | +|---|---|---| +| `runtime-base` | alpine:3.22 | bash/git/curl/jq/make/CA certs/UTC+8 timezone; the runtime foundation for Go/Rust static binaries and scripts | +| `runtime-node` | node:24-alpine | Node backend: pnpm install + build at startup, launched with `node $APP_ENTRY` | +| `runtime-pm2` | node:24-alpine | same + pm2: `pm2-runtime` cluster startup | +| `runtime-python` | python:3.13-alpine | pip/setuptools/wheel + uv/pipx/virtualenv (no source compile chain — packages without musl wheels are compiled in the build stage) | +| `runtime-dotnet` | dotnet/aspnet:10.0-alpine | ASP.NET Core runtime, no SDK | +| `runtime-cpp` | alpine:3.22 | libstdc++/libgcc runtime libraries (musl dynamically linked artifacts only) | +| `runtime-java` | temurin:21-jre-alpine | JRE 21 LTS, no JDK/Maven/Gradle | + +The Node family (node/pm2) additionally preinstalls: pnpm/yarn (versions controllable via ARG `PNPM_VERSION`/`YARN_VERSION`), python3/make/g++ (the node-gyp compile chain) — because they run `pnpm install` at container start, and native modules compile at install time right there. + +## Windows side (Linux containers + Wine, x86_64 only) + +| Image | Base | Preinstalled | +|---|---|---| +| `runtime-wine-base` | debian:bookworm-slim | Wine (win32+win64) + winetricks + Xvfb + CJK fonts + pre-initialized wine prefix `/opt/wineprefix` | +| `runtime-wine-dotnet` | runtime-wine-base | .NET Framework 4.8 preinstalled (ARG `DOTNET_FX` switches the verb) | +| `runtime-mt4` | runtime-wine-base | MetaTrader 4 + corefonts preinstalled, Xvfb headless startup | + +Why Wine instead of Windows containers: Windows containers require a Windows Server host — high maintenance cost; .NET Framework 4.x and MT4 are mature and stable under Wine. Debian as base rather than Alpine: musl Wine has incomplete 32-bit (wow64) support. + +## Node-family environment variables (inject at docker run, never bake into layers) + +| Variable | Purpose | Default | +|---|---|---| +| `NPM_REGISTRY` | npm registry address (including private ones) | official registry | +| `NPM_TOKEN` | private registry token (via env it ends up in `docker inspect`; not recommended) | none | +| `NPM_TOKEN_FILE` | token file path (docker secret / k8s mounted volume; recommended, takes precedence over NPM_TOKEN) | none | +| `APP_DIR` | app directory | `/app` | +| `BUILD_CMD` | build script name (auto-skipped if absent from package.json) | `build` | +| `APP_ENTRY` | build artifact entry file | `dist/main.js` | +| `PM2_APP_NAME` | [pm2] app name | `app` | +| `PM2_INSTANCES` | [pm2] cluster instance count | `max` (core count) | + +## Common CMD / COPY patterns + +- Go: `COPY app /usr/local/bin/app` + `CMD ["app"]` +- Java: `COPY target/app.jar /app/app.jar` + `CMD ["java","-jar","/app/app.jar"]` +- .NET: `COPY out /app` + `CMD ["dotnet","/app/MyService.dll"]` +- wine-dotnet: `COPY bin/Release/ "$WINEPREFIX/drive_c/app/"` + `CMD ["xvfb-run","-a","wine","C:\\app\\MyService.exe"]` +- MT4: no COPY; `docker run -v /srv/mt4/data:/opt/wineprefix/drive_c/"Program Files (x86)"/"MetaTrader 4"`; drop EAs/indicators into the data directory's `MQL4/Experts`, `MQL4/Indicators` and restart the container + +## Pre-step: host registration on the pulling side (manual) + +Add `{ "insecure-registries": ["livebook:8418"] }` to `/etc/docker/daemon.json` and restart docker (the internal registry uses plain HTTP on the LAN). + +## Tag policy + +`latest` (rolling rebuild with upstream every Monday 02:30 Beijing time) / `short-sha` / `version tag` (v*). Production pins a version tag or short sha. + +## Package-manager mirrors for China + +Each runtime Dockerfile has comments showing how to enable them; you can also configure them in the project's build-stage Dockerfile (the README provides ready-made snippets for crates.io rsproxy and Maven aliyun). diff --git a/skills/runtime-image-en/scripts/detect-registry.sh b/skills/runtime-image-en/scripts/detect-registry.sh new file mode 100755 index 0000000..e94ad3a --- /dev/null +++ b/skills/runtime-image-en/scripts/detect-registry.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# detect-registry.sh — detect the reachable registry and print the image prefix +# Internal livebook:8418 preferred (plain HTTP on the LAN, fast); external code.innev.cn as fallback. +# Usage: scripts/detect-registry.sh +# Output: last stdout line is the image prefix (e.g. livebook:8418/images); exit 1 = both routes failed +set -u + +IN_HOST="livebook:8418" +IN_PREFIX="livebook:8418/images" +OUT_HOST="code.innev.cn" +OUT_PREFIX="code.innev.cn/images" + +say() { printf '%s\n' "$*" >&2; } +ok() { say "✔ $*"; } +no() { say "✘ $*"; } +step() { say "▸ $*"; } + +# --- Check 1: is livebook:8418 registered in insecure-registries (internal is plain HTTP; docker refuses to pull otherwise) --- +insecure_ok=0 +if command -v docker >/dev/null 2>&1; then + if docker info 2>/dev/null | grep -qi "$IN_HOST"; then + insecure_ok=1 + fi +fi +if [ "$insecure_ok" -eq 0 ]; then + for f in /etc/docker/daemon.json "$HOME/.docker/daemon.json"; do + [ -f "$f" ] && grep -q "$IN_HOST" "$f" && insecure_ok=1 && break + done +fi +[ "$insecure_ok" -eq 1 ] && ok "insecure-registries has $IN_HOST registered" || no "insecure-registries missing $IN_HOST" + +# --- Check 2: internal registry connectivity (/v2/ returning 200 or 401 both count) --- +in_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 2 --max-time 4 "http://$IN_HOST/v2/" 2>/dev/null || echo 000) +if [ "$in_code" = 200 ] || [ "$in_code" = 401 ]; then + ok "internal $IN_HOST reachable (HTTP ${in_code})" +else + no "internal $IN_HOST unreachable (HTTP ${in_code})" +fi + +if [ "$insecure_ok" -eq 1 ] && { [ "$in_code" = 200 ] || [ "$in_code" = 401 ]; }; then + step "conclusion: use internal registry (fastest build pulls)" + printf '%s\n' "$IN_PREFIX" + exit 0 +fi + +# --- Fallback: external --- +step "internal unavailable, trying external $OUT_HOST" +out_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 6 "https://$OUT_HOST/v2/" 2>/dev/null || echo 000) +if [ "$out_code" = 200 ] || [ "$out_code" = 401 ]; then + ok "external $OUT_HOST reachable (HTTP ${out_code})" + say "⚠ using external: slower pulls than internal; if the registry requires auth, run docker login $OUT_HOST first (manual)" + printf '%s\n' "$OUT_PREFIX" + exit 0 +fi + +no "external $OUT_HOST also unreachable (HTTP ${out_code})" +say "Both routes failed. Troubleshoot: internal machine — confirm same subnet as $IN_HOST, daemon.json registered, docker restarted; external machine — confirm https://$OUT_HOST is reachable" +exit 1