Deploy a container service
A service app is a container the zone runs for you. You can upload source and have Tori build it, or bring an image you already built. Which kind an app is is decided when it is created and cannot be changed afterwards.
What decides that this is a service
Section titled “What decides that this is a service”Three things make a service:
- a
Dockerfileat the root of the directory you deploy —npx tori.hostsees it and needs no flag; --service, for a project with no Dockerfile:npx tori.host deploy --service;--image, for an image you built elsewhere.
Anything else is a static site. That is on purpose: a project that could run as a server (a package.json with a start script) is not turned into one by guesswork.
The first deploy writes tori.json into the directory that was packed — the service’s own directory, also when it was named as tori deploy --service ./services/api from a monorepo root. Later deploys read it there. A service is built from source whenever --image is absent, so --service is only needed the first time. tori deploy status, build-log, logs and env read the same file, so run them from the service’s directory.
Deploy from source
Section titled “Deploy from source”From the project root:
npx tori.host deploy --service→ packing . (no Dockerfile: the builder detects the language)→ packed 23 files, 41.2 kB→ uploaded 41.2 kB in 0s (312.0 kB/s)→ queued for build — position 2 in the build queue→ building — attempt 1→ built registry.tori.zone/tenant-a_6d9009a4ef/app@sha256:9f2c…✓ live: https://plucky-wren.tori.cloudlive means the revision built from this upload is Ready and serving. The image line names what was built, pinned to its digest; tori deploy status shows it again later.
What is uploaded
Section titled “What is uploaded”The directory as it is, minus:
.gitandnode_modules, at any depth;- whatever
.gitignorematches — build output,.env,target/, the things that stay out of the repository stay out of the upload; tori.json.
Every .gitignore git itself reads applies: the ones above the directory up to the repository root (a monorepo’s top-level .env rule covers each service under it), the directory’s own, and the nested ones, the deeper file winning. Outside a repository only the packed tree’s files are read. Symlinks and file modes are kept, so ./scripts/build.sh is still executable when the build runs it. A .dockerignore is applied by the build next to the Dockerfile, not by the upload.
The plan caps the compressed upload and the time a build may run; the server names the cap when either is hit and the CLI prints it.
How it is built
Section titled “How it is built”With a Dockerfile at the root, that Dockerfile is built. Without one, the build detects the language — Node, Python, Go, PHP, Ruby, Java, Rust, Deno, .NET, Elixir and others — and produces an image that listens on port 8080 as a non-root user. A tree with neither a Dockerfile nor a recognisable language fails with a message naming what was looked for.
A Dockerfile has to meet the container requirements below itself. A detected build is arranged that way for you.
A detected build runs on base images Tori provides, pulled from Tori’s own registry rather than from the internet; PHP’s base images (dunglas/frankenphp and composer) come through Tori’s cache of Docker Hub. A railpack.json in the tree may still name a base image or a step input image, but only one on Docker Hub, which is pulled through that cache; naming an image on any other registry fails the build with a message naming the image. A Docker Hub tag that does not exist fails the same way as Docker Hub being unavailable — the build is retried once and the message names the image — so check the tag first. If you need a specific base image, use a Dockerfile.
When a build fails
Section titled “When a build fails”Nothing changes for the version that is live. The CLI prints the cause and the last lines of the build log, and where the rest is:
✗ deploy failed: Dockerfile build failed: exit status 1 … ERROR: process "/bin/sh -c npm ci" did not complete successfully: exit code: 1 Full build log: tori deploy build-log d_7f3a…npx tori.host deploy build-log d_7f3a… | lessThe log goes to stdout, whole, so it can be paged or attached to a report. Only a deploy built from source has one, and only once its attempt has ended. If the build finished while the builder could not reach Tori, the log arrives a little later: the builder keeps it and delivers it once the path is back.
A build that exits non-zero, that no provider matches, or that runs past the plan’s time limit is not retried: the result would be the same. Infrastructure failures are retried once.
npx tori.host deploy cancel d_7f3a…A queued build is cancelled immediately and the CLI exits 0. A leased or running build is marked for cancellation; the CLI then prints The API did not confirm cancellation and exits non-zero until the builder acknowledges. The previous version keeps serving. Ctrl+C in the terminal only stops watching — the build carries on, and tori deploy watch <id> resumes.
Digest-pinned images
Section titled “Digest-pinned images”Nothing is packed or uploaded on this path: the image is already in a registry, and all that travels is its name. Deploy from a directory with no tori.json (or a new directory) so the CLI creates a service rather than trying to run an image on an existing static site.
The reference must be pinned to a digest — registry/repo@sha256: followed by 64 lower-case hex characters. A tag is refused locally, before any request:
$ npx tori.host deploy --image nginx:latest✗ --image must name an image by digest, and nginx:latest names one by tagResolve a tag once, then deploy the digest:
docker inspect --format "{{index .RepoDigests 0}}" my-image:tagnpx tori.host deploy --image registry.example.com/acme/api@sha256:<64 hex>A tag can be moved onto different bytes tomorrow, so it cannot identify a deploy.
$ npx tori.host deploy --image registry.example.com/acme/api@sha256:e3b0c442…→ deploying registry.example.com/acme/api@sha256:e3b0c442…⠹ starting… (6s)✓ live: https://plucky-wren.tori.cloudlive means the revision the zone started is Ready and serving. Pass -v / --verbose to print the deploy ID, recovery commands, and phase timings.
Release-phase migrations
Section titled “Release-phase migrations”A service deploy can run a command in a Job after the image is ready and before the new revision takes traffic. Set it explicitly — it is not inferred from the tree:
npx tori.host deploy --image "$IMAGE" --release "npx prisma migrate deploy"{ "slug": "plucky-wren", "release": ["npx", "prisma", "migrate", "deploy"] }The Job is bounded at 10 minutes and that bound is not adjustable. If the Job fails or times out, the previous revision keeps serving. Database changes the command already committed are not rolled back. A Prisma, Alembic or Drizzle tree without "release" only prints a hint.
If that image and environment are already live, the CLI reports already live and does not create a new revision. --restart forces one:
npx tori.host deploy --image "$IMAGE" --restartContainer requirements
Section titled “Container requirements”The process must:
- listen on port 8080
- declare a numeric non-root
USER, for exampleUSER 1000
The zone enforces runAsNonRoot. Root images and named users whose UID Kubernetes cannot verify will not start. The upstream knative/helloworld-go image runs as root and is incompatible without rebuilding it.
A verified request-echo example (nothing is built):
IMAGE='ghcr.io/mendhak/http-https-echo@sha256:1721d9487ee774fd1d37de3014afd9c215df5056c125af941ee17512178129f6'npx tori.host deploy --image "$IMAGE"That image returns request data as JSON. It does not implement a Hello World TARGET variable.
Environment
Section titled “Environment”tori env set KEY=VALUE stores a variable for this service. A change applies on the next deploy, not immediately. Each deploy snapshots the environment; a running revision keeps the one it started with. After env set, deploy again: npx tori.host for a source-built service, or npx tori.host deploy --image … for a digest you brought. --restart is only for forcing a revision when that image and environment are already live, and it still requires --image.
Values never come back: tori env list shows names and a mask. See Environment variables.
Idle apps take naps
Section titled “Idle apps take naps”Every current plan sets the service to scale to zero when idle. The first request after a nap starts a new replica. That can add a short wake-up delay; it is not an outage. There is no always-on replica to buy today.
Watch, cancel, recover
Section titled “Watch, cancel, recover”Ctrl+C stops watching; the deploy continues on the server. Resume or cancel with the ID the CLI printed:
npx tori.host deploy statusnpx tori.host deploy watch d_examplenpx tori.host deploy cancel d_exampleCancellation applies only to pending service attempts. It prevents that attempt from going live and leaves an existing live version in place. A deploy that has already gone live cannot be cancelled. Repeating cancellation is harmless. Use the ID from your deploy, not the placeholder above.
If another attempt is already in progress, the CLI prints its ID and the watch/cancel commands (DEPLOY_IN_PROGRESS). See Deploy status and recovery.
Failures
Section titled “Failures”| What you see | What to do |
|---|---|
| Tag refused locally | Pin a digest; nothing was sent |
… is a static site, and a static site cannot run an image |
Deploy files to that app, or start a service from a directory with no tori.json |
deploy … is already in progress |
tori deploy watch <id> or tori deploy cancel <id> |
deploy failed: … |
The zone stored a reason (image pull, runAsNonRoot, crash on start, listen port, or a failed source build). Fix it, then deploy again |
| App never answers on 8080 | The probe fails and the revision does not become Ready. Listen on 8080 |
| Build log | npx tori.host deploy build-log <id> after a source build ends |
Pull the image yourself (docker pull <digest>) before blaming Tori if the registry requires auth Tori does not have. Public images work as in the echo example.
Custom hostnames work the same way as on a static app, if the plan includes them — Deploy a static site. Managed Postgres is not generally available. Logs are; see Logs.
See tori deploy for the flags and the API for the HTTP side of the same flow.
Download this page as Markdown.