Services
A service is one deployable unit, built from a GitHub repository or pulled as a Docker image. This page covers builders, start commands, health checks, resources, replicas, cron jobs and App Sleeping.
Sources#
| Source | How it is built | Deploys on |
|---|---|---|
GitHub repository (owner/repo, branch, optional root directory) | Railpack, or the repository's Dockerfile | every push to the tracked branch, skiffly up, redeploy |
Docker image (ghcr.io/org/app:1.2.3, postgres:17) | not built, pulled by the node | manual deploy, image tag change |
Private repositories need the Skiffly GitHub App (Settings → Developer → Connect GitHub). Private registries are not supported yet: push to a public registry or build from a repository.
New service on the project canvas → GitHub repo, Docker image or Template. For a repository pick the branch. Root directory, Dockerfile path, port and build command are set through the CLI, the API, MCP update-service or config as code for now.
Builds#
Railpack (default) inspects the repository and produces an image for Node, Bun, Python, Go, Rust, PHP, Ruby, Java, Elixir, static sites and more. It reads the usual files (package.json scripts, requirements.txt, go.mod, …), so most projects need no configuration.
Dockerfile: when the repository (or the root directory) contains a Dockerfile, it is used automatically. Set a different path with dockerfilePath, and force a builder with builder: RAILPACK | DOCKERFILE (serviceUpdate, skiffly config, MCP update-service).
Other build settings: build command (overrides Railpack's build step) and root directory (monorepos: the build runs inside it). Watch patterns are accepted by the API but not enforced yet: every push to the branch deploys.
Builds run in a Kata microVM with BuildKit and layer caching; the image lands in a registry on the node. At most two builds run at once per node; a queued build shows waiting for a build slot in its log.
Start command and port#
Railpack derives the start command (npm start, gunicorn …, the compiled binary). Override it in the service's Settings → Deploy → Start command or with startCommand; for a Docker image the start command replaces CMD.
The service must listen on $PORT. Skiffly sets PORT to the service port (port on the service, 8080 when unset) and routes the public domain and the private hostname to it. Services from templates come with the right port already.
Health checks#
With a healthcheck path (/healthz) the node probes GET <path> on the service port: a new deployment receives traffic only once the check passes, and a container that fails the check three times in a row is restarted. Without a path the probe is a TCP connect to the port. Startup is given up to five minutes (a probe every 5 s, 60 attempts) before the deployment is marked FAILED, so migrations on boot are fine. healthcheckTimeout adjusts the per-probe timeout.
Resources and replicas#
Each replica gets a CPU and memory limit: 2 vCPU / 2 GiB by default, changeable in Settings → Resources within the limits of your plan. Memory is never overcommitted; CPU can burst up to the limit. Billing counts the reserved limit while the replica runs.
Replicas (numReplicas) run identical copies behind the same domain; requests are balanced across them. Volumes cannot be shared, so a service with a volume runs one replica.
skiffly api 'mutation { serviceInstanceUpdate(serviceId: "…", environmentId: "…", input: { numReplicas: 2, cpuMillis: 1000, memoryBytes: 1073741824 }) }'Restart policy#
ON_FAILURE (default, up to restartPolicyMaxRetries), ALWAYS or NEVER. A container that keeps crashing (three restarts in a row) puts the deployment into CRASHED; the last log lines are shown in the deployment message.
Cron jobs#
Set a cron schedule (0 3 * * *, UTC) on a service and it stops being a long-running process: each tick starts one container with the start command and waits for it to exit. Runs do not overlap; nextCronRunAt shows the next tick. Cron services do not get a domain and are not put to sleep.
App Sleeping#
Turn on App Sleeping (sleepApplication: true) for HTTP services that are idle most of the time. After about 10 minutes without a request through the public domain the service is scaled to zero and its deployment shows SLEEPING; the next request wakes it (typically 2–10 s, the requester waits) and traffic resumes. A sleeping service is not billed for CPU and memory; its volume is.
Sleeping needs a public domain and applies only to HTTP services without a TCP proxy; cron services and workers that only make outbound requests cannot be woken and are left alone. The setting applies immediately, without a redeploy.
Isolation runtime#
Services run in gVisor (runtimeClass: gvisor) by default. Some images need a full kernel (databases that use io_uring, anything with SO_REUSEPORT multi-threading, FUSE); switch such a service to Kata (runtimeClass: kata), a lightweight microVM. See Security.
Deleting a service#
Settings → Danger zone → Delete, skiffly service delete or serviceDelete(id). Deletion removes the workload in every environment, the domains and the volumes.