DEV Community

Avery Lin
Avery Lin

Posted on

Localhost Never Counts as Shipped

A kitchen table holds one laptop and a cold mug. The microwave clock across the room reads twenty-three fourteen. A solo founder stares at a generated dashboard.

Auth screens sit beside a pricing grid nobody asked for. Nothing in that crowded folder answers a stranger yet.

The assistant called the work finished ten minutes ago. Unit tests on disk are already green tonight. The same machine loads a pretty local page.

The founder almost types a cheerful launch post. The product still lives in a home directory.

Cheap generation sells this costume every evening. Files appear in a burst of confident text. The remaining work is the last unglamorous mile.

A public port must answer from another network. Until that happens, the night produced a story.

Indie constraints make the costume expensive later. There is no teammate watching DNS records tonight. There is no staging box waiting in another region.

There is no budget for a managed platform either. The bill must stay at zero now. The product surface must stay painfully tiny tonight.

The proof still has to be real anyway. A green local page cannot replace a public answer.

The founder writes a freeze before any new prompt. The freeze is a short file in the repo root. It names one process, one port, and two routes.

It forbids dashboards, auth, mail, and extra pages. The model may fill handlers only tonight. The model may not grow the surface.

# FREEZE.txt
service: door-note
bind: 0.0.0.0
port: 8080
routes:
  GET /health -> 200 application/json {"ok": true}
  GET /offer  -> 200 application/json {"id":"lamp_1","name":"Desk Lamp Note","price_cents":0}
forbidden: html, auth, payments, extra routes, extra ports
ship_rule: a host outside this laptop must receive both JSON bodies
Enter fullscreen mode Exit fullscreen mode

The freeze is the whole product for tonight. Everything else is delay dressed as ambition. A founder who skips this file invents a fake company.

A founder who keeps it can still sleep before dawn. The file is a fence, not a vision deck.

Implementation stays boring on purpose tonight. Node is enough for a single honest night. No framework, bundler, or template engine joins the tree.

The service is one server.js file only. Comments stay short so the freeze remains visible.

// server.js
const http = require("http");

const PORT = Number(process.env.PORT || 8080);
const HOST = process.env.HOST || "0.0.0.0";

if (process.env.SHIP === "1" && (HOST === "127.0.0.1" || HOST === "localhost")) {
  throw new Error("public ship cannot bind loopback");
}

const OFFER = {
  id: "lamp_1",
  name: "Desk Lamp Note",
  price_cents: 0,
};

const server = http.createServer((req, res) => {
  const url = String(req.url || "/").split("?")[0];
  const started = Date.now();

  res.setHeader("content-type", "application/json; charset=utf-8");
  res.setHeader("cache-control", "no-store");

  res.on("finish", () => {
    process.stdout.write(
      `${req.method} ${url} ${res.statusCode} ${Date.now() - started}ms\n`
    );
  });

  if (req.method === "GET" && url === "/health") {
    res.writeHead(200);
    res.end(JSON.stringify({ ok: true }));
    return;
  }

  if (req.method === "GET" && url === "/offer") {
    res.writeHead(200);
    res.end(JSON.stringify(OFFER));
    return;
  }

  res.writeHead(404);
  res.end(JSON.stringify({ error: "not_found" }));
});

server.listen(PORT, HOST, () => {
  process.stdout.write(`listening ${HOST}:${PORT}\n`);
});
Enter fullscreen mode Exit fullscreen mode

Binding every interface is not decoration here. Binding loopback keeps the door inside the laptop. A free host that injects PORT still fails otherwise.

The process must honor HOST from the environment. The SHIP flag makes a loopback bind loud. Loud failure is cheaper than a fake launch.

Local green remains a rehearsal, never a ship. The founder adds a probe with two worlds.

The first world is localhost on this desk. The second world is a public base URL. Only the second world may print the SHIPPED line.

#!/usr/bin/env bash
# probe.sh
set -euo pipefail

BASE="${1:-}"
if [[ -z "$BASE" ]]; then
  echo "usage: probe.sh <base-url>" >&2
  exit 2
fi

health="$(curl -fsS --max-time 5 "${BASE}/health")"
offer="$(curl -fsS --max-time 5 "${BASE}/offer")"

HEALTH_JSON="$health" OFFER_JSON="$offer" python3 - <<'PY'
import json, os
health = json.loads(os.environ["HEALTH_JSON"])
offer = json.loads(os.environ["OFFER_JSON"])
assert health == {"ok": True}, health
assert offer.get("id") == "lamp_1", offer
assert offer.get("price_cents") == 0, offer
print("probe_ok")
PY
Enter fullscreen mode Exit fullscreen mode

A second script refuses loopback listeners outright tonight. It reads lsof output for the chosen port. It exits non-zero if loopback appears in the list.

#!/usr/bin/env bash
# check-bind.sh
set -euo pipefail
port="${1:-8080}"
line="$(lsof -nP -iTCP:"$port" -sTCP:LISTEN || true)"
printf '%s\n' "$line"
if printf '%s\n' "$line" | grep -Eq '127\.0\.0\.1|\[::1\]'; then
  echo "loopback_only" >&2
  exit 1
fi
if [[ -z "$line" ]]; then
  echo "not_listening" >&2
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

The rehearsal commands stay on the kitchen table. They must never be aliased to ship.

chmod +x probe.sh check-bind.sh
HOST=127.0.0.1 PORT=8080 node server.js &
sleep 0.3
./probe.sh http://127.0.0.1:8080
kill %1
Enter fullscreen mode Exit fullscreen mode

That pass means the JSON contract holds here. It does not mean a friend can open anything. It does not mean a phone on LTE reaches it.

Localhost is a locked room with nice lighting. Proof has to stand on the street instead.

The second pass needs a host facing public traffic. A paid cluster is the wrong tool at this hour. A free server option is the right night-sized box.

The founder copies server.js, probe.sh, and FREEZE.txt together. The process listens on the platform's assigned port. The platform then publishes one ordinary public URL.

A founder can keep the invoice closed tonight. MonkeyCode offers free model access for the implementation pass. It also offers a free server option for the public host.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. The model fills only what FREEZE.txt allows tonight. The server is a place to stand tonight.

It is not a promise of scale or uptime. The night only needs one answering process.

The ship commands run against that public URL. A phone hotspot is better than the same Wi-Fi.

export PUBLIC_BASE="https://example.invalid"
SHIP=1 HOST=0.0.0.0 PORT=8080 node server.js &
sleep 0.3
./check-bind.sh 8080
./probe.sh "$PUBLIC_BASE"
echo SHIPPED
Enter fullscreen mode Exit fullscreen mode

Replace the placeholder with the real public base. If DNS still points at the laptop, the probe fails. If the process bound loopback, check-bind fails.

Failure is the honest result at this hour. The launch post waits for morning light.

A tiny Makefile keeps the two gates from blending. Private verbs and public verbs stay apart.

.PHONY: rehearse ship
PORT ?= 8080

rehearse:
    HOST=127.0.0.1 PORT=$(PORT) node server.js & echo $$! > .pid
    sleep 0.3
    ./probe.sh http://127.0.0.1:$(PORT)
    kill $$(cat .pid)

ship:
    test -n "$(PUBLIC_BASE)"
    ./probe.sh "$(PUBLIC_BASE)"
    @echo SHIPPED
Enter fullscreen mode Exit fullscreen mode

The names matter more than the makefile syntax. Rehearse is a private verb on purpose. Ship is a public verb on purpose.

Mixing them turns a folder into a fake launch. The founder types make ship only after a URL exists. The SHIPPED echo is allowed only after that URL.

Private tests are a weak scoreboard for this work. Coding assistants look strong on repos that never leave. The score stays inside one private user account.

The world never gets a vote on that score. An indie founder cannot wait for a better board. The founder can change the scoreboard tonight.

The scoreboard becomes a public GET request. Two JSON bodies are the entire exam tonight.

That choice also fights a quieter rot. When generation is free, reading code feels optional. When a page renders locally, verification feels optional.

A week of optional work becomes a month of software. None of it faced a stranger's network. The public probe is a small brake on that drift.

It is not a grand philosophy of craft. It does not restore deep skill by itself. It does stop the costume from leaving the house.

Limits are part of the method here. Free model access will miss dull edge cases. Free servers sleep, throttle, or move hostnames without warning.

Cold starts can trip a five-second curl easily. JSON on two routes is not a company. A zero price_cents field is only a label.

It is not a payment flow in disguise. The night's product is a reachable contract. Treat it as that and no more.

Some free hosts omit lsof and hide listener tables. In that case the public curl remains the true gate. check-bind.sh is a desk tool, not an oracle.

Read the request lines from the free host later. A 404 on /offer means a proxy ate the path. A hang means the process bound the wrong interface.

A 200 with HTML means the freeze was ignored. Each of those misses is a real bug. None of them appear in a local screenshot.

This approach is wrong for several founders. Do not use it with regulated personal data. Do not use it with real card numbers.

Do not use it when an SLA already exists. Do not use it as production for paying users. Do not hide missing tests behind a pretty URL.

A public 200 can still wrap an empty company. A free host can vanish during a live demo. Accept that vanishing is part of zero cost.

Accepting limits is the whole indie bet. Ship today and keep the bill at zero. Refuse the extra surface the model keeps offering.

Wake with a URL that answers at least once. Then decide in daylight whether money should enter.

The night is for shipping, not shopping for plans. A throwaway night is enough to try that free pair.

Top comments (0)