Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 7 min read

Weekend Build Log: A First-Boot Smoke Test for a Free Server and a Free Model Endpoint

Weekend Build Log: A First-Boot Smoke Test for a Free Server and a Free Model Endpoint Most one-weekend deployments do not fail in the feature code. They fail in the ten minutes after the first deploy: an unset environ

Weekend Build Log: A First-Boot Smoke Test for a Free Server and a Free Model Endpoint

Most one-weekend deployments do not fail in the feature code. They fail in the ten minutes after the first deploy: an unset environment variable, a cold start that outlasts the client timeout, a model endpoint that answers fine in a browser and hangs inside a container. This build log covers the one artifact that catches those failures before a demo link goes out β€” a first-boot smoke test of nine checks that writes a machine-readable verdict.

The scope cut was aggressive and intentional. The weekend shipped one script, one decision table, and one demo route. Everything else was skipped, and the skipped list is documented at the end rather than quietly forgotten.

Assumptions: bash, curl, Node 20+, a single-region host, and one model endpoint reachable over HTTPS. Nothing here depends on a specific vendor response format except one clearly marked extraction line.

Why first boot, and not unit tests

Unit tests answer "does this function behave." They do not answer "does this deployment behave." The gap between those two questions is where weekend projects die.

There are three failure classes that only appear after deploy:

  1. Configuration drift. .env.example lists seven variables; the host has five. The process starts, serves /health, and throws on the first request that needs the missing pair.
  2. Cold-start budget overrun. The first request after idle takes 8–20 seconds on a shared free host. The client timeout is 5 seconds. Every demo reviewer sees a spinner and leaves.
  3. Endpoint concurrency surprises. One request succeeds. Two simultaneous requests return a 429, a hang, or a truncated body β€” and the UI is written as if neither exists.

A smoke test that runs before warm-up is the only cheap way to see all three. Warm the service first and check #2 becomes invisible forever.

The nine checks

# Check What it proves Typical first symptom when it fails
1 Env completeness Every required variable is present and non-empty 500 on first real request, healthy /health
2 Cold start within budget First request after idle fits the client timeout Spinner, then a client-side abort
3 Model reachability The endpoint answers with the provided credential Timeout or 401 buried in a generic error
4 Response shape The reply parses into the field the app reads undefined rendered as the answer
5 Concurrency = 2 Two parallel calls both resolve, or one fails fast with a status Hanging request with no timeout
6 Restart persistence Data written before restart is readable after Empty demo after a host recycle
7 Log line format One structured line per request, parseable Logs exist but cannot be filtered
8 /health semantics Health reflects dependency state, not just process liveness Health green while model calls fail
9 Graceful shutdown SIGTERM drains instead of dropping in-flight work 502s on every redeploy

Checks 1–5, 7, and 8 are automated. 6 and 9 are manual by design β€” both need an action the script cannot safely take on a host it does not own.

The script

The harness below is a reference implementation, not a drop-in for every provider. It appends one NDJSON line per check so the verdict is greppable and diffable between runs.

#!/usr/bin/env bash
# smoke.sh - first-boot acceptance for a one-weekend deploy
set -uo pipefail

BASE_URL="${BASE_URL:-http://127.0.0.1:8787}"
MODEL_URL="${MODEL_URL:?MODEL_URL is required}"
MODEL_KEY="${MODEL_KEY:?MODEL_KEY is required}"
COLD_BUDGET_MS="${COLD_BUDGET_MS:-4000}"
RECORD="smoke.ndjson"
: > "$RECORD"

record() {  # record <check> <status> <ms> [detail]
  printf '{"check":"%s","status":"%s","ms":%s,"detail":"%s"}\n' \
    "$1" "$2" "$3" "${4:-}" >> "$RECORD"
}

timed() {   # timed <check> <fn>
  local check="$1" fn="$2" start end status
  start=$(date +%s%3N)
  if "$fn"; then status=PASS; else status=FAIL; fi
  end=$(date +%s%3N)
  record "$check" "$status" "$((end - start))"
  [ "$status" = PASS ]
}

Each check is a small function so a failing name is readable in the report without a lookup table.

check_env() {
  local missing=0
  for v in MODEL_URL MODEL_KEY APP_BASE_URL DATABASE_URL SESSION_SECRET; do
    [ -n "${!v:-}" ] || { echo "missing: $v" >&2; missing=1; }
  done
  [ "$missing" -eq 0 ]
}

check_health() {
  local body
  body=$(curl -fsS --max-time 5 "$BASE_URL/health") || return 1
  grep -q '"ok":true' <<<"$body"
}

check_model() {
  local code
  code=$(curl -s -o /tmp/model.json -w '%{http_code}' --max-time 30 \
    -H "Authorization: Bearer $MODEL_KEY" \
    -H 'Content-Type: application/json' \
    -d '{"input":"Reply with the single word: ready"}' \
    "$MODEL_URL") || return 1
  [ "$code" = "200" ] || return 1
  # provider-specific: adjust the field path to the envelope you actually get
  node -e 'const j=require("/tmp/model.json");process.exit(j.text?0:1)'
}

check_concurrency() {
  curl -s -o /tmp/c1.json --max-time 30 "$BASE_URL/api/draft" &
  local p1=$!
  curl -s -o /tmp/c2.json --max-time 30 "$BASE_URL/api/draft" &
  local p2=$!
  wait $p1; local r1=$?
  wait $p2; local r2=$?
  # acceptable: both succeed, or one fails fast (non-zero curl exit is NOT fast)
  [ $r1 -eq 0 ] || [ $r2 -eq 0 ]
}

The run order matters more than the checks themselves. Cold start runs first, against a service confirmed idle for at least five minutes. Warm the process and the measurement means nothing.

timed env_completeness check_env
timed cold_start       check_health   # must run first, on an idle host
timed model_reachable  check_model
timed concurrency_2    check_concurrency

A cold start beyond COLD_BUDGET_MS is not automatically a failure of the host. It is a failure of the pairing β€” the host's boot time against the client's patience. Fix either side, then re-measure.

Reading the verdict

One summarizer, run at the end, converts the NDJSON into a decision instead of a vibe.

node -e '
const fs=require("fs");
const rows=fs.readFileSync("smoke.ndjson","utf8").trim().split("\n").map(JSON.parse);
const failed=rows.filter(r=>r.status!=="PASS");
const slow=rows.filter(r=>r.ms>2000);
console.log(`${rows.length} checks, ${failed.length} failed, ${slow.length} slow`);
for (const r of failed) console.log(`FAIL ${r.check} (${r.ms}ms)`);
for (const r of slow)   console.log(`SLOW ${r.check} (${r.ms}ms)`);
process.exit(failed.length ? 1 : 0);
'
Verdict Condition Weekend action
GO 0 failures, cold start inside budget, manual checks done Publish the demo link
PATCH 1–2 failures, both in stub-able paths Time-box a 90-minute fix, rerun the full script
PARK Model check fails twice, or concurrency hangs past 30s Do not build UI on top of an unproven endpoint
ABANDON Three sequential model calls cannot finish inside the client timeout Wrong endpoint for this design; stop

The ABANDON row is the one that saves weekends. A free endpoint that answers in 20 seconds is fine for a nightly batch job and unusable for an interactive demo. The script distinguishes those cases in four minutes.

Where MonkeyCode fits, and where it does not

This build has exactly two external unknowns: where the code runs, and where the model call goes. Free model access removed the credential-and-billing detour from the first unknown, and the free server option removed the "do I rent a box just to measure cold start" question. That kept the weekend to one artifact instead of three.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

What the free tier does not remove: the checks above still have to be run. A free host can cold-start, a free endpoint can rate-limit at concurrency two, and free-tier terms change without notice. The current token allowance, hardware spec, and any limits are published on the project's own pages β€” read them on the day you build, not from a screenshot in a blog post (including this one). The verdict table exists precisely because those numbers are not guaranteed.

What was skipped

  • Auth. The demo route is public and rate-limited by nothing. Acceptable for a 48-hour link, not for a second weekend.
  • Migrations. One SQLite file, created on boot if absent. No versioning.
  • Observability beyond check 7. Structured request lines, no traces, no dashboards.
  • Load testing. Two concurrent requests is a smoke test, not a benchmark.
  • Anything multi-region. Latency was measured from one location, once.

Limitations and who should not use this

The approach is a pre-demo gate, not a production readiness process. Skip it if any of the following is true:

  • The service already has CI smoke tests against a staging environment; this would duplicate them at lower fidelity.
  • The project has real users. Checks 6 and 9 need proper integration tests, not a manual restart ritual.
  • Compliance, data residency, or retention requirements apply. A first-boot script says nothing about any of them.
  • The failure mode is correctness, not availability. No amount of endpoint pinging finds a wrong SQL join.

One more honest caveat: the script measures what a single machine in a single region sees at a single moment. Free tiers are shared, so two runs on different days can disagree. That variance is itself useful information β€” record it rather than average it away.

Next weekend

The obvious follow-up is a timing history: append each run's summary to a dated file and plot cold-start over a week. That turns a one-shot verdict into a trend, and a trend is what tells the difference between "free host is slow" and "free host was slow on Sunday." The second follow-up is a /health that returns dependency status β€” check 8 stays manual only because most health endpoints are too shallow to assert on.

If a weekend build is going to start with a deploy like this one, the free model tier and free server option are worth a look before renting anything β€” start from the project page, confirm the current allowance and spec yourself, then run the script against it and let the verdict decide.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.