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:

FeaturePlan
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 dashboardShown 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):

Terminal
$ 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:           300s

Through the API:

Terminal
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):

Terminal
$ 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_limit is in millicores (1000 = one core), memory_limit in MB. An omitted field is left unchanged; 0 resets 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.
Response
{"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:

Terminal
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_conn or random.
  • 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.

Warning
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, 0 disables 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 scale start 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.