Scaling
Replicas, CPU and memory limits, and autoscaling for an app, all on the server that runs it.
What needs a license
Setting your own replicas and limits on your own machine is available on every plan. Only the automation is licensed:
| Feature | Plan |
|---|---|
Manual replica count (sh0 scale, POST …/scale) | All plans |
CPU and memory limits (PUT …/resources) | All plans |
| Autoscaling (creating a scaling configuration, enabling it, a minimum or maximum above 1) | Business |
| The Scaling tab of the dashboard | Shown only with a Business license; on other plans, use the CLI or the API |
Manual scaling
sh0 scale <app> <replicas> starts or stops containers so that the app runs the requested number of replicas; --status shows the configuration (actual output):
$ sh0 scale my-api 2
my-api: scaled 1 -> 2 replica(s)
$ sh0 scale my-api --status
Scaling status for my-api:
Current replicas: 2
Min replicas: 1
Max replicas: 1
Autoscaling: disabled
CPU threshold: 80%
Memory threshold: 80%
LB policy: round_robin
Cooldown: 300sThrough the API:
curl -X POST https://panel.example.com/api/v1/apps/<app-id>/scale \
-H "Authorization: Bearer $SH0_TOKEN" -H "Content-Type: application/json" \
-d '{"replicas":2}'The API accepts 1 to 50 replicas; the CLI refuses more than 10 (“Replicas must be between 1 and 10”). Scaling requires the developer role on the app's project.
CPU and memory limits
Each app container runs with a CPU quota and a memory limit. Change them with PUT /api/v1/apps/<id>/resources (actual response):
$ curl -X PUT https://panel.example.com/api/v1/apps/<app-id>/resources \
-H "Authorization: Bearer $SH0_TOKEN" -H "Content-Type: application/json" \
-d '{"cpu_limit":500,"memory_limit":256}'
{"data":{"app_id":"<app-id>","autoscale_cpu_threshold":80,"autoscale_enabled":false,"autoscale_memory_threshold":80,"cooldown_seconds":300,"cpu_limit":500,"created_at":"2026-10-02 02:55:05","current_replicas":2,"id":"4930c97a-d6ab-44ac-bfd0-368d2c6ac4cd","last_scale_at":null,"lb_policy":"round_robin","max_replicas":1,"memory_limit":256,"min_replicas":1,"updated_at":"2026-10-02 02:55:05"}}cpu_limitis in millicores (1000 = one core),memory_limitin MB. An omitted field is left unchanged;0resets it to the default.- Defaults when nothing is set: 512 MB of memory, and half the machine's cores for CPU (at least 0.5 core).
- Bounds come from the machine: CPU from 100 millicores up to the number of cores, memory from 64 MB up to the machine's RAM. Outside them, the error names the real figures, for example:
- The new limits apply to containers created afterwards: redeploy the app for the running container to get them.
{"error":{"code":"VALIDATION_ERROR","message":"memory_limit must be between 64 and 7751 MB (this machine has 7751 MB of RAM), or 0 to reset"}}Autoscaling
With a Business license, sh0 adjusts the number of replicas from CPU and memory usage. Create the configuration once:
curl -X POST https://panel.example.com/api/v1/apps/<app-id>/scaling \
-H "Authorization: Bearer $SH0_TOKEN" -H "Content-Type: application/json" \
-d '{"min_replicas":1,"max_replicas":4,"target_cpu_percent":70,"target_memory_percent":80,"cooldown_seconds":300,"enabled":true}'min_replicas,max_replicas-- 1 to 50, maximum not below minimum. Both default to 1.target_cpu_percent,target_memory_percent-- 10 to 100, default 80.cooldown_seconds-- 60 to 3600, default 300: no new scaling action before this delay after the previous one.lb_policy--round_robin(default),least_connorrandom.cpu_limit,memory_limit-- optional, same meaning and bounds as above.
A configuration already exists as soon as limits have been set, and a second POST is refused (“Scaling config already exists for this app. Use PATCH to update.”). Change it with PATCH /api/v1/apps/<id>/scaling/update, every field optional. Without a Business license, the refusal is a 403 with the code LICENSE_REQUIRED.
sh0 scale <app> --auto creates a configuration with 1 to 3 replicas. If a configuration already exists, it only switches autoscaling on and keeps the existing bounds: with a maximum of 1 (the value left by setting limits), nothing will ever scale. Check with --status and raise max_replicas through the API.How the autoscaler decides
- The autoscaler runs every 30 seconds (
sh0 serve --autoscale-interval,0disables it) and reads the app's CPU and memory metrics of the last two minutes. - Up when the average CPU or the average memory exceeds its target.
- Down when both averages are below half their target, with at least 10 samples of each.
- One replica at a time, within the bounds, and never during the cooldown.
- The plan is checked at every pass: if the license lapses, automatic scaling stops.
Load balancing
All replicas sit behind the same domains: Caddy spreads requests among them according to lb_policy. Inside the project network, the app's alias (my-api.internal) resolves to every replica.
Limitations
- One server. Replicas run on the server that hosts the app; they add CPU parallelism and resilience to a crash, not more machine.
- Mounts. Replicas added by
sh0 scalestart without the app's volume mounts. Do not scale an app that writes to a volume. - State. Requests from one user can reach different replicas: keep sessions and uploads outside the container (database, Redis, object storage).
- Databases. Database servers have their own resource settings; this page is about apps.