Dev.to WebDev 🛠 Dev 👁 0 📖 9 min read

Add a Boot Probe Behind a Deploy Contract, Not a Free Host

I want to walk you through a Friday failure I keep arguing about, and it never starts inside the model call itself. Someone has a generated app that looks finished on a laptop, then a fresh host answers with a blank 502.

I want to walk you through a Friday failure I keep arguing about, and it never starts inside the model call itself. Someone has a generated app that looks finished on a laptop, then a fresh host answers with a blank 502. The page had already talked to a model, saved nothing durable, and assumed a dotenv file would appear by magic. Does that sound like a model problem to you, or like a boot problem wearing a model costume?

I take a hard position on this handoff, and I am not interested in a soft maybe. A free host is a staging bench until a deploy contract passes, and a passing laptop demo is not that contract. If your first live check is a human refreshing the homepage, you already skipped the only test that matters. Why would you trust a process you have not asked to name its own missing pieces out loud?

Think of the free server as a borrowed workbench in a shared shop, not as a house you already own. You can set your tools down and make a cut, but the clamps, the power drop, and the lock were never implied. A generated Dockerfile is the tool roll, while the contract is the agreement about power, identity, and what happens when the shop closes. People paste the tool roll onto the bench and then act surprised when the lights cut mid-cut.

The contract I want is small enough to fail in one request, and boring enough that a reviewer can read it aloud. It names required environment keys without printing their values, and it refuses to continue when one name is absent. It checks that the migration head matches the revision the running code expects, because a fresh disk will not forgive a guessed schema. It also proves the process can answer a ready route before any product router accepts a user request.

People have been arguing lately that a generated interface is enough to show your work, and I think that claim skips the boot. The generator optimized for a happy path on one machine, so it hid configuration inside a local file and a long-lived dev server. A free host starts cold, drops your process when idle, and will not smuggle your laptop secrets into the environment for you. Have you ever watched an app demand a database URL in production while the only copy lived in a gitignored file on one laptop?

The failure then climbs the stack in a way that fools the person who is watching the browser. The UI treats every slow response as the model thinking, so a 502 becomes a spinner and a 503 becomes a retry toast. Meanwhile the API never recorded who owned the boot, and the database never confirmed that this build knows this schema. I would rather show a dull not-ready page with a code than a chat box on a process that cannot name its database.

So I would not point any traffic at the host until a boot probe owns that boot. The module below is a proposal you can run yourself, and I am not claiming I timed it on any particular machine. It writes a single decision in memory, returns a stable error code, and leaves the product routes closed when the contract is red. That result is less glamorous than another chat wrapper, and that dullness is the point.

# Unexecuted example: a boot contract, not a measured production trace.
from __future__ import annotations

import os
from dataclasses import dataclass

REQUIRED_KEYS = ('DATABASE_URL', 'SESSION_SECRET', 'MIGRATION_HEAD')

@dataclass(frozen=True)
class ProbeResult:
    ok: bool
    code: str
    missing: tuple[str, ...]
    migration_head: str | None

def read_contract(expected_head: str) -> ProbeResult:
    missing = tuple(key for key in REQUIRED_KEYS if not os.environ.get(key))
    if missing:
        return ProbeResult(False, 'config_missing', missing, None)
    actual = os.environ['MIGRATION_HEAD']
    if actual != expected_head:
        return ProbeResult(False, 'migration_mismatch', (), actual)
    return ProbeResult(True, 'ready', (), actual)

I keep the product surface behind that result on purpose, because a green function nobody calls is just a comment. The ready route is the only public door until the code is ready, and every other route can stay unmounted. If you mount the chat route first, you have already decided that a missing secret is a user-facing incident. Would you let a customer hit checkout while the cash drawer is still in a box on the sidewalk?

# Unexecuted example: product routes stay unmounted until the probe is green.
from fastapi import FastAPI, HTTPException

app = FastAPI()
EXPECTED_HEAD = '20261008_01'

@app.get('/ready')
def ready() -> dict:
    result = read_contract(EXPECTED_HEAD)
    if not result.ok:
        # 503 tells the host and the UI this build is up but not eligible.
        raise HTTPException(
            status_code=503,
            detail={'code': result.code, 'missing': list(result.missing)},
        )
    return {'code': result.code, 'migration_head': result.migration_head}

The matching test is the part I actually want you to keep, because a contract without a failing test is a wish. This one deletes a key, expects config_missing, and never asserts on a secret value you might later leak. You can add the mismatch case in the same style, and you should, before you argue about which model to call. A test that only covers the happy path will smile at you on the laptop and then vanish on the fresh host.

# Unexecuted example. Requires pytest; monkeypatch is the built-in fixture.
def test_missing_config_stays_dark(monkeypatch) -> None:
    monkeypatch.delenv('DATABASE_URL', raising=False)
    monkeypatch.setenv('SESSION_SECRET', 'dev-only')
    monkeypatch.setenv('MIGRATION_HEAD', '20261008_01')
    result = read_contract('20261008_01')
    assert result.ok is False
    assert result.code == 'config_missing'
    assert 'DATABASE_URL' in result.missing
# Unexecuted local dry run. Do not commit these placeholder values.
# Terminal A:
# uvicorn app:app --host 127.0.0.1 --port 8000
# Terminal B:
export DATABASE_URL='postgres://app:[email protected]:5432/app'
export SESSION_SECRET='dev-only-not-a-real-secret'
export MIGRATION_HEAD='20261008_01'
python -m pytest tests/test_boot_probe.py -q
curl -sS -D - http://127.0.0.1:8000/ready

Read the status line before you interpret the prose in the body, because the code is the contract. A 503 with code config_missing means the process booted and the gate held, which is a success even though the app is not live. A connection reset or a platform 502 means your code never ran, so stop blaming the model client and inspect the start command. A 200 with code ready means you may mount the next route, and only that next route, after a person reads the body.

I also want the model client behind a seam that this probe does not import at all. The boot check should pass on a machine with no model credential, because host configuration is a different job from prompt completion. If your ready route opens a socket to a model provider, you have coupled two failure domains and you will page the wrong person. Can you explain, in one sentence, which credential the probe is allowed to touch, and why that list is so short?

This is where a free model path and a free server option actually earn a place in the workflow. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source project that, as described for this article, offers free model access and a free server option. I will not print a token count, a hardware shape, or a duration, because those figures move and go stale.

Read the current project docs before you design a budget around any allowance, including a free token figure an outreach note mentioned. My opinion on that free capacity is narrow, and it is not a slogan about shipping faster than your review. Use it as a cheap bench where a red probe is affordable, then exercise one read path that cannot mutate customer state. A free server will not invent your migration head, and a free model call will not rotate a session secret for you.

If the docs show a token allowance, treat it as a ceiling you re-check, not as proof of isolation. A ceiling is not a deed, and it does not make the host isolated, retained, or yours to keep. There is a plan step and an apply step, and I would not collapse them because a button labeled deploy feels nicer. Plan is the probe on the free host, with product routes unmounted, and the only artifact is the status code plus the code string.

Apply is a second change that mounts one route after a human reads that result and agrees the host may take a request. If a generated script both boots the host and flips traffic, you have hidden the apply inside a tool you did not review. Is the person who clicked deploy the same person who can read a 503 body without asking a model to translate it? Who should walk away from this approach before they copy the probe into a repository they do not own?

If you already run a reviewed cluster with secret injection and a migration gate, keep the probe and leave the free host alone. If the workload holds customer data, payment details, or private prompts, do not park it on a free server you have not reviewed. An unreviewed free server is not a softer kind of production, and I will not pretend the logs are private. If you want a one-command portfolio that never shows a failure code, this probe will feel obstinate on purpose.

A static page is the honest alternative when you do not want a status code in the review. If your work cannot tolerate a cold start, do not pretend a free server is a worker fleet. Would you rather explain a red probe on Monday, or explain a vanished job that never had an owner? The example has limits I do not want you to sand off in the name of a cleaner demo.

It checks that key names exist and that a head string matches, but it does not prove a role can migrate. A private network path is a separate review, and this probe will not vouch for that path at all. A green ready can still die on the first real query, so the next test should be one read against a scratch schema with writes refused. The snippets are unexecuted proposals, so adapt the key names, the framework, and the head format before you trust them in a review.

I am not offering a benchmark, a hardware note, or a claim that any host will keep a process warm for you. Before I call a host eligible, I want the pull request to answer the gate in sentences a reviewer can challenge. The required key names are listed, values are absent, and the expected migration head is pinned to this exact build. The ready route returns 503 with config_missing or migration_mismatch when someone breaks it on purpose in the test log.

Product routes stay unmounted until that probe is green, and the model client is not imported by the probe module. A human records the status code from the free host before any traffic flip, and customer data stays off that host. A separate review has to say the host is acceptable before any private prompt is stored, replayed, or sent to a model. If those sentences are missing, the deploy is not done, no matter how polished the generated interface looks in a screenshot.

If you remember one argument, remember that a free host is a cheap place to fail the contract, not a pardon for skipping it. Which handoff is least stable in your app right now, the missing environment name, the migration head, or the ready route? Reply with the status code and the first line of the body, and leave every secret value out of the comment. If you want a low-cost bench for that probe, check the current MonkeyCode docs, then come back with the failure you actually hit.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.