Repository navigation
Run a dozen side projects on one small VPS with scale to zero #1242
thegdsks
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Most side projects get a handful of visits a day and sit idle the rest of the time. On a small VPS, memory runs out first, and an idle container holds its memory just as tightly as a busy one. Levelrail's sleep when idle feature changes that: an app nobody is visiting is stopped, holds no memory, and starts again on the next request.
Everything below comes from the sleep when idle docs and the CLI help.
Contents
Why scale to zero on your own box
Hosted platforms made scale to zero normal. Cloud Run scales a revision to zero instances when it gets no traffic, and Fly.io's autostop and autostart stop machines when traffic goes quiet and start them on the next request. In both, the first request after a quiet spell pays a cold start.
On a hosted platform you pay per use. On a VPS you pay a flat price for the box, so what you save is headroom: how many apps fit on the machine you already rent. A sleeping app does not shrink your bill by itself. It lets you fit more apps on the same VPS, or pick a smaller one.
How it works
Sleep is off by default and opt-in per app. Once it is on, a check runs every minute. An app sleeps when its last request, or the moment you turned sleep on or last woke it, is older than the idle time you chose (5 minutes to 7 days). Request activity comes from the same per-app request metrics the dashboard charts use.
Sleeping stops the app the same way Stop does, so settings, domains, secrets and volumes stay. While it is asleep, the app's domains answer a short "Waking up" page that retries on its own. The first request starts the app, so the first visitor waits for the cold start: the container start plus its readiness time, usually a few seconds to a minute. A sleeping app costs no memory and no CPU. It still takes disk for its image and volumes.
sequenceDiagram participant V as Visitor participant I as Ingress participant L as Levelrail participant A as App container Note over A: Running, no requests L->>L: Minute check: idle time passed L->>A: Stop (reason idle_timeout) Note over A: Asleep, 0 memory, 0 CPU V->>I: First request I->>L: Domain belongs to a sleeping app L->>A: Start (reason request) I-->>V: Waking up page, retries on its own A-->>L: Healthy V->>I: Retry I->>A: Request A-->>V: ResponseThe same life cycle as states:
stateDiagram-v2 [*] --> Awake: sleep enabled Awake --> Asleep: idle time passed, or sleep now Asleep --> Waking: request, wake, or API start Waking --> Awake: app healthy Awake --> [*]: sleep disabled Asleep --> Awake: sleep disabled (wakes first)Turn it on
The CLI is
levelrail-cli. Enable sleep for one app with a 30 minute idle time:levelrail-cli apps sleep enable web --after 30m--aftertakes 5m to 168h and defaults to 30m. The other verbs:Every verb accepts
--jsonif you want to script it. In the dashboard, the same switch lives on App, Deploys, in the Sleep when idle card, with a Wake now button while the app is asleep. Sleeping apps get an Asleep badge in the apps list.To try the whole cycle, pick the app that matters least, enable it with
--after 30m, then runapps sleep now <app>. Visit its domain, watch the waking-up page, then runapps sleep history <app>. You will see thesleptandwokeevents you just caused.Hold mode or the waking-up page
By default the first visitor sees the waking-up page. That works for a person in a browser. An API client or webhook sender will not retry an HTML page, so for those add
--hold:levelrail-cli apps sleep enable api --after 30m --holdWith hold on, the request waits for the start and is replayed. Per the docs, the client gets one
307redirect back to the same URL once the app is ready, so it needs to follow redirects (curl -L, and most HTTP libraries do for GET). Hold waits up to 30 seconds. After that, the visitor gets the waking-up page again while the start continues.Functions turn on sleeping with hold for you, if you want the full serverless feel for an image.
Read the sleep history
Every sleep and wake is recorded. The Sleep history card on an app's Deploys page draws the last 7 days as asleep and awake runs, shows totals, and lists recent events. The CLI gives the same events:
levelrail-cli apps sleep history web --since 7dEach event has a kind (
slept,woke,enabled,disabled,wake_failed), a time, a reason, and who caused it (a user, a token, orsystem). The reasons you will see areidle_timeout(the idle time passed with no requests),manual(someone ran sleep now or wake, or turned sleep off while asleep),request(a request to the app's domain woke it), andapi(enabled, disabled, or started outside sleep when idle).scheduleanddeployexist but are reserved.A few things the history gives you:
wokeevent carries how long the app had slept and, once it is healthy, how long the wake took. If the app is not healthy within 30 seconds, awake_failedevent records it.health probeorbotwhen the request looks automated. Client addresses are never stored.APP_SLEEP_EVENTS_KEEPto change that.The
botandhealth probelabels are the quickest way to spot an uptime monitor waking your app every few minutes. If you see them, point the monitor somewhere else or let that app stay awake.What counts as activity
Sleep decides an app is idle by watching HTTP requests that come through the built-in ingress. Per the docs, TCP streams, traffic from other apps, and a pinned host port are not seen as activity, and neither are background work and scheduled tasks. An app that is only used one of those ways would look idle while it is in use, so leave sleep off for it. A pinned host port bypasses the ingress entirely, so nothing could wake the app, and turning sleep on returns a 400.
In practice that makes sleep a good fit for anything people reach through a domain: portfolios, blogs, internal tools, demos, webhook receivers and APIs. Databases, queue workers and cron-style jobs are better left awake, since they do their work without a visitor request.
The docs' starting point is 30 minutes. A shorter idle time frees more memory but makes more visitors wait for a cold start. Apps with slow starts do better with a longer idle time, so they sleep less often. Deploy freezes and protected environments do not change any of this, since neither changes what runs.
Sizing the box
Set a memory limit on each app (App, Settings) before you start. The dashboard counts a sleeping app's limit as memory it is not using, and without a limit there is no number to count.
The arithmetic is the one the product uses: memory limit times hours asleep. As an example, an app with a 256 MiB limit that sleeps 20 hours in a day shows 5 GiB-hours (0.25 GiB x 20 h). Twelve apps at 256 MiB add up to 3 GiB if they are all awake. If three of them are awake at once, you need room for 0.75 GiB of app memory plus the platform. These are illustrative numbers, not measurements. Your own
apps sleep listand history show the real ones.A rollout that works:
--after 30mon the least important app firstapps sleep nowand watch one full wake inapps sleep history--holdfor anything another service callsapps sleep listand adjust idle timesTroubleshooting
sleep nowsays to turn sleep on first, or the app is already stopped (409)apps sleep enable <app> --after 30m, or start the app first--holdfor API callersapps logs <app>, fix slow startup, use a longer idle timeapps sleep wake <app>If you need help, run
levelrail-cli apps sleep status <name>,levelrail-cli apps logs <name>andlevelrail-cli doctor, then see Troubleshooting.Links and next steps
Try it on one app: set a memory limit, run
levelrail-cli apps sleep enable <app> --after 30m, and check the history tomorrow. Then tell us in this thread which idle times you ended up with and which app surprised you.All reactions