---
title: Deploy a static site
description: Pack a directory, upload it with the CLI, and wait until the URL answers.
---

A static app is files. The CLI packs a directory, uploads the archive, and waits until `https://<slug>.tori.cloud` serves those files. Nothing runs in a container, so environment variables do not apply. The CLI refuses `tori env set` on a static site; see [Environment variables](/guides/environment/#static-sites).

An app is static or a service for its whole life. A static site cannot later become a service. Create a new app with `--service` or `--image` in a directory that has no `tori.json` if you need a container — see [Deploy a container service](/guides/service-deploy/).

## Directory deploys

From the project root:

```sh
npx tori.host
```

or, with an explicit directory:

```sh
npx tori.host deploy ./dist
```

Detection, when you omit the directory, is [documented in the quickstart](/getting-started/quickstart/#what-the-cli-looks-for). Flags live on [tori deploy](/cli/deploy/).

The first deploy writes `tori.json` with the app slug. Later deploys from that directory update the same app. Put `tori.json` in git; do not hand-edit the slug to a name you do not own.

## Sign-in on the deploy path

If the machine has no saved token, deploy opens a browser (or prints a device code). In CI set `TORI_TOKEN`. Revoke a token in the console under Settings → API tokens. See [Sign in](/getting-started/#sign-in).

## Uploads

The upload can take minutes on a slow link. The progress line on stderr is how you tell a wait from a hang; when stderr is a pipe (CI), progress is periodic lines instead.

Archives over the CLI's local size cap are refused before the request starts. The server also enforces the plan's artefact limit, a per-file size cap, and a limit on how many members the archive may contain. A single file that is too large fails after upload with a reason such as `artifact contains a file larger than 64 MB`. Unsafe paths (`..`, absolute names) are refused the same way.

A very slow upload can be closed by the API with HTTP 408 before the last byte arrives. That is a server request deadline, not a broken connection. Retry from a faster link; nothing was deployed.

## When it is live

`live` means the files of this deploy are the ones the URL serves. Ctrl+C stops watching; it does not cancel an upload that already finished. There is no separate “promote” step.

If another deploy of the same app is already in progress, the server refuses the new upload with HTTP 409 and nothing is sent. Wait, or inspect it with `npx tori.host deploy status`.

In the console, open the app → **deploys** for the same history. Rollback is offered there for a previous static deploy whose files have not been reclaimed yet.

## Custom hostnames

An app can use a hostname you already own if the plan includes custom hostnames. The console hides **Add domain** when the plan has none; the API refuses with a message that names the plan. `tori whoami` and Settings show the plan.

When the plan allows it:

1. In the console, open the app → **domains**, or run `npx tori.host domains add www.example.com` from the directory that holds `tori.json`.
2. Create the DNS record the command prints — a CNAME to the app's `*.tori.cloud` name for a subdomain, or an A record to the zone's addresses for a two-label name.
3. `npx tori.host domains check www.example.com` re-runs the check (at most once per 30 seconds). A certificate is issued only after the record points here.

See [tori domains](/cli/domains/) for flags. Do not point a hostname at Tori until the add command has reserved it — the printed record is the one to create.

## In CI

```sh
export TORI_TOKEN=tori_…          # Settings → API tokens
npx tori.host deploy ./dist
```

Progress becomes periodic lines because stderr is not a terminal. The deploy still waits until the URL is live.

See [tori deploy](/cli/deploy/) for flags and [the API](/api/) for the HTTP side of the same flow.

Download this page as [Markdown](/downloads/markdown/guides/static-deploy.md).
