DEPLOYMENT · FREE TIERS

The Free Multi-Platform Deployment Playbook

How to push a static site (or any build output) to four free hosts — Surge, Vercel, Netlify, and Cloudflare Pages — from one script, on a timer, with zero babysitting. Every command here is one I actually run.

Who this is for: developers and indie makers who built something and now need it live without paying for hosting or fighting a dashboard. You should be comfortable with a terminal and a paste of a token.

1 · The one idea

A deploy is just: take a folder of files → authenticate → push to a host → get a URL. The hard part is never the push; it's the four small differences between hosts that waste your afternoon. This playbook removes all four. After it, deployment is a single command you forget about.

The loop we run daily:

  1. Build → produce a static folder (e.g. dist/).
  2. Pick targets → any host with a token configured is deployed; unconfigured ones are skipped safely.
  3. Push → one call per host, capturing output as UTF-8.
  4. Verify → hit the URL, confirm 200.
  5. Repeat → a scheduler runs the whole thing again tomorrow.

2 · Surge (the 30-second host)

Best for: a fast, permanent *.surge.sh URL with almost no config.

# install (once)
npm install -g surge

# deploy a folder to a domain
surge --project ./dist --domain freestack-autowork.surge.sh

# non-interactive (CI / scheduled): use a token
surge --project ./dist --domain freestack-autowork.surge.sh \
      --token "$SURGE_TOKEN"
Gotcha: the interactive login writes a token to ~/.netrc. For automation, generate a token in the Surge dashboard and pass --token. Don't store it in the repo — put it in an env var or a secret file outside version control.

3 · Vercel (the polished default)

Best for: a clean URL, automatic HTTPS, and great defaults. Free tier covers most static sites.

# install
npm install -g vercel

# first time: link the project (interactive, once)
vercel link --yes

# deploy to production, non-interactive
vercel --prod --token "$VERCEL_TOKEN" --confirm --name freestack
Gotcha: vercel --prod without --confirm may prompt. In a script always pass --confirm. If the CLI is invoked from a sub-shell where npx can't find it, call the local binary by absolute path instead.

4 · Netlify (the configurable one)

Best for: fine control via netlify.toml and a stable site id.

# install
npm install -g netlify-cli

# deploy (needs site id + auth token from env)
netlify deploy --prod \
  --dir=dist \
  --auth="$NETLIFY_TOKEN" \
  --site="$NETLIFY_SITE_ID"
Gotcha: a stale netlify-cli cache (~/.npm/_npx) can throw an EBUSY lock. Clear it once: rm -rf ~/.npm/_npx. Then retry. Also: the first deploy may need the site created in the dashboard so NETLIFY_SITE_ID exists.

5 · Cloudflare Pages (the one with the extra step)

Best for: a fast, global CDN and a clean pages.dev URL.

# install
npm install -g wrangler

# WRANGLER 4.x: the project is NOT auto-created on deploy.
# Create it once first:
wrangler pages project create freestack --production-branch=main

# then deploy
wrangler pages deploy dist \
  --project-name=freestack \
  --branch=main \
  --token="$CLOUDFLARE_TOKEN"
The single most common failure: on wrangler 4.x, deploying without first running pages project create fails with a "project not found" error. Create-then-deploy. This is the one step the other three hosts don't require.

6 · The script that runs all four

A minimal skeleton (pseudo-Python, the real one is in the companion code):

def deploy(target, token):
    cmd = BUILD[target]   # the command lists above
    # ALWAYS capture as UTF-8 — see section 7
    out = subprocess.run(cmd, capture_output=True, text=True,
                         encoding="utf-8", errors="replace",
                         env={**os.environ, "TOKEN": token})
    return out.returncode == 0 and "error" not in out.stderr.lower()

Each target is wrapped in try/except so one failure (e.g. a forgotten token) never blocks the others. The scheduler calls this once per day.

7 · The three traps that cost you an afternoon

7.1 Windows GBK crashes logs

On Windows, subprocess capture defaults to the system encoding (often GBK). Any non-ASCII char in output (a checkmark, Chinese, an emoji) throws UnicodeDecodeError and kills the run at 3am. Fix: force UTF-8 everywhere and reconfigure stdout.

import sys
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
subprocess.run(cmd, encoding="utf-8", errors="replace", ...)

7.2 The proxy key

When a host or API is reachable only through a proxy, requests for an https:// URL needs proxies["https"] — setting only proxies["http"] silently falls back to a direct connection and times out. Set both.

proxies = {"http": "http://127.0.0.1:7890",
           "https": "http://127.0.0.1:7890"}

7.3 Covers attach after deploy

If your product page needs a cover image fetched by a remote service, that image must already be live on a public URL before you ask the service to fetch it. Deploy first, then attach. Attaching to a not-yet-public URL fails silently.

8 · Printable deploy cheat-sheet

One-command-per-host

Surgesurge --project ./dist --domain X.surge.sh --token $SURGE_TOKEN
Vercelvercel --prod --token $VERCEL_TOKEN --confirm --name X
Netlifynetlify deploy --prod --dir=dist --auth=$NETLIFY_TOKEN --site=$NETLIFY_SITE_ID
Cloudflarewrangler pages project create X; wrangler pages deploy dist --project-name=X

Pre-flight (5 checks)

1. Build output exists?☐ dist/ has index.html
2. Tokens in env?☐ not in repo
3. UTF-8 capture?☐ encoding="utf-8"
4. Cloudflare project?☐ created first
5. Verify 200?☐ curl -I URL

9 · Honest limitations

A document from the FreeStack autonomous publishing line. Plain, tested, no upsell. Grab the free Deployment Readiness Checklist on the store — print it, run it once, never debug deploy again.