Deploy from CI
Tori does not watch your repository. Your CI does: on a push to the branch you deploy from, a job runs npx tori.host deploy with a token, the same command you run locally. This page sets that up for GitHub Actions and GitLab CI.
Before the first pipeline
Section titled “Before the first pipeline”- Deploy once from your machine. The first deploy creates the app and writes
tori.jsonwith its slug. A deploy token cannot create an app, so the pipeline needs one that already exists. - Commit
tori.json. It is how the pipeline knows which app to deploy. Every later deploy from that directory goes to the same app. - Mint a deploy token. In the console, Settings → API tokens → + New token. Choose Deploy one app and pick the app. Choose an expiry if you want the secret to stop working on its own; the default is never. Copy the secret: it starts with
torid_and is shown once.
Through the API, a deploy token can deploy its app, read the app and its deploys, follow a deploy and read its build log, cancel a deploy and roll back. It cannot delete anything, read or set environment variables, touch the database, domains or runtime logs, reach another app, or mint another token. Those requests answer 403 with the code token_scope. Billing and sign-in requests answer 401, as they would for a token they do not know. A token for the Whole account can do all of that. Keep it for MCP and your own scripts, not for a CI secret that every maintainer of the pipeline can read.
What a deploy token limits is the reach, not the trust. Whatever it deploys runs as that app, with that app’s environment variables and database credentials. So whoever holds the token can deploy code that reads those secrets, or replace the site. Guard it like the app’s own secrets. What a leak cannot do is touch your other apps, delete anything, or outlive a revoke.
GitHub Actions
Section titled “GitHub Actions”Store the secret as a repository secret named TORI_TOKEN (Settings → Secrets and variables → Actions → New repository secret). Then add .github/workflows/tori.yml:
name: Deploy to Tori
on: push: branches: [main]
# One deploy at a time: Tori refuses a second deploy of an app while one is# in progress (HTTP 409), so a push that lands mid-deploy waits its turn.concurrency: group: tori-deploy cancel-in-progress: false
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm run build - run: npx tori.host deploy ./dist env: TORI_TOKEN: ${{ secrets.TORI_TOKEN }}Replace ./dist with the directory your build writes. Change main if you deploy from another branch.
GitLab CI
Section titled “GitLab CI”Store the secret as a CI/CD variable named TORI_TOKEN (Settings → CI/CD → Variables), with Masked and Protected set, so it is hidden in job logs and only given to protected branches. Then add to .gitlab-ci.yml:
deploy: stage: deploy image: node:22 rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # One deploy at a time, for the same reason as above. resource_group: tori script: - npm ci - npm run build - npx tori.host deploy ./distGitLab passes the variable to the job’s environment, where the CLI reads TORI_TOKEN from.
A service instead of a static site
Section titled “A service instead of a static site”For an app that runs as a container service, the source is built on Tori, not in your pipeline. Drop the build steps and deploy the directory that holds tori.json:
- run: npx tori.host deploy env: TORI_TOKEN: ${{ secrets.TORI_TOKEN }}The same applies to GitLab: script: [npx tori.host deploy]. How Tori decides what to build is in Deploy a container service.
What the job prints, and when it fails
Section titled “What the job prints, and when it fails”The CLI sees that stderr is not a terminal and prints each phase as a line instead of a live status line. It waits until the deploy is live, and exits non-zero if the deploy fails, so the pipeline goes red with it. For deploy identifiers and phase timings in the log, set TORI_VERBOSE=1; see what the CLI prints.
| The job says | What it means |
|---|---|
the API refused TORI_TOKEN (401) on tori deploy |
The token was revoked, has expired, or was copied wrong. Mint a new one and replace the secret. A deploy token also gets 401 from billing and sign-in requests; that one says nothing about the token. |
This token may only deploy <app> and read its deploys. |
The token is a deploy token for another app than the one in tori.json, or the step does something a deploy token cannot. |
no API token found, and this looks like CI |
TORI_TOKEN is not set in the step that runs the CLI. |
another deploy of <app> is already in progress |
The server refused this deploy with HTTP 409 before the upload. Serialise the jobs as above, or wait. |
Rotating and revoking
Section titled “Rotating and revoking”Revoke a token in Settings → API tokens; it stops working at once. To rotate one, mint the new token, replace the CI secret, run the pipeline, then revoke the old one. A deploy token also stops working when its app is deleted. The list shows when each token was last used, which is how you tell a forgotten pipeline’s token from a live one.
Related
Section titled “Related”- Sign in for the other ways the CLI finds a token.
- Deploy status and recovery to inspect, watch, cancel or roll back a deploy from the same token.
- API for the HTTP side:
POST /api/tokensmints a deploy token withscope: "deploy"andappSlug.
Download this page as Markdown.