Skip to content
LazyFrog

Services (`services.json`)

How your app's services are set up in services.json on LazyFrog: web servers, APIs, background workers, scheduled jobs and static sites.

The platform runs the services defined in /workspace/services.json. Change them with the service tool (it validates and reloads), or edit the file with fs_write/fs_edit: it is reloaded at once, and an invalid file is reported (and not applied).

  • type: web (a site or app), api, worker (background process), cron (scheduled job) or static (a directory served as files; with dev, the preview runs that dev server instead, for hot reload, and production serves dir after build: the shape for Vite and other SPAs).
  • dev: the command in preview (with hot reload); build and start: production. Commands get PORT and must listen on it, on every address (0.0.0.0 or ::, --port $PORT): in production the platform picks the port, so a new version can start beside the live one. An app that ignores $PORT cannot be published. Every process a command starts stays in the foreground (php-fpm -F, caddy run, nginx -g 'daemon off;'; a script that starts two ends with wait): LazyFrog ends what a service started, daemons included, when it restarts or stops, but a daemon's crash and output aren't the service's, so the service looks healthy while that part is down.
  • build runs in a build sandbox of its own at publish (and deploy preflight): don't run a production build (pnpm build, next build) in the preview to check your work. Beside the dev server it needs about as much memory again and is stopped on Free and Hobby; check with the type checker and tests, and with deploy preflight for the build itself. Run a package.json script beside a dev server as node --run typecheck rather than pnpm typecheck: pnpm stays beside the script for as long as it runs (about 50 MB), and a type check beside next dev in 1 GB needs that room. On Free a type check (tsc) doesn't fit beside next dev at all, and there is no deploy preflight (it needs a paid plan): stop the dev server for the check or the build (service stop), run it, then service restart the dev server.
  • build (run before a release starts: pnpm build, uv sync --locked), test (run against a copy of production at publish: pnpm test, uv run --locked python -m pytest -q, which finds the app from the project root; a failure warns, or stops the publish with project settings block_publish_on_test_failure: true; DATABASE_URL is that copy, and Rails merges it into its test database too, so a Rails test runs as env -u DATABASE_URL RAILS_ENV=test bin/rails db:test:prepare test with its own test database), and in the preview tests use the preview's own database: make what they need in a fixture and delete it after, even when a test fails (2026-11-05: a failed run left test users behind). With FastAPI, one with TestClient(app) starts the app and its pool; for several people signed in at once add plain TestClient(app) clients beside it (a second with closes the pool),
  • port (web, api, static), healthcheck_path (must answer 200 quickly without login), env (non-secret variables), schedule (cron, e.g. */15 * * * *), dir and spa (static).
  • A cron service runs start on its schedule (dev instead in preview, if set), in the time zone named by "timezone" at the top of services.json (UTC if unset: set the user's, e.g. "Europe/London"). The same zone is the app's clock (TZ) and its database sessions' (PGTZ), so "today" is the user's today. service run runs it once now; service_status shows the next run. It runs only while its environment is awake: a live site sleeps after 15 quiet minutes on every plan unless it's always on (project settings always_on: true; Hobby keeps one live site awake, Pro three, Business ten; the owner can add one more for $5 / €5 a month on Billing), and a sleeping site catches up one missed run when it next wakes. For reminders and reports that must go out on time, turn always on for the live site and tell the user why.
  • Ports 5432, 6432, 7070 and 2525 are taken by the platform (and 80, below the 1024 minimum).
  • The first web service (else static, else api) is the site. Every other web, api or static service has its own address: <service>--<site address>, e.g. https://api--p-<id>.lazyfrog.app (two hyphens). Or serve it under a path of the site with a rewrite in routing.json.
  • Services call each other at SERVICE_<NAME>_URL (e.g. SERVICE_API_URL, http://127.0.0.1:…), set for every service with a port in both environments. Never localhost:<its declared port>: the live site runs each release on ports of its own, so that works on the preview only.
  • service_status shows what runs and whether health checks pass; logs_tail shows output (service name, or postgres, migrate, supervisor).
  • service stop stops one preview service for a while and keeps it stopped until service restart starts it (or the workspace sleeps and wakes, which starts everything): the way to give a build or another heavy command the dev server's memory. A restart alone gives it back only until the next request. While the site's service is stopped, its preview address says the site has stopped. A live site's services aren't stopped.
  • A service that keeps crashing goes to crashed and stops restarting: fix it, then service restart (in preview; environment: "production" restarts a live one, or publish the fix; the service tool says environment because its env is a service's variables, where the other tools say env). A live site's crashed service is tried again by itself every two minutes. A crashed service is shown to the owner and in Notifications.
  • A page that loads a whole table (every row into memory, then a sort or a sum in code) works on a few rows and kills the service once the table grows: the sandbox's memory is the limit, and a restart only gives it back until the next visit. Bound it in the code: LIMIT and pagination (keyset: WHERE id > $last ORDER BY id LIMIT 50), aggregates in SQL (count, sum, GROUP BY), or a stream. Don't raise Node's heap (--max-old-space-size) above the sandbox's memory: the process is then killed before Node can say it ran out.
  • exec and db_query run in UTC whatever timezone says (services and cron use it): give times with a zone (AT TIME ZONE, an aware datetime) in scripts and tests.
  • What the app declares for the platform's own steps (top level, all optional). LazyFrog knows no app by name: when an app needs something at a step, you declare it here, and the platform runs it the same way for every app.
    • "migrate": the app's own migration command, instead of the SQL files in migrations/ (run on a copy of the live data at the rehearsal, then on the live data after a snapshot).
    • "version": a command that prints one JSON line, {"code": "…", "data": "…"}: the version the code expects and the version the data is at (add "compatible": false or true to say it yourself; otherwise dotted numbers are compared). A publish, its rehearsal and a rollback refuse code older than the live data; service_status and project get show both. For an app that keeps a schema or data version of its own outside the migrations LazyFrog runs.
    • "after_seed": a command run once, after the one-time copy of the preview's data into the live site and before it goes live, in the release being published, with LF_PREVIEW_ORIGIN and LF_LIVE_ORIGIN (https://p-….lazyfrog.app and the live address) set. A non-zero exit takes the copy back and publishes nothing; the last lines of its output go in the publish log. For data that holds the app's own address, or anything else the preview leaves in it.
    • "vendored": globs from the repository root of other people's code the project keeps itself ("vendored": ["lib/legacy/**", "public/vendor-x"]; ** any depth, * and ? within a folder). project_scan, the check for keys before a publish and the commit's scan leave their look-alike findings out; a real provider key in them still refuses the commit. Installed packages and the usual third-party folders are already left out.
    • "migrate_on_push": false: a push (to git.lazyfrog.dev, or through the GitHub connection) doesn't run the preview's pending migrations; run them with db migrate instead. Without it they run on every push, on the preview's database only, after a snapshot.

For your AI: this page as Markdown · llms.txt