Skip to content

shopify hydrogen dev --host does not propagate to inner workerd config #3759

@paul-phan

Description

@paul-phan

shopify hydrogen dev --host does not propagate to inner workerd config

Summary

When running shopify hydrogen dev --host inside a container, the outer Vite dev server correctly binds to 0.0.0.0, but the inner workerd (Cloudflare Workers runtime that MiniOxygen wraps) silently keeps its bind on 127.0.0.1. This makes external requests time out with instance refused connection even though the dev server appears to start successfully.

The host value is hard-coded as a string literal inside the compiled CLI bundle, so the --host flag has no path to reach it.

Environment

  • @shopify/cli 3.x (latest at the time of writing)
  • @shopify/hydrogen (current Pilot revision)
  • Node 24.x
  • Linux container (Debian slim), but reproducible on any container runtime
  • Started via npm run devshopify hydrogen dev --codegen --port 3456 --host

Reproduction

FROM node:24-slim
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates libatomic1 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN git clone --depth=1 https://github.com/Weaverse/pilot.git .
RUN npm ci
EXPOSE 3456
CMD ["npx", "shopify", "hydrogen", "dev", "--codegen", "--port", "3456", "--host"]
docker build -t hydrogen-repro .
docker run --rm -p 3456:3456 hydrogen-repro
# In another terminal:
curl --max-time 10 http://localhost:3456/
# → curl: (28) Operation timed out

Inside the container:

$ docker exec <id> sh -c 'cat /proc/net/tcp6 | awk "\$2 ~ /:0D80\$/"'
   0: 0000000000000000FFFF00000100007F:0D80 ...   ← 127.0.0.1:3456 only

(0100007F = 127.0.0.1 in little-endian hex.)

The dev server even prints a hint:

➜  Local:   http://localhost:3456/
➜  Network: http://172.17.0.2:3456/

— which is misleading. The Vite proxy is on the network, but workerd is not, so any request that bypasses the proxy (e.g. an HTTP/2 keepalive that pins to the bound socket) hangs forever.

Expected

--host propagates to both the outer Vite server and the inner workerd config. After shopify hydrogen dev --host, /proc/net/tcp6 should show :: or 0.0.0.0:3456 listening.

Actual

Vite is on 0.0.0.0. workerd is on 127.0.0.1. Requests proxied through Vite work; requests that land on the listener directly (Fly Replay, Cloud Run, ECS Fargate health checks, etc.) time out.

Root cause

host: "localhost" is a string literal in two locations inside the installed bundle:

  1. @shopify/cli/dist/workerd-*.js — the bundled workerd loader. The filename includes a content hash and varies across releases; the literal is consistent.
  2. @shopify/cli-hydrogen/dist/lib/mini-oxygen/workerd.js — present in older bundle layouts; still emitted in some Pilot lockfiles.

Both files set the workerd host config without checking any flag, env var, or option object. The CLI's flag parser receives --host, but the value never reaches the workerd config builder.

Workaround (current)

A sed-patch on the bundle, applied post-npm install:

# Pattern 1: @shopify/cli's workerd loader
find node_modules/@shopify/cli/dist -maxdepth 2 -type f -name "workerd-*.js" \
  | xargs -r sed -i 's|host: "localhost"|host: "0.0.0.0"|g'

# Pattern 2: @shopify/cli-hydrogen's mini-oxygen workerd (older layout)
sed -i 's|host: "localhost"|host: "0.0.0.0"|g' \
  node_modules/@shopify/cli-hydrogen/dist/lib/mini-oxygen/workerd.js \
  2>/dev/null || true

Brittle because the bundle filename changes per release. We pin to specific CLI versions to keep the patch reliable; an upstream fix would let us drop the pinning.

Documented externally: https://docs.weaverse.io/docker-deployment#1-dockerfile

Suggested fix

Plumb the existing --host flag value into the workerd config builder. Default to "localhost" when the flag is absent, so existing behavior is unchanged for local development.

Rough sketch (untested, but illustrates the surface area):

// inside the place that builds the workerd config
const host = flags.host === true ? '0.0.0.0'
           : typeof flags.host === 'string' ? flags.host
           : 'localhost'

workerdConfig.sockets.push({
  name: 'entry',
  address: `${host}:${port}`,  // was: `localhost:${port}`
  // ...
})

The --host flag is already declared as a boolean in the CLI's dev command's flags; this fix consumes its value rather than ignoring it.

Why this matters beyond our deployment

Anyone running Hydrogen dev mode inside a container hits this:

  • Docker on a developer's laptop with port-forward to a non-loopback interface
  • Fly.io, Render, Railway, Cloud Run, ECS Fargate
  • GitHub Codespaces (the user-facing URL is a proxied origin, not loopback)
  • Remote dev environments (Coder, Gitpod, etc.)

In all these the listener needs to be on 0.0.0.0 (or ::) for traffic to reach it, and --host is the documented knob. The current behavior is "the knob is wired but disconnected from the thing it claims to control".

Happy to send a PR if it helps — the change is small and the test could be a unit test that asserts the generated workerd config contains the requested host.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions