# Deploy a static site to a VPS: Vite, Astro, HTML Deploy a Vite single-page app, an Astro site or plain HTML to your own VPS. Caddy serves the files over HTTPS, no app process, with a tiny ox.toml or none. To deploy a static site to a VPS with ox, add the repo as a project and deploy: ox detects a Vite app (React, Vue, Svelte), an Astro site or plain HTML, runs your build, and Caddy serves the files over HTTPS with no app process. A Vite app or plain HTML needs no ox.toml; write a short one for your own domain or when the output folder is not the usual one. ## What ox detects in a static site repo ox reads the files at the root of your repo: - **Vite:** a `package.json` with `vite` as a dependency, a `build` script and no `start` script. ox builds it and serves `dist`, or the `build.outDir` in `vite.config.*`, as a single-page app. This needs no ox.toml. - **Plain HTML:** an `index.html` at the root and no `package.json`. ox serves the whole repo as it is, with no build. This needs no ox.toml either. - **Astro:** `astro` as a dependency and an `astro.config.*` file. ox builds it and serves `dist`, or the `outDir` your config names. An Astro config with a server adapter is not a static site, and `ox check` says so. - **Other generators:** Docusaurus (`build`), Eleventy (`_site`), SvelteKit with `adapter-static` (`build`), Nuxt with a `generate` script (`.output/public`) and a Next.js static export (`out`). Each needs its dependency and its config file. A Nuxt app with both a `build` and a `generate` script gets a hint instead, because either could be meant. For a build, the install comes from your lockfile (`npm ci` for `package-lock.json`, `pnpm install --frozen-lockfile`, `yarn install --frozen-lockfile` or `bun install --frozen-lockfile`), and the build is your `build` script, like `npm run build`. The Node.js version comes from `.nvmrc`, `.node-version`, `mise.toml` or `engines` in `package.json`, else ox uses Node.js 24. A `start` script changes the plan: ox then runs it as an app instead of serving files. Remove it, or write the ox.toml below. ## The ox.toml for a static site Write one when you want your own domain, or when detection got the folder wrong. Pick the one for your site and put it at the root of the repo. ### A Vite single-page app ox.toml for a Vite app: ```toml domains = ["www.example.com"] [static] dir = "dist" spa = true [tools] node = "24" ``` ### An Astro site ox.toml for an Astro site: ```toml domains = ["www.example.com"] [static] dir = "dist" [tools] node = "24" ``` ### Plain HTML ox.toml for plain HTML: ```toml domains = ["www.example.com"] [static] dir = "." ``` What each key does: - `domains` is the name Caddy serves your site on, with HTTPS. - `[static] dir` is the folder Caddy serves, relative to the repo root. It must exist after the build. - `[static] spa` sends every path with no file to `index.html`, so your app's own router handles links like `/pricing`. Leave it out for Astro and plain HTML, where each page is its own file. - `[tools] node` pins the Node.js version for the build. The install and build still come from your lockfile and `package.json`. Set `[build] commands` only when your build is not the `build` script. With `dir = "."`, Caddy serves every file in the repo, so keep anything private out of it, or move the site into a folder such as `public` and set `dir = "public"`. ## Services and an API next to the site A static site needs none, so leave `[services]` out. If your site calls an API, that API is its own project, or the same repo with an `[app]` and `api = ["/api"]` under `[static]` (see the [config reference](https://deploywithox.com/docs/config#static)). ## Build-time variables such as VITE_API_URL Your variables are there during the build, so a Vite app can read `VITE_API_URL` with `import.meta.env.VITE_API_URL`. Set it on the dashboard's Variables tab, or with `ox vars set VITE_API_URL`. A changed value needs a new build, which **Save & deploy** starts. Everything the build puts into your files is public, because every visitor downloads them. Never put a secret key in a static site. ## Check and deploy the static site Run `ox check` in the repo. It works offline and lists the plan. For the Vite file above it prints: ox check for a Vite app: ```text static.dir dist declared build.install npm ci detected:package-lock.json build.commands[0] npm run build detected:package.json tools.node 24 declared domains[0] www.example.com declared Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST Ready to deploy. ``` Then ship it: Ship from a terminal: ```sh ox check ox deploy --wait ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` `ox deploy --wait` prints the build as it runs and exits with its result. A static site has no app process, so `ox logs --follow` says so and points you to `ox runs`, and the dashboard's Logs tab points to the Deployments tab. Read a past build with `--run` instead. ## If the static site deploy fails - `the build left no dist directory`: the build wrote its files somewhere else. Set `[static] dir` to the folder your build makes, like `build` for Create React App or `out` for a Next.js static export. - `nothing to run: ox found no start command, static site, worker, or cron job`: ox did not see a site, such as a generator whose config file is missing. Add `[static] dir`; you do not need an `[app] start` for a static site. - `build 1/1: npm run build: exit status 1`: your build failed. The lines above it are your build's own output. Run `npm run build` on your computer, fix it, and push. Your visitors never see a failed deploy: the previous release keeps serving. [Troubleshooting](https://deploywithox.com/docs/troubleshooting#build-failed) explains each message, and the [config reference](https://deploywithox.com/docs/config#static) lists every `[static]` key. ## Next steps - [Set build-time variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read a deploy's build log](https://deploywithox.com/docs/operate/logs#run-logs) with `ox logs --run `. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [Turn on a preview per branch](https://deploywithox.com/docs/operate/previews) to check a change before it goes live. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/static-site