# ox: the full docs > ox is a deployment platform: you connect a GitHub repo and ox deploys it to your own Ubuntu LTS server (a VPS) with systemd and Caddy, no Docker. The user signs in to ox Cloud, the hosted control plane; their server runs only a small agent and Caddy. ox Cloud (deploywithox.com) is not Open-Xchange's OX App Suite or OX Security, which are unrelated products. Every ox docs page, stack guide and site page as Markdown, then the blog's post summaries, in the order below. The home page is linked from llms.txt as Markdown, and the privacy policy and the terms as their web pages. Each part opens with its title and its address. The list of links with a note each is https://console.t58.sorv.dev/llms.txt Contents: - [ox docs: deploy a GitHub repo to your own Ubuntu VPS](https://console.t58.sorv.dev/docs) - [Quickstart: deploy a GitHub repo to a VPS in 5 steps](https://console.t58.sorv.dev/docs/quickstart) - [Example projects on GitHub you can deploy with ox](https://console.t58.sorv.dev/docs/examples) - [Stacks: deploy guides for Node, Python, PHP and Java](https://console.t58.sorv.dev/docs/guides) - [Deploy Django to a VPS with gunicorn and Postgres](https://console.t58.sorv.dev/docs/guides/django) - [Deploy Next.js to a VPS: self-host without Vercel](https://console.t58.sorv.dev/docs/guides/nextjs) - [Deploy FastAPI to a VPS with uvicorn and Postgres](https://console.t58.sorv.dev/docs/guides/fastapi) - [Deploy a Node.js or Express API to your own VPS](https://console.t58.sorv.dev/docs/guides/node) - [Deploy Laravel to a VPS with a queue and scheduler](https://console.t58.sorv.dev/docs/guides/laravel) - [Deploy Ruby on Rails to a VPS with Puma and Postgres](https://console.t58.sorv.dev/docs/guides/rails) - [Deploy a static site to a VPS: Vite SPA or plain HTML](https://console.t58.sorv.dev/docs/guides/static-site) - [Deploy a React app built with Vite to your own VPS](https://console.t58.sorv.dev/docs/guides/react-vite) - [Deploy a Create React App build to your own VPS](https://console.t58.sorv.dev/docs/guides/create-react-app) - [Deploy a Vue app to a VPS: build, HTTPS, no Docker](https://console.t58.sorv.dev/docs/guides/vue) - [Deploy an Angular app to a VPS with Caddy and HTTPS](https://console.t58.sorv.dev/docs/guides/angular) - [Deploy Angular SSR to your own VPS with Node.js 24](https://console.t58.sorv.dev/docs/guides/angular-ssr) - [Deploy a Next.js static export to your own VPS](https://console.t58.sorv.dev/docs/guides/nextjs-static-export) - [Deploy a Gatsby site to your own VPS with HTTPS](https://console.t58.sorv.dev/docs/guides/gatsby) - [Deploy a Docusaurus docs site to your own VPS](https://console.t58.sorv.dev/docs/guides/docusaurus) - [Deploy an Astro static site to your own VPS server](https://console.t58.sorv.dev/docs/guides/astro) - [Deploy Astro SSR with the Node adapter to a VPS](https://console.t58.sorv.dev/docs/guides/astro-ssr) - [Deploy SvelteKit with adapter-node to your own VPS](https://console.t58.sorv.dev/docs/guides/sveltekit) - [Deploy a static SvelteKit site to your own VPS](https://console.t58.sorv.dev/docs/guides/sveltekit-static) - [Deploy Nuxt to a VPS: SSR on Node with no Docker](https://console.t58.sorv.dev/docs/guides/nuxt) - [Deploy React Router 7 or Remix to your own VPS](https://console.t58.sorv.dev/docs/guides/react-router) - [Deploy a React Router SPA (ssr: false) to a VPS](https://console.t58.sorv.dev/docs/guides/react-router-spa) - [Deploy SolidStart to your own VPS with Node.js](https://console.t58.sorv.dev/docs/guides/solidstart) - [Deploy Qwik City with the Express adapter to a VPS](https://console.t58.sorv.dev/docs/guides/qwik) - [Deploy NestJS to a VPS with PostgreSQL, no Docker](https://console.t58.sorv.dev/docs/guides/nestjs) - [Deploy a Fastify API with PostgreSQL to a VPS](https://console.t58.sorv.dev/docs/guides/fastify) - [Deploy a Hono API on Bun to your own VPS, no Docker](https://console.t58.sorv.dev/docs/guides/hono-bun) - [Deploy a BullMQ worker and Redis queue to a VPS](https://console.t58.sorv.dev/docs/guides/bullmq-worker) - [Deploy Deno Fresh to your own VPS with deno serve](https://console.t58.sorv.dev/docs/guides/deno-fresh) - [Deploy Flask to a VPS with gunicorn and PostgreSQL](https://console.t58.sorv.dev/docs/guides/flask) - [Deploy Symfony to a VPS with FrankenPHP and Doctrine](https://console.t58.sorv.dev/docs/guides/symfony) - [Deploy a plain PHP site to a VPS with FrankenPHP](https://console.t58.sorv.dev/docs/guides/php) - [Deploy Phoenix to a VPS: mix release and Postgres](https://console.t58.sorv.dev/docs/guides/phoenix) - [Deploy Spring Boot to a VPS with Maven and Postgres](https://console.t58.sorv.dev/docs/guides/spring-boot) - [Deploy Spring Boot built with Gradle to your own VPS](https://console.t58.sorv.dev/docs/guides/spring-boot-gradle) - [Deploy Quarkus to a VPS in JVM mode with Postgres](https://console.t58.sorv.dev/docs/guides/quarkus) - [Deploy ASP.NET Core to a Linux VPS with PostgreSQL](https://console.t58.sorv.dev/docs/guides/aspnet-core) - [Deploy a Rust web app to a VPS with cargo, no Docker](https://console.t58.sorv.dev/docs/guides/rust) - [ox.toml reference: every key, default and example](https://console.t58.sorv.dev/docs/config) - [ox CLI reference: deploy, logs and vars from a shell](https://console.t58.sorv.dev/docs/cli) - [Security: how ox hardens your VPS and isolates apps](https://console.t58.sorv.dev/docs/security) - [Operate your app: logs, rollbacks, backups, domains](https://console.t58.sorv.dev/docs/operate) - [View app and deploy logs: live, search, download](https://console.t58.sorv.dev/docs/operate/logs) - [Roll back a deployment to the previous release](https://console.t58.sorv.dev/docs/operate/rollback) - [Daily database backups and restore on your VPS](https://console.t58.sorv.dev/docs/operate/backups) - [What ox backs up, what it does not, and how to leave](https://console.t58.sorv.dev/docs/operate/leaving) - [Environment variables and secrets for your app](https://console.t58.sorv.dev/docs/operate/variables) - [Staging and branch preview deployments on a VPS](https://console.t58.sorv.dev/docs/operate/previews) - [Service dashboards: Postgres, Redis, NATS and more](https://console.t58.sorv.dev/docs/operate/services) - [Add a custom domain to your app with automatic HTTPS](https://console.t58.sorv.dev/docs/operate/domains) - [Test each pull request like a user with an AI tester](https://console.t58.sorv.dev/docs/operate/tests) - [Troubleshooting failed deploys: errors and fixes](https://console.t58.sorv.dev/docs/troubleshooting) - [Features: push to deploy on your own server](https://console.t58.sorv.dev/features) - [How ox self-hosts your apps on a server you own](https://console.t58.sorv.dev/self-hosting) - [Why I built ox](https://console.t58.sorv.dev/story) - [Guides to deploying on your own server](https://console.t58.sorv.dev/blog) # ox docs: deploy a GitHub repo to your own Ubuntu VPS https://console.t58.sorv.dev/docs Deploy a GitHub repo to your own Ubuntu server with ox, on systemd and Caddy, no Docker. Start with the quickstart, a framework guide or the ox.toml keys. ox is a deployment platform: you connect a GitHub repo and ox deploys it to your own Ubuntu server with systemd and Caddy, no Docker. You run one command on the server, pick the repo in the dashboard, and every push deploys. A new release gets traffic only after its health check passes, so a failed deploy never reaches your visitors. You push to your GitHub repo, and ox tells the agent on your server to deploy it. The agent builds the app on the server and runs it as a systemd service behind Caddy, which serves it over HTTPS. A new release goes live only after its health check passes. ## Start here Pick the page that matches what you are doing now. - [**Quickstart** Five steps from a fresh server to a live app.](https://console.t58.sorv.dev/docs/quickstart) - [**Examples** Public repos on GitHub that ox deploys](https://console.t58.sorv.dev/docs/examples) - [**Guides** A complete, checked ox.toml for a specific kind of app.](https://console.t58.sorv.dev/docs/guides) - [**Config reference** Every ox.toml key, what ox detects, services, and variables.](https://console.t58.sorv.dev/docs/config) - [**Troubleshooting** A failed health check, a failed build, or a domain that does not work.](https://console.t58.sorv.dev/docs/troubleshooting) - [**Operate** Logs, rollbacks, backups, variables, domains, staging and previews.](https://console.t58.sorv.dev/docs/operate) - [**CLI reference** Every ox command, with `--json` for scripts and agents.](https://console.t58.sorv.dev/docs/cli) - [**Security** What ox locks down on your server, and how to check it.](https://console.t58.sorv.dev/docs/security) ## For AI agents The **Copy skill for AI agent** button above copies the whole ox.toml authoring contract, so your coding agent can write and check the file for you. It first asks you to install the [ox CLI](https://console.t58.sorv.dev/docs/cli) and sign in, so the agent can also deploy, set variables and read logs. The same text is at [/ox-skill.md](https://console.t58.sorv.dev/ox-skill.md). Every docs page also comes as Markdown: add `.md` to its address, or press **Copy page as Markdown**. [/llms.txt](https://console.t58.sorv.dev/llms.txt) lists them all with a note each, and [/llms-full.txt](https://console.t58.sorv.dev/llms-full.txt) is every page and guide in one Markdown file. Last updated 2026-10-08. The page as HTML: https://console.t58.sorv.dev/docs # Quickstart: deploy a GitHub repo to a VPS in 5 steps https://console.t58.sorv.dev/docs/quickstart Deploy a GitHub repo to a fresh Ubuntu VPS in five steps: sign in, add the server with one command, add the repo, review the plan, and deploy it live. To deploy a GitHub repo to your own VPS with ox, sign in with GitHub, run the Add server command on a fresh Ubuntu LTS server, pick the repository, check the plan ox detected, and press Deploy. ox builds the app on the server, starts it as a systemd service behind Caddy, and moves traffic once the health check passes. A simple repo needs no config file. To try ox before you deploy your own app, fork one of the [example projects](https://console.t58.sorv.dev/docs/examples). The five steps, each with what to press or read in the dashboard: sign in with GitHub, add a server by running the command it shows as root, make a project from the repository, check what ox will run, and deploy. Beside each is the command that does the same from a terminal. ## Before you start You need a GitHub account, a repository with an app in it, and a fresh Ubuntu LTS server you can reach as root over SSH, with at least 1 GB of memory, 10 GB of free disk, and ports 80 and 443 free. ox installs everything else. ## 1. Sign in with GitHub Open [the sign-in page](https://console.t58.sorv.dev/login) and press **Continue with GitHub**. ox asks only for what it needs to read your repositories and get push events. ## 2. Add a server In the dashboard, press **Add a server**. It shows one command with a fresh token in it. Run it on the server as root. On the server, as root: ```sh curl -fsSL https://deploywithox.com/install/ | sudo bash ``` The token works once and expires, so copy the command from the dashboard each time. The script installs the ox agent and Caddy, then the server shows up in the dashboard. ## 3. Add the repository as a project Press **New project**, pick the server, pick the repository, and press **Continue**. To work from a terminal instead, install the ox CLI and sign in once. The steps below show the command for each button. The same from a terminal: ```sh curl -fsSL https://deploywithox.com/install.sh | sh ox login ox new --server ``` ## 4. Review the plan The **What ox will run** card shows the start command, build steps, services and variables ox detected from the repo. Answer what it still needs, such as a `SECRET_KEY`, and add an [ox.toml](https://console.t58.sorv.dev/docs/config) only for what detection got wrong. The [framework guides](https://console.t58.sorv.dev/docs/guides) show a complete one for Django, Next.js, FastAPI, Node, Laravel, Rails and static sites, and [Variables](https://console.t58.sorv.dev/docs/operate/variables#set) explains how to set a secret. Check the repo before the first deploy: ```sh ox check # offline: validate ox.toml, show the plan and missing variables ox review # what the first deploy still needs ``` ## 5. Deploy and follow the log Press **Deploy**, or run the command below. The run's log streams as it builds, migrates, starts the new release and switches traffic once the health check passes. Deploy from a terminal: ```sh ox deploy --wait # stream the run, exit with its result ox logs --follow # the app's logs ``` If the run fails, the log names the step and the fix. [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting) covers the common ones. ## Next steps after your first deploy - [Add a custom domain with HTTPS](https://console.t58.sorv.dev/docs/operate/domains): create one A record and Caddy gets the certificate. - [Read your app's logs](https://console.t58.sorv.dev/docs/operate/logs) live, or search them. - [Roll back a deploy](https://console.t58.sorv.dev/docs/operate/rollback) that went wrong, without a rebuild. - [See your daily backups](https://console.t58.sorv.dev/docs/operate/backups) and add an S3 bucket. - [Use the CLI](https://console.t58.sorv.dev/docs/cli) for everything the dashboard does. - [See what ox locked down](https://console.t58.sorv.dev/docs/security) on your server, and check it yourself. The [Operate](https://console.t58.sorv.dev/docs/operate) section covers the rest of running your app day to day. Last updated 2026-10-08. The page as HTML: https://console.t58.sorv.dev/docs/quickstart # Example projects on GitHub you can deploy with ox https://console.t58.sorv.dev/docs/examples Example repos on GitHub that ox deploys: Django, Rails, Laravel, Next.js, FastAPI, Phoenix, Go and Rust, each with an ox.toml when it needs one. Every repository below is a public app on GitHub that ox deploys as it is, with its ox.toml in the repo when it needs one. Read one next to the [guide for its stack](https://console.t58.sorv.dev/docs/guides), copy its ox.toml, or fork it and follow the [quickstart](https://console.t58.sorv.dev/docs/quickstart) to deploy it to your own server. The [config reference](https://console.t58.sorv.dev/docs/config) explains every key they use. ## Larger apps Each of these is a full app with several services, such as a web app with a worker, a queue and a database. | Repository | What it shows | Guide | | --- | --- | --- | | [oxzoo-live-control](https://github.com/saurav-codes/oxzoo-live-control) | A Vite, React and TypeScript static panel that checks every other example | [React with Vite](https://console.t58.sorv.dev/docs/guides/react-vite) | | [oxzoo-live-celery-hub](https://github.com/saurav-codes/oxzoo-live-celery-hub) | Django 5 with a Celery worker and beat, PostgreSQL and Redis | [Deploy Django](https://console.t58.sorv.dev/docs/guides/django) | | [oxzoo-live-rails-queue](https://github.com/saurav-codes/oxzoo-live-rails-queue) | Rails 8 on Puma with a Solid Queue worker and PostgreSQL | [Deploy Rails](https://console.t58.sorv.dev/docs/guides/rails) | | [oxzoo-live-phoenix-live](https://github.com/saurav-codes/oxzoo-live-phoenix-live) | Phoenix 1.8 LiveView with PostgreSQL | [Phoenix](https://console.t58.sorv.dev/docs/guides/phoenix) | | [oxzoo-live-sveltekit-ssr](https://github.com/saurav-codes/oxzoo-live-sveltekit-ssr) | SvelteKit server rendering that calls apps on three other servers | [SvelteKit](https://console.t58.sorv.dev/docs/guides/sveltekit) | | [oxzoo-live-nest-bullmq](https://github.com/saurav-codes/oxzoo-live-nest-bullmq) | NestJS with a BullMQ worker, PostgreSQL and Redis | [NestJS](https://console.t58.sorv.dev/docs/guides/nestjs) | | [oxzoo-live-catalog-api](https://github.com/saurav-codes/oxzoo-live-catalog-api) | FastAPI with SQLAlchemy 2 and Alembic, PostgreSQL and a Redis cache | [Deploy FastAPI](https://console.t58.sorv.dev/docs/guides/fastapi) | | [oxzoo-live-search-svc](https://github.com/saurav-codes/oxzoo-live-search-svc) | A Go chi API with Meilisearch as a custom service and an indexer worker | | | [oxzoo-live-laravel-jobs](https://github.com/saurav-codes/oxzoo-live-laravel-jobs) | Laravel 12 with MySQL, a Redis queue worker and the scheduler | [Deploy Laravel](https://console.t58.sorv.dev/docs/guides/laravel) | | [oxzoo-live-remix-router](https://github.com/saurav-codes/oxzoo-live-remix-router) | React Router 7 server rendering with build-time and run-time variables | [React Router](https://console.t58.sorv.dev/docs/guides/react-router) | | [oxzoo-live-cron-ledger](https://github.com/saurav-codes/oxzoo-live-cron-ledger) | Flask on gunicorn with PostgreSQL and four cron jobs | [Flask](https://console.t58.sorv.dev/docs/guides/flask) | | [oxzoo-live-hono-files](https://github.com/saurav-codes/oxzoo-live-hono-files) | Bun and Hono with SQLite, uploads kept across deploys, SeaweedFS as S3 | [Hono on Bun](https://console.t58.sorv.dev/docs/guides/hono-bun) | | [oxzoo-live-event-bus](https://github.com/saurav-codes/oxzoo-live-event-bus) | A Go chi API on NATS JetStream with Go and Python consumers | | | [oxzoo-live-next-prisma](https://github.com/saurav-codes/oxzoo-live-next-prisma) | Next.js 15 server rendering with Prisma and PostgreSQL | [Deploy Next.js](https://console.t58.sorv.dev/docs/guides/nextjs) | | [oxzoo-live-deno-fresh](https://github.com/saurav-codes/oxzoo-live-deno-fresh) | Deno Fresh 2 | [Deno Fresh](https://console.t58.sorv.dev/docs/guides/deno-fresh) | | [oxzoo-live-rabbit-relay](https://github.com/saurav-codes/oxzoo-live-rabbit-relay) | Fastify with RabbitMQ, a consumer worker and PostgreSQL | [Fastify](https://console.t58.sorv.dev/docs/guides/fastify) | | [oxzoo-live-mesh-shop](https://github.com/saurav-codes/oxzoo-live-mesh-shop) | A monorepo: Go gateway, Bun, Python and Node workers, and a static web app | | | [oxzoo-live-symfony-notes](https://github.com/saurav-codes/oxzoo-live-symfony-notes) | Symfony 7 with Doctrine and MySQL | [Symfony](https://console.t58.sorv.dev/docs/guides/symfony) | | [oxzoo-live-angular-static](https://github.com/saurav-codes/oxzoo-live-angular-static) | A static Angular 20 app that calls an API on another server | [Angular](https://console.t58.sorv.dev/docs/guides/angular) | | [oxzoo-live-astro-api](https://github.com/saurav-codes/oxzoo-live-astro-api) | A static Astro site with a Hono API on Node | [Astro](https://console.t58.sorv.dev/docs/guides/astro) | | [oxzoo-live-axum-cache](https://github.com/saurav-codes/oxzoo-live-axum-cache) | Rust Axum with sqlx, PostgreSQL and Redis | [Rust](https://console.t58.sorv.dev/docs/guides/rust) | | [oxzoo-live-nuxt-chat](https://github.com/saurav-codes/oxzoo-live-nuxt-chat) | A Nuxt 4 chat with server-sent events over Redis pub/sub | [Nuxt](https://console.t58.sorv.dev/docs/guides/nuxt) | | [oxzoo-live-pipeline-py](https://github.com/saurav-codes/oxzoo-live-pipeline-py) | Python workers and cron jobs with ClickHouse, Meilisearch, Qdrant and a Starlette API | | | [oxzoo-live-release-lab](https://github.com/saurav-codes/oxzoo-live-release-lab) | A Go standard library app for staging, previews, promote and rollback | [Operate](https://console.t58.sorv.dev/docs/operate) | ## Smaller examples Each of these shows one stack or one kind of app, such as a single-page app with its API or a worker with no web address. | Repository | What it shows | Guide | | --- | --- | --- | | [oxzoo-django-celery](https://github.com/saurav-codes/oxzoo-django-celery) | A Django API with a Celery worker and beat, Redis, PostgreSQL and a React app | [Deploy Django](https://console.t58.sorv.dev/docs/guides/django) | | [oxzoo-react-django](https://github.com/saurav-codes/oxzoo-react-django) | A Django API with a React single-page app, installed with uv | [Deploy Django](https://console.t58.sorv.dev/docs/guides/django) | | [oxzoo-vue-django](https://github.com/saurav-codes/oxzoo-vue-django) | A Django API with a Vue 3 single-page app, installed with uv | [Deploy Django](https://console.t58.sorv.dev/docs/guides/django) | | [oxzoo-fastapi-react](https://github.com/saurav-codes/oxzoo-fastapi-react) | A FastAPI API with a React single-page app, installed with uv | [Deploy FastAPI](https://console.t58.sorv.dev/docs/guides/fastapi) | | [oxzoo-svelte-flask](https://github.com/saurav-codes/oxzoo-svelte-flask) | A Flask API with a Svelte single-page app, installed with pip | [Flask](https://console.t58.sorv.dev/docs/guides/flask) | | [oxzoo-next](https://github.com/saurav-codes/oxzoo-next) | Next.js server rendering with client-side variables | [Deploy Next.js](https://console.t58.sorv.dev/docs/guides/nextjs) | | [oxzoo-sveltekit](https://github.com/saurav-codes/oxzoo-sveltekit) | SvelteKit server rendering with a build-time public variable | [SvelteKit](https://console.t58.sorv.dev/docs/guides/sveltekit) | | [oxzoo-react-vite](https://github.com/saurav-codes/oxzoo-react-vite) | A React app built with Vite and an Express API, with pnpm | [Deploy a Node API](https://console.t58.sorv.dev/docs/guides/node) | | [oxzoo-react-yarn](https://github.com/saurav-codes/oxzoo-react-yarn) | A React app built with Vite and an Express API, with Yarn classic | [Deploy a Node API](https://console.t58.sorv.dev/docs/guides/node) | | [oxzoo-vue-vite](https://github.com/saurav-codes/oxzoo-vue-vite) | A Vue 3 app built with Vite and an Express API, with npm | [Vue CLI](https://console.t58.sorv.dev/docs/guides/vue) | | [oxzoo-web3-vue](https://github.com/saurav-codes/oxzoo-web3-vue) | A Vue 3 web3 app with an Express API | [Vue CLI](https://console.t58.sorv.dev/docs/guides/vue) | | [oxzoo-bun-vue](https://github.com/saurav-codes/oxzoo-bun-vue) | A Bun and Hono API with a Vue single-page app | [Hono on Bun](https://console.t58.sorv.dev/docs/guides/hono-bun) | | [oxzoo-php-vue](https://github.com/saurav-codes/oxzoo-php-vue) | A PHP API with a Vue 3 single-page app | [Plain PHP](https://console.t58.sorv.dev/docs/guides/php) | | [oxzoo-cron](https://github.com/saurav-codes/oxzoo-cron) | A plain PHP page with a cron job that writes a heartbeat | [Plain PHP](https://console.t58.sorv.dev/docs/guides/php) | | [oxzoo-rust-react](https://github.com/saurav-codes/oxzoo-rust-react) | A Rust Axum API with a React single-page app | [Rust](https://console.t58.sorv.dev/docs/guides/rust) | | [oxzoo-go-react](https://github.com/saurav-codes/oxzoo-go-react) | A Go standard library API with a React single-page app | | | [oxzoo-ruby-react](https://github.com/saurav-codes/oxzoo-ruby-react) | A Sinatra API with a React single-page app | | | [oxzoo-wasm](https://github.com/saurav-codes/oxzoo-wasm) | A Go WebAssembly page with a Go API | | | [oxzoo-worker-go](https://github.com/saurav-codes/oxzoo-worker-go) | A Go background worker with no web address | | | [oxzoo-worker-python](https://github.com/saurav-codes/oxzoo-worker-python) | A Python 3.13 worker on a Redis queue, installed with uv | | | [oxzoo-worker-bun](https://github.com/saurav-codes/oxzoo-worker-bun) | A Bun and TypeScript background worker with no web address | | If one of them does not deploy for you, [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting) covers the common failures. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/examples # Stacks: deploy guides for Node, Python, PHP and Java https://console.t58.sorv.dev/docs/guides Deploy guides for every stack ox runs: React, Next.js, Django, Rails, Laravel, Spring Boot and more, each with its plan and proof, filterable by language. Pick your stack to deploy it to your own VPS with ox. Each guide shows the plan `ox check` prints for a minimal repo of that stack, the ox.toml if you need one, the variables to set, and the messages you see when a deploy fails. The badge says how far it is proven. Three of the guides, drawn from their own ox.toml: what ox starts or serves for each. Each guide's file goes through the same checks as `ox check`, which says `Ready to deploy.` for every one. - **Tested on a real server:** an app of this stack deployed and served on a real Ubuntu server in ox's test lab. - **Plan verified, real-server test pending:** ox's tests pin the plan it makes for the stack, and no real-server deploy is recorded yet. ## All stack guides Filter by language, or read by kind of app. A simple repo needs no ox.toml at all, so the [Quickstart](https://console.t58.sorv.dev/docs/quickstart) is the shortest path for any of them. - [All](https://console.t58.sorv.dev/docs/guides#lang-all) - [JavaScript and TypeScript](https://console.t58.sorv.dev/docs/guides#lang-js) - [Python](https://console.t58.sorv.dev/docs/guides#lang-python) - [Ruby](https://console.t58.sorv.dev/docs/guides#lang-ruby) - [PHP](https://console.t58.sorv.dev/docs/guides#lang-php) - [Java](https://console.t58.sorv.dev/docs/guides#lang-java) - [C# and .NET](https://console.t58.sorv.dev/docs/guides#lang-dotnet) - [Elixir](https://console.t58.sorv.dev/docs/guides#lang-elixir) - [Rust](https://console.t58.sorv.dev/docs/guides#lang-rust) ### Static sites and single-page apps ox runs the build and Caddy serves the files over HTTPS. No app process runs. - [**Deploy a static site** 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. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/static-site) - [**React with Vite** Deploy a React app built with Vite to your own VPS with no Dockerfile: ox runs the build from your lockfile and Caddy serves dist over HTTPS, no ox.toml. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/react-vite) - [**Create React App** Deploy a Create React App project to your own VPS: ox runs react-scripts build and Caddy serves the build folder over HTTPS. No Docker, no ox.toml. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/create-react-app) - [**Vue CLI** Deploy a Vue CLI app to your own VPS: ox installs from yarn.lock, runs your build, and Caddy serves dist over HTTPS. The plan comes from the repo alone. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/vue) - [**Angular** Deploy an Angular single-page app to your own VPS: ox reads the output folder from angular.json, runs ng build, and Caddy serves it, with no Docker. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/angular) - [**Next.js static export** Deploy a Next.js app with output: 'export' to your own VPS: ox runs next build and Caddy serves the out folder over HTTPS, with no Node process running. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/nextjs-static-export) - [**Gatsby** Deploy a Gatsby site to your own VPS: ox reads gatsby-config.js, runs gatsby build from package.json, and Caddy serves the public folder over HTTPS. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/gatsby) - [**Docusaurus** Deploy a Docusaurus documentation site to your own VPS: ox runs the build from package.json and Caddy serves the build folder over HTTPS, no ox.toml. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/docusaurus) - [**Astro** Deploy an Astro static site to your own VPS: ox reads astro.config.mjs, runs astro build, and Caddy serves dist over HTTPS with no app process running. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/astro) - [**SvelteKit static** Deploy a SvelteKit site built with adapter-static to your own VPS: ox reads the pages folder from svelte.config and Caddy serves it over HTTPS, no Docker. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/sveltekit-static) - [**React Router SPA** Deploy a React Router 7 app with ssr: false to your own VPS: ox reads react-router.config.ts, builds it, and Caddy serves build/client over HTTPS, no Node. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/react-router-spa) ### Server-rendered JavaScript apps ox builds the app and runs its server under systemd, with Caddy in front. - [**Deploy Next.js** Self-host Next.js on your own VPS without Vercel or Docker: next start, a health check, PostgreSQL, Prisma migrations and the image cache, in one ox.toml. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/nextjs) - [**Angular SSR** Deploy an Angular app with server-side rendering to your own VPS: ox builds it, runs the server.mjs Angular writes under systemd, and Caddy adds HTTPS. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/angular-ssr) - [**Astro SSR** Deploy an Astro app with the @astrojs/node adapter to your own VPS: ox builds it, runs dist/server/entry.mjs under systemd, with Caddy and HTTPS in front. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/astro-ssr) - [**SvelteKit** Deploy a SvelteKit app with adapter-node to your own VPS: ox reads svelte.config.js, builds it, runs node build under systemd, and Caddy adds HTTPS. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/sveltekit) - [**Nuxt** Deploy a Nuxt app with server-side rendering to your own VPS: ox runs nuxt build, starts .output/server/index.mjs under systemd, and Caddy adds HTTPS. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/nuxt) - [**React Router** Deploy a React Router 7 app (the Remix successor) with server rendering to your own VPS: ox builds it, runs npm start under systemd, and Caddy adds HTTPS. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/react-router) - [**SolidStart** Deploy a SolidStart app to your own VPS: ox reads the Node version from package.json, runs the build, starts it under systemd and adds HTTPS with Caddy. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/solidstart) - [**Qwik City** Deploy a Qwik City app with its Express adapter to your own VPS: ox finds the adapter, builds it, runs npm run serve under systemd, and Caddy serves HTTPS. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/qwik) - [**Deno Fresh** Deploy a Deno Fresh app to your own VPS: ox installs Deno 2, runs deno install and deno task build, then deno serve under systemd with Caddy for HTTPS. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/deno-fresh) ### Full-stack web frameworks The framework's own server, its migrations and its database, from the repo. - [**Deploy Django** Deploy Django to your own VPS without Docker: gunicorn, PostgreSQL, migrations, static files served by Caddy, uploads kept across deploys, in one ox.toml. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/django) - [**Deploy Rails** Deploy Rails 8 to your own VPS without Docker or Kamal: precompiled Ruby, gems, assets, db:prepare and Solid Cache, Queue and Cable on PostgreSQL. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/rails) - [**Deploy Laravel** Deploy Laravel to your own Ubuntu VPS: PHP from apt, Composer, a Vite build, PostgreSQL, a queue worker and the scheduler every minute, from one ox.toml. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/laravel) - [**Symfony** Deploy Symfony to your own VPS: ox runs composer install for prod, Doctrine migrations and FrankenPHP under systemd, and asks for APP_SECRET first. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/symfony) - [**Plain PHP** Deploy a plain PHP site to your own VPS: ox finds index.php, installs FrankenPHP, and serves the site under systemd behind Caddy with HTTPS, no ox.toml. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/php) - [**Phoenix** Deploy an Elixir Phoenix app to your own VPS: ox builds a mix release with its assets, runs Release.migrate, starts it under systemd, and adds PostgreSQL. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/phoenix) ### APIs, backends and workers A long-running server or worker, with the database it uses. - [**Deploy a Node API** Deploy an Express, Fastify or other Node.js API to your own VPS: start script, lockfile install, migrations, PostgreSQL and Redis, from one short ox.toml. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/node) - [**Deploy FastAPI** Deploy FastAPI to your own VPS: ox detects the uvicorn start command, installs from uv.lock, runs Alembic migrations, and adds PostgreSQL from one ox.toml. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/fastapi) - [**NestJS** Deploy a NestJS API to your own VPS: ox installs from package-lock.json, runs nest build and start:prod under systemd, and sets up PostgreSQL for the app. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/nestjs) - [**Fastify** Deploy a Fastify API to your own VPS: ox finds server.js, runs node server.js under systemd behind Caddy, and adds PostgreSQL with DATABASE_URL set. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/fastify) - [**Hono on Bun** Deploy a Hono API on Bun to your own VPS: ox installs Bun, runs bun install from bun.lock, and starts src/index.ts under systemd behind Caddy. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/hono-bun) - [**BullMQ worker** Deploy a Node app with a BullMQ queue to your own VPS: ox adds Redis with REDIS_URL set, runs your server, and the [workers] line runs worker.js beside it. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/bullmq-worker) - [**Flask** Deploy a Flask app to your own VPS: ox installs with uv from requirements.txt, runs gunicorn app:app under systemd behind Caddy, and adds PostgreSQL. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/flask) - [**Spring Boot (Maven)** Deploy a Spring Boot app built with Maven to your own VPS: ox builds the jar with mvnw, installs Java 21, runs it under systemd and adds PostgreSQL for it. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/spring-boot) - [**Spring Boot (Gradle)** Deploy a Spring Boot app built with Gradle to your own VPS: ox runs gradlew bootJar, reads the Java version from build.gradle.kts, and runs the jar. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/spring-boot-gradle) - [**Quarkus** Deploy a Quarkus app to your own VPS in JVM mode: ox builds it with mvnw, runs quarkus-run.jar under systemd, checks /q/health and adds PostgreSQL for it. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/quarkus) - [**ASP.NET Core** Deploy an ASP.NET Core app to your own Linux VPS: ox installs the .NET 10 SDK, runs dotnet publish, starts the DLL under systemd, and adds PostgreSQL. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/aspnet-core) - [**Rust** Deploy a Rust web app from a Cargo workspace to your own VPS: ox installs stable Rust, runs cargo build --release --locked, and starts the binary. *Tested on a real server*](https://console.t58.sorv.dev/docs/guides/rust) ## More stacks ox has deployed or detected These have no page of their own yet. Each links to the page to start from. - [Django with Celery and Redis](https://console.t58.sorv.dev/docs/guides/django) *Tested on a real server* - [Node API and SPA in one repo](https://console.t58.sorv.dev/docs/guides/node) *Tested on a real server* - [Vue with Vite on Bun](https://console.t58.sorv.dev/docs/guides/static-site) *Tested on a real server* - [Go (net/http or Gin) with Postgres and Redis](https://console.t58.sorv.dev/docs/config#detection) *Tested on a real server* - [Python bot or worker with no web process](https://console.t58.sorv.dev/docs/config#workers) *Tested on a real server* - [Django ASGI (uvicorn or daphne) with Redis](https://console.t58.sorv.dev/docs/guides/django) *Tested on a real server* - [FastAPI with Redis, a worker and cron](https://console.t58.sorv.dev/docs/guides/fastapi) *Tested on a real server* - [Django with pgvector](https://console.t58.sorv.dev/docs/guides/django) *Tested on a real server* - [Python with Qdrant](https://console.t58.sorv.dev/docs/guides/fastapi) *Tested on a real server* - [FastAPI with Neo4j](https://console.t58.sorv.dev/docs/guides/fastapi) *Tested on a real server* - [CPU model inference (FastAPI and a Hugging Face model)](https://console.t58.sorv.dev/docs/guides/fastapi) *Tested on a real server* - [Playwright with headless Chromium (x64, Playwright below 1.61)](https://console.t58.sorv.dev/docs/config#build) *Tested on a real server* - [WordPress with MariaDB (needs an ox.toml)](https://console.t58.sorv.dev/docs/config#services) *Tested on a real server* - [Go Fiber, Echo or Chi](https://console.t58.sorv.dev/docs/config#detection) *Plan verified, real-server test pending* - [Strapi with Postgres](https://console.t58.sorv.dev/docs/guides/node) *Plan verified, real-server test pending* - [Hono on Node](https://console.t58.sorv.dev/docs/guides/node) *Plan verified, real-server test pending* - [Koa](https://console.t58.sorv.dev/docs/guides/node) *Plan verified, real-server test pending* - [Rust Actix Web](https://console.t58.sorv.dev/docs/guides/rust) *Plan verified, real-server test pending* - [Rust Rocket](https://console.t58.sorv.dev/docs/guides/rust) *Plan verified, real-server test pending* - [Eleventy](https://console.t58.sorv.dev/docs/guides/static-site) *Plan verified, real-server test pending* - [Payload CMS on Next.js](https://console.t58.sorv.dev/docs/guides/nextjs) *Plan verified, real-server test pending* - [Wagtail](https://console.t58.sorv.dev/docs/guides/django) *Plan verified, real-server test pending* - [Jekyll](https://console.t58.sorv.dev/docs/config#static) *Plan verified, real-server test pending* ## Works with an ox.toml, not yet tested ox does not detect these yet, and no test has deployed them. Write the start, build and services in an ox.toml, and check it with `ox check` first. - [Express with MySQL (mysql2)](https://console.t58.sorv.dev/docs/config#services) - [Streamlit](https://console.t58.sorv.dev/docs/config#app) - [Gradio](https://console.t58.sorv.dev/docs/config#app) - [Django with MariaDB (mysqlclient)](https://console.t58.sorv.dev/docs/config#services) - [Rails with Sidekiq and Redis](https://console.t58.sorv.dev/docs/config#workers) - [Rails with Solid Queue](https://console.t58.sorv.dev/docs/config#workers) - [Laravel with a queue worker and the scheduler](https://console.t58.sorv.dev/docs/guides/laravel) - [Express in TypeScript (tsc to dist)](https://console.t58.sorv.dev/docs/config#app) - [Hugo](https://console.t58.sorv.dev/docs/config#static) - [Telegram or Discord bot in Node](https://console.t58.sorv.dev/docs/config#workers) - [Kotlin Ktor](https://console.t58.sorv.dev/docs/config#build) - [n8n](https://console.t58.sorv.dev/docs/config#app) - [Uptime Kuma](https://console.t58.sorv.dev/docs/config#app) - [Express with SQLite (better-sqlite3)](https://console.t58.sorv.dev/docs/config#app) - [Flask with SQLite](https://console.t58.sorv.dev/docs/config#app) - [Go with SQLite](https://console.t58.sorv.dev/docs/config#app) - [Rust full-stack (Leptos or Dioxus)](https://console.t58.sorv.dev/docs/config#build) - [Bun with Elysia](https://console.t58.sorv.dev/docs/config#app) - [TanStack Start](https://console.t58.sorv.dev/docs/config#app) - [MkDocs or VitePress](https://console.t58.sorv.dev/docs/config#static) - [Directus](https://console.t58.sorv.dev/docs/config#app) - [Medusa](https://console.t58.sorv.dev/docs/config#app) - [Sinatra or Roda](https://console.t58.sorv.dev/docs/config#app) - [Hanami](https://console.t58.sorv.dev/docs/config#app) - [Odoo](https://console.t58.sorv.dev/docs/config#app) - [Drupal](https://console.t58.sorv.dev/docs/config#app) - [Slim PHP API](https://console.t58.sorv.dev/docs/config#app) - [CodeIgniter](https://console.t58.sorv.dev/docs/config#app) - [Laravel with Inertia (Vue or React)](https://console.t58.sorv.dev/docs/guides/laravel) - [Blazor Server](https://console.t58.sorv.dev/docs/config#build) - [Scala Play](https://console.t58.sorv.dev/docs/config#build) - [Clojure Ring](https://console.t58.sorv.dev/docs/config#build) - [Elixir Plug or Bandit without Phoenix](https://console.t58.sorv.dev/docs/config#build) - [Gleam](https://console.t58.sorv.dev/docs/config#build) - [Zig HTTP server](https://console.t58.sorv.dev/docs/config#build) - [C or C++ binary](https://console.t58.sorv.dev/docs/config#build) - [Dart (Shelf or Serverpod)](https://console.t58.sorv.dev/docs/config#build) ## Not supported - **Express with MongoDB:** ox runs no MongoDB: Ubuntu 26.04 has no MongoDB server package. Use a hosted one and set its URL as a variable. - **Ghost:** Ghost needs MySQL 8, and ox ships MariaDB. - **Meteor:** Meteor needs MongoDB, which ox does not run. - **Swift Vapor:** No Swift toolchain for Ubuntu 26.04 is verified yet. - **Puppeteer worker:** ox skips Puppeteer's Chrome download and installs none of its system packages, and Ubuntu 26.04's own Chromium is a snap, which ox does not run. - **GPU inference (vLLM, Ollama on a GPU):** ox does not manage GPUs or their drivers. - **Docker Compose app:** ox runs apps without Docker, by design. - **Windows, IIS or .NET Framework:** ox runs on Ubuntu only. Your stack is not here? The [detection rules](https://console.t58.sorv.dev/docs/config#detection) and the [config examples](https://console.t58.sorv.dev/docs/config#examples) cover more, and [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting) lists the messages every deploy can print. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides # Deploy Django to a VPS with gunicorn and Postgres https://console.t58.sorv.dev/docs/guides/django Deploy Django to your own VPS without Docker: gunicorn, PostgreSQL, migrations, static files served by Caddy, uploads kept across deploys, in one ox.toml. To deploy Django to a VPS with ox, add the repo as a project, let ox detect gunicorn, the migrate step and `collectstatic`, set `SECRET_KEY`, and deploy. ox runs gunicorn as a systemd service behind Caddy, with no Docker and no nginx to configure, and with the ox.toml below, Caddy serves your static files too. Migrations run after a database snapshot, and traffic moves to the new release only after its health check answers. ox reads the ox.toml on this page, makes the plan on the card, and starts `.venv/bin/gunicorn mysite.wsgi --bind 127.0.0.1:$PORT --workers 2` on your server. The cylinder is the service the file asks for: `postgres`. ## What ox detects in a Django repo A Django repo often deploys with no ox.toml at all. From the files at the repository root, ox proposes: - `uv venv --allow-existing && uv pip install -r requirements.txt` as the install step when there is a `requirements.txt`, or `uv sync --frozen --no-dev` when there is a `uv.lock`. - `.venv/bin/gunicorn mysite.wsgi --bind 127.0.0.1:$PORT` as the start command, with the project name read from `DJANGO_SETTINGS_MODULE` in `manage.py`. It picks uvicorn instead when the project has an `asgi.py` (or an `asgi/` package) and uvicorn is a dependency. - `manage.py migrate --noinput` as the migrate step, run after a snapshot of the database. - `manage.py collectstatic --noinput` as a build command, when `settings.py`, or a module of a `settings/` package (`base.py`, `production.py`), sets `STATIC_ROOT`, then `manage.py compress --force` when django-compressor is a dependency and the settings set `COMPRESS_OFFLINE = True`. - A hint about the production settings: when `manage.py`, `wsgi.py` or `asgi.py` defaults `DJANGO_SETTINGS_MODULE` to a module of the settings package other than `production` (bakerydemo's `dev`, cookiecutter-django's `local`) and `settings/production.py` exists, `ox check` and the deploy log say the app runs that module and name `DJANGO_SETTINGS_MODULE=.settings.production` to set. It is a hint, not a required key, and it goes once the key is set. - The headers a source-only package builds against, when the install reaches it: `libldap-dev` and `libsasl2-dev` for `python-ldap`, `libmysqlclient-dev` and `pkg-config` for `mysqlclient`, `libpq-dev` for `psycopg2` and `psycopg[c]`. The C compilers are on the server for every Python app. - PostgreSQL, when `psycopg`, `psycopg2` or `asyncpg` is a dependency, and Redis when `redis` or `celery[redis]` is. - `[storage] keep = ["db.sqlite3"]` when the settings are the ones `startproject` writes (SQLite the one engine, `NAME = BASE_DIR / "db.sqlite3"`), so every release starts on the same rows in the project's data folder. A `db.sqlite3` the repo commits fills that kept file on the first deploy only: a later commit of it never replaces the rows there, and the deploy log says so. When uwsgi is the only server, ox starts `.venv/bin/uwsgi --http-socket 127.0.0.1:$PORT --module mysite.wsgi:application --master --threads 4 --need-app --lazy-apps --hook-master-start 'unix_signal:15 gracefully_kill_them_all'`. It does not read your `uwsgi.ini`, because its pidfile and stats port cannot run under ox's service; `ox check` says so, and the `static-map` folders it served go under `[static] paths`. To run uwsgi your own way, set `[app] start`. When none of gunicorn, uvicorn and uwsgi is a dependency, `ox check` says `Django found, but none of gunicorn, uvicorn or uwsgi is a dependency; add one or set [app] start`. Add `gunicorn` to your requirements to fix it. ## The ox.toml for Django Write one when you want a domain, a health check, a cron job or uploads that survive deploys. Once a repo has an ox.toml, its services are exactly the ones it lists, so `postgres` is declared here. ox.toml for a Django project named mysite: ```toml domains = ["example.com"] [app] start = ".venv/bin/gunicorn mysite.wsgi --bind 127.0.0.1:$PORT --workers 2" health = "/healthz" [static] paths = { "/static" = "staticfiles" } [build] install = "uv venv --allow-existing && uv pip install -r requirements.txt" commands = [".venv/bin/python manage.py collectstatic --noinput"] migrate = ".venv/bin/python manage.py migrate --noinput" [cron] clearsessions = { schedule = "@daily", run = ".venv/bin/python manage.py clearsessions" } [services] postgres = {} [storage] keep = ["media"] ``` Caddy serves `/static` straight from `staticfiles` ([[static] paths](https://console.t58.sorv.dev/docs/config#static)), so WhiteNoise is not needed. `[storage] keep` links `media` to a writable folder that survives deploys, because each deploy starts a new release from the commit. ## Django settings and variables for production ox provides `DATABASE_URL`, `PUBLIC_URL` and `PUBLIC_HOST`, so `settings.py` reads them from the environment. settings.py: ```python import os import dj_database_url SECRET_KEY = os.environ["SECRET_KEY"] DEBUG = False ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",") CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in ALLOWED_HOSTS] DATABASES = {"default": dj_database_url.config()} STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "staticfiles" MEDIA_ROOT = BASE_DIR / "media" ``` You set `SECRET_KEY` on the dashboard or with `ox vars set SECRET_KEY`, which asks for the value. `ALLOWED_HOSTS` can point at ox's value as `${PUBLIC_HOST}`. Keys in `.env.example` that need your answer (empty, a secret, or a placeholder) are required, and a deploy waits until each one is set. ## Add a health check view ox sends traffic to a new release only after `/healthz` answers with a 2xx or 3xx status. A view that answers without touching the database keeps the check fast. urls.py, next to your other paths: ```python from django.http import HttpResponse from django.urls import path urlpatterns = [ path("healthz", lambda request: HttpResponse("ok")), ] ``` ## Check and deploy the Django app Run `ox check` in the repo. For the files above it prints the plan, then the variables to set, then `Ready to deploy.` ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, PUBLIC_SCHEME, PUBLIC_PORT, DATABASE_URL Set on the dashboard before the first deploy: SECRET_KEY, ALLOWED_HOSTS Ready to deploy. ``` Then deploy with `ox deploy --wait`. If the health check fails, [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed) explains the message. ## Next steps - [Set SECRET_KEY and other variables](https://console.t58.sorv.dev/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://console.t58.sorv.dev/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://console.t58.sorv.dev/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://console.t58.sorv.dev/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://console.t58.sorv.dev/docs/operate/backups) and restore one. Last updated 2026-10-16. The page as HTML: https://console.t58.sorv.dev/docs/guides/django # Deploy Next.js to a VPS: self-host without Vercel https://console.t58.sorv.dev/docs/guides/nextjs Self-host Next.js on your own VPS without Vercel or Docker: next start, a health check, PostgreSQL, Prisma migrations and the image cache, in one ox.toml. To self-host Next.js on a VPS with ox, add the repo as a project and deploy: ox detects your install, `next build` and `next start` from `package.json` and the lockfile, so most apps need no ox.toml. `next start` runs as a systemd service behind Caddy, with no Vercel, Docker or PM2. Add the short ox.toml below for your own domain, a real health check and PostgreSQL. ox reads the ox.toml on this page, makes the plan on the card, and starts `npm run start` on your server. The cylinder is the service the file asks for: `postgres`. ## What ox detects in a Next.js repo ox reads `package.json` and your lockfile. When `next` is a dependency, it proposes from the repo root: - The Node version from `devEngines.runtime` or `engines.node` in `package.json`, or from `.nvmrc`, `.node-version` or `mise.toml`. With none of them, Node 24. A range takes Node 24 when it allows it, so an `engines.node` of `>=22` installs Node 24, and an `engines.node` of `^20` installs Node 20. Once your app is live, it keeps its Node version until you change the range so it no longer allows it. - The install step from the lockfile: `npm ci --include=dev` for `package-lock.json`, `pnpm install --frozen-lockfile` (with `--prod=false` before pnpm 10), `yarn install --frozen-lockfile --production=false` (Yarn 1) or `bun install --frozen-lockfile`. The flags keep the `devDependencies` your build needs when you set `NODE_ENV=production`. - `npm run build` when there is a `build` script, else `npx next build` (with your package manager in place of npm). - `npm run start` when there is a `start` script that serves on the port ox gives. A script of `next dev` is a development server, and one that fixes its own port never gets `$PORT`, so for either, and with no script, ox runs Next.js itself: a script of `next start -p 3000` runs `npx next start -H 127.0.0.1 -p $PORT`, and a script of `next start` runs `npm run start`. - A static site in `out` when `next.config.*` sets `output: 'export'`, with no app process. - `npx prisma migrate deploy` as the migrate step when there is a `prisma/schema.prisma`. - When the repo has no ox.toml: PostgreSQL when `pg`, `postgres` or `@neondatabase/serverless` is a dependency (or the Prisma schema uses `postgresql`), and Redis when `redis`, `ioredis` or `bullmq` is. In a monorepo, the `dependencies` of each workspace member count too (npm or yarn `workspaces`, or `pnpm-workspace.yaml`), but not a member's `devDependencies`. Once your app is live, if you set `DATABASE_URL` yourself before ox found the driver, ox keeps using yours and runs no PostgreSQL. ox does not guess a health path. Without one it only checks that something accepts a connection on the port. `next start` reads `PORT` by itself, so the usual scripts work as they are. With `output: 'standalone'`, ox still runs `next start` and `ox check` prints a hint on how to run the standalone server instead. package.json, the scripts ox uses: ```json { "engines": { "node": ">=22" }, "scripts": { "build": "next build", "start": "next start" } } ``` ## The ox.toml for Next.js You can deploy with no ox.toml. Add this one when you want your own domain and a real health check. ox.toml for a Next.js app with PostgreSQL: ```toml domains = ["app.example.com"] [app] start = "npm run start" health = "/healthz" [build] install = "npm ci --include=dev" commands = ["npm run build"] [services] postgres = {} ``` - `domains`: the names your app answers on. ox gets the HTTPS certificate for each. - `start`: the command that runs your app. It is the same one ox detects, so you can leave it out. - `health`: a path that must answer with a 2xx or 3xx status before a new release gets traffic. - `install` and `commands`: install your packages, then build. These also match detection. - `postgres`: a database for this app. Leave it out if you do not use one. Each deploy starts a new release from the commit, so what the app writes there is gone on the next deploy. Next.js saves optimized images under `.next/cache`, so if you use `next/image`, add this ([[storage] keep](https://console.t58.sorv.dev/docs/config#tools)), and ox links that folder to a writable one that survives deploys. Add to ox.toml when you use next/image: ```toml [storage] keep = [".next/cache"] ``` The health path can be a tiny route handler that does not touch the database. app/healthz/route.js: ```javascript export function GET() { return new Response("ok"); } ``` ## PostgreSQL and Prisma migrations With `postgres = {}`, ox creates a database and gives your app `DATABASE_URL`. Add `redis = {}` and you also get `REDIS_URL`. Once a repo has an ox.toml, ox runs only the services it lists. If you use Prisma, every deploy takes a snapshot of the database and then runs `npx prisma migrate deploy`. ## Variables, secrets and NEXT_PUBLIC_ values ox also gives you `PORT`, `HOST`, `PUBLIC_URL` and `PUBLIC_HOST`. Set your own keys, like `AUTH_SECRET`, on the dashboard or with `ox vars set AUTH_SECRET`, which asks for the value. Every key in `.env.example` that needs your answer (empty, a secret, or a placeholder) must be set before a deploy can start; a key with a real example value keeps the app's default. Your variables are there during the build too. Next.js bakes `NEXT_PUBLIC_` values into the build, so a change to one needs a new deploy. `ox vars set` deploys again right away, unless you add `--no-deploy`. ## Check and deploy the Next.js app Check, deploy, and watch: ```sh ox check # in the repo: prints the plan and "Ready to deploy." ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` For the ox.toml above, `ox check` ends like this: ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, PUBLIC_SCHEME, PUBLIC_PORT, DATABASE_URL Set on the dashboard before the first deploy: AUTH_SECRET Ready to deploy. ``` ## If the Next.js deploy fails - `set these before deploying: AUTH_SECRET`. A key from `.env.example` has no value yet. Set it, then deploy again. - `GET http://127.0.0.1:/healthz did not answer within 120s`. The health path returned an error or a 404, or the app is not listening on `$PORT`. Check that the route exists, and that a custom server (a `server.js` your start script runs) listens on `process.env.PORT`. - `build 1/1: npm run build: …` with `fix: the command's own output is above; fix it in the repo and push`. `next build` failed, often on a type or lint error. Run `npm run build` on your machine, fix what it prints, and push. If an older release is live, it keeps serving while you fix any of these. More causes and fixes are in [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting), and every key is in the [config reference](https://console.t58.sorv.dev/docs/config). ## Next steps - [Set AUTH_SECRET and other variables](https://console.t58.sorv.dev/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://console.t58.sorv.dev/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://console.t58.sorv.dev/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://console.t58.sorv.dev/docs/operate/rollback) to a kept release without a rebuild. - [Turn on a preview per branch](https://console.t58.sorv.dev/docs/operate/previews), each with its own database. Last updated 2026-10-08. The page as HTML: https://console.t58.sorv.dev/docs/guides/nextjs # Deploy FastAPI to a VPS with uvicorn and Postgres https://console.t58.sorv.dev/docs/guides/fastapi Deploy FastAPI to your own VPS: ox detects the uvicorn start command, installs from uv.lock, runs Alembic migrations, and adds PostgreSQL from one ox.toml. To deploy FastAPI to a VPS with ox, add the repo as a project and deploy: ox detects the uvicorn start command, installs your packages from `uv.lock`, `poetry.lock` or `requirements.txt`, and runs Alembic migrations after a database snapshot. Uvicorn runs as a systemd service behind Caddy, which serves HTTPS on your domain. Write an ox.toml, as below, when you want a domain, a health path or PostgreSQL. ox reads the ox.toml on this page, makes the plan on the card, and starts `uv run uvicorn app.main:app --host 127.0.0.1 --port $PORT --workers 2` on your server. The cylinder is the service the file asks for: `postgres`. ## What ox detects in a FastAPI repo ox reads the Python files at the repo root. It proposes: - The install step from your lockfile: `uv sync --frozen --no-dev` for `uv.lock`, `poetry install --only main --no-root` for `poetry.lock`, `uv venv --allow-existing && uv pip install -r requirements.txt` for `requirements.txt`, or `uv sync --no-dev` for a `pyproject.toml` alone. - The Python version from `.python-version`, `.tool-versions` or `mise.toml`; with none of them, ox's default (`ox check` prints it) for `requirements.txt` and `poetry.lock`, and for `uv.lock` or a `pyproject.toml` alone when its `requires-python` allows that default (otherwise the nearest Python it allows, such as 3.11 for `>=3.10,<3.12`). A `uv` project with no pin used to run the server's own `python3`; it moves to ox's default at its next deploy, and a `.python-version` keeps any other version. - A start command, when `uvicorn` or `fastapi[standard]` is a dependency and one of `main.py`, `app.py`, `app/main.py` or `src/main.py` contains `FastAPI(`. ox reads the object's name from the line that makes it, such as `app = FastAPI()`. For `app/main.py` with a `uv.lock` that is `uv run uvicorn app.main:app --host 127.0.0.1 --port $PORT`. - The migrate step `uv run alembic upgrade head`, when `alembic.ini` is in the repo and `alembic` is a dependency. - When the repo has no ox.toml: PostgreSQL when `psycopg`, `psycopg2` or `asyncpg` is a dependency, and Redis when `redis` is. When ox cannot be sure, it proposes no start and `ox check` prints a hint saying why: uvicorn is not a dependency, the app is made inside a function, or the file makes more than one FastAPI object. Then set `[app] start` yourself. ## The ox.toml for FastAPI ox.toml for a FastAPI app with PostgreSQL and Alembic: ```toml domains = ["api.example.com"] [app] start = "uv run uvicorn app.main:app --host 127.0.0.1 --port $PORT --workers 2" health = "/health" [build] install = "uv sync --frozen --no-dev" migrate = "uv run alembic upgrade head" [services] postgres = {} ``` - `domains`: the names your API answers on. ox gets the HTTPS certificate for each. - `start`: runs uvicorn on the port ox picks, with two worker processes. - `health`: a path that must answer with a 2xx or 3xx status before a new release gets traffic. - `install`: installs your packages from `uv.lock`. - `migrate`: runs your Alembic migrations on every deploy, after ox takes a snapshot of the database. - `postgres`: a database for this app. The health route should answer without touching the database, so it stays fast. app/main.py: ```python from fastapi import FastAPI app = FastAPI() @app.get("/health") def health(): return {"ok": True} ``` ## PostgreSQL with SQLAlchemy and Alembic With `postgres = {}`, ox creates a database and gives your app `DATABASE_URL` ([[services]](https://console.t58.sorv.dev/docs/config#services)). Add `redis = {}` and you also get `REDIS_URL`. Once a repo has an ox.toml, ox runs only the services it lists. `DATABASE_URL` starts with `postgresql://`, which SQLAlchemy reads as the psycopg2 driver. With psycopg 3 or asyncpg, put the driver in the name when you read it. When SQLAlchemy is a dependency on one of them, `ox check` prints this line for your driver as a hint. app/db.py: ```python import os import re from sqlalchemy import create_engine url = re.sub(r"^postgres(ql)?://", "postgresql+psycopg://", os.environ["DATABASE_URL"]) engine = create_engine(url) ``` Point Alembic's `env.py` at the same `url`, so migrations and the app use one database. ## Variables and secrets ox also gives you `PORT`, `HOST`, `PUBLIC_URL` and `PUBLIC_HOST`. Set your own keys, like `SECRET_KEY`, on the dashboard or with `ox vars set SECRET_KEY`, which asks for the value. Every key in `.env.example` that needs your answer (empty, a secret, or a placeholder) must be set before a deploy can start; a key with a real example value keeps the app's default. ox never reads your `.env` file. ## Check and deploy the FastAPI app Check, deploy, and watch: ```sh ox check # in the repo: prints the plan and "Ready to deploy." ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` For the ox.toml above, `ox check` ends like this: ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, PUBLIC_SCHEME, PUBLIC_PORT, DATABASE_URL Set on the dashboard before the first deploy: SECRET_KEY hint: SQLAlchemy (from uv.lock) reads DATABASE_URL's postgresql:// as psycopg2, which it does not list; in your code, use re.sub(r"^postgres(ql)?://", "postgresql+psycopg://", os.environ["DATABASE_URL"]) Ready to deploy. ``` ## If the FastAPI deploy fails - `nothing to run: ox found no start command, static site, worker, or cron job; set [app] start (or declare [workers])`. `ox check` says this when ox found no start. The hint above it says why, such as `FastAPI found in main.py, but uvicorn is not a dependency`. Add `uvicorn` to your dependencies, or set `[app] start`. With no hint, your app file is somewhere ox does not look. - `the app exited while starting (…)`, or `GET http://127.0.0.1:/health did not answer within 120s`. Read the app's log above it. The usual causes are a wrong module path, such as `main:app` instead of `app.main:app`, or a `KeyError` for a missing variable. - `migrate: uv run alembic upgrade head: …` with `fix: the command's own output is above; fix it in the repo and push`. Alembic failed. Run the migration against a local database, fix it, and push. If an older release is live, it keeps serving while you fix any of these. More causes and fixes are in [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting), and every key is in the [config reference](https://console.t58.sorv.dev/docs/config). ## Next steps - [Set SECRET_KEY and other variables](https://console.t58.sorv.dev/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://console.t58.sorv.dev/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://console.t58.sorv.dev/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://console.t58.sorv.dev/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://console.t58.sorv.dev/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://console.t58.sorv.dev/docs/guides/fastapi # Deploy a Node.js or Express API to your own VPS https://console.t58.sorv.dev/docs/guides/node Deploy an Express, Fastify or other Node.js API to your own VPS: start script, lockfile install, migrations, PostgreSQL and Redis, from one short ox.toml. To deploy a Node.js API such as Express to a VPS with ox, give `package.json` a `start` script, listen on `PORT` and `HOST`, and deploy. ox detects the Node version, the lockfile install, your build script and the migrate tool, then runs the server as a systemd service behind Caddy, with no Docker or PM2. PostgreSQL and Redis come from the `[services]` table of the ox.toml below. ox reads the ox.toml on this page, makes the plan on the card, and starts `npm run start` on your server. The cylinders are the services the file asks for: `postgres` and `redis`. ## What ox detects in a Node.js repo ox reads `package.json` and your lockfile. From the repo root it proposes: - The Node version from `devEngines.runtime` or `engines.node` in `package.json`, or from `.nvmrc`, `.node-version` or `mise.toml`. With none of them, Node 24. A range takes Node 24 when it allows it, else the newest version below it the range allows: an `engines.node` of `>=18` installs Node 24, and an `engines.node` of `>=18 <21` installs Node 20. Once your app is live, it keeps its Node version until you change the range so it no longer allows it. - The install step from the lockfile: `npm ci --include=dev` for `package-lock.json`, `pnpm install --frozen-lockfile` (with `--prod=false` before pnpm 10), `yarn install --frozen-lockfile --production=false` (Yarn 1) or `bun install --frozen-lockfile`. The flags keep the `devDependencies` your build needs when you set `NODE_ENV=production`. With no lockfile, an install that freezes none: `npm install --include=dev`, `pnpm install --no-frozen-lockfile`, `yarn install` or `bun install`. - `npm run build` when there is a `build` script, such as a TypeScript compile, and `npm run start` when there is a `start` script (with your package manager in place of npm): a script of `node src/server.js` runs `npm run start`. A script that fixes its own port, such as `PORT=3000 node server.js`, never gets the port ox gives, so ox does not run it and `ox check` says why in a hint. - With no `start` script, `node `, but only when a web framework ox knows (Express, Fastify, Koa, Hapi, Restify, Polka or Hono's Node server) is a dependency. The file is the `main` field of `package.json` when that file is in the repo, or else the one `index.js`, `server.js` or `app.js` at the root. - The migrate step `npx drizzle-kit migrate` with a `drizzle.config.*`, `npx knex migrate:latest` with a `knexfile.*`, or `npx prisma migrate deploy` with `prisma/schema.prisma` (or `schema.prisma` beside a package.json), when the tool is a dependency. A monorepo's workspace member that holds the schema runs it from its own folder. - When the repo has no ox.toml: PostgreSQL when `pg`, `postgres` or `@neondatabase/serverless` is a dependency, and Redis when `redis`, `ioredis` or `bullmq` is. In a monorepo, the `dependencies` of each workspace member count too (npm or yarn `workspaces`, or `pnpm-workspace.yaml`), but not a member's `devDependencies`. Once your app is live, if you set `DATABASE_URL` yourself before ox found the driver, ox keeps using yours and runs no PostgreSQL. A `start` script is the surest way: when ox cannot tell which file is the server, such as a `main` that points into a `dist` folder your build makes, it proposes nothing and `ox check` says why in a hint. Your app must listen on the port and address ox gives it in `PORT` and `HOST`. src/server.js: ```javascript import express from "express"; const app = express(); app.get("/health", (req, res) => res.send("ok")); app.listen(process.env.PORT, process.env.HOST); ``` package.json, the scripts ox uses: ```json { "type": "module", "engines": { "node": "24" }, "scripts": { "start": "node src/server.js", "migrate": "node src/migrate.js" } } ``` ## The ox.toml for an Express API ox.toml for an Express API with PostgreSQL and Redis: ```toml domains = ["api.example.com"] [app] start = "npm run start" health = "/health" [build] install = "npm ci --include=dev" migrate = "npm run migrate" [services] postgres = {} redis = {} ``` - `domains`: the names your API answers on. ox gets the HTTPS certificate for each. - `start`: the command that runs your server. It is the same one ox detects, so you can leave it out. - `health`: a path that must answer with a 2xx or 3xx status before a new release gets traffic. - `install`: installs your packages from `package-lock.json`. - `migrate`: your own migration script, run on every deploy after ox takes a snapshot of the database. Leave it out if you have none. - `postgres` and `redis`: a database and a Redis for this app. Keep only the ones you use. ## PostgreSQL and Redis for a Node API ox gives your app `DATABASE_URL` for `postgres` and `REDIS_URL` for `redis` ([[services]](https://console.t58.sorv.dev/docs/config#services)). Both `pg` and `ioredis` take the URL as it is. Once a repo has an ox.toml, ox runs only the services it lists. src/db.js: ```javascript import pg from "pg"; import Redis from "ioredis"; export const db = new pg.Pool({ connectionString: process.env.DATABASE_URL }); export const redis = new Redis(process.env.REDIS_URL); ``` ## Variables and secrets ox also gives you `PUBLIC_URL` and `PUBLIC_HOST`. Set your own keys, like `SESSION_SECRET`, on the dashboard or with `ox vars set SESSION_SECRET`, which asks for the value. Every key in `.env.example` that needs your answer (empty, a secret, or a placeholder) must be set before a deploy can start; a key with a real example value keeps the app's default. ox does not set `NODE_ENV` for you, so set it to `production` yourself if your code checks it. ## Check and deploy the Node API Check, deploy, and watch: ```sh ox check # in the repo: prints the plan and "Ready to deploy." ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` For the ox.toml above, `ox check` ends like this: ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, PUBLIC_SCHEME, PUBLIC_PORT, DATABASE_URL, REDIS_URL Set on the dashboard before the first deploy: SESSION_SECRET Ready to deploy. ``` ## If the Node deploy fails - `nothing to run: ox found no start command, static site, worker, or cron job; set [app] start (or declare [workers])`. `ox check` says this when `package.json` has no `start` script and ox could not pick the server file; the hint above it says why. Add a `start` script, or set `[app] start`. - `GET http://127.0.0.1:/health did not answer within 120s`. The app listens on a fixed port like 3000 instead of `process.env.PORT`, or `/health` returns a 404. Use the port ox gives you. - `set these before deploying: SESSION_SECRET`. A key from `.env.example` has no value yet. Set it, then deploy again. If an older release is live, it keeps serving while you fix any of these. More causes and fixes are in [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting), and every key is in the [config reference](https://console.t58.sorv.dev/docs/config). ## Next steps - [Set SESSION_SECRET and other variables](https://console.t58.sorv.dev/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://console.t58.sorv.dev/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://console.t58.sorv.dev/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://console.t58.sorv.dev/docs/operate/rollback) to a kept release without a rebuild. - [See the daily database backups](https://console.t58.sorv.dev/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://console.t58.sorv.dev/docs/guides/node # Deploy Laravel to a VPS with a queue and scheduler https://console.t58.sorv.dev/docs/guides/laravel Deploy Laravel to your own Ubuntu VPS: PHP from apt, Composer, a Vite build, PostgreSQL, a queue worker and the scheduler every minute, from one ox.toml. To deploy Laravel to a VPS with ox, add the ox.toml below to your repo, set `APP_KEY` and the other variables, and deploy. ox installs PHP and Composer from Ubuntu's packages, builds your assets with Vite, runs `php artisan migrate --force` after a database snapshot, and runs the queue worker and the scheduler beside the app. ox's real-server tests deploy a plain PHP app this way, but no Laravel app yet. ox reads the ox.toml on this page, makes the plan on the card, and starts `php -S 127.0.0.1:$PORT ../vendor/laravel/framework/src/Illuminate/Foundation/resources/server.php` on your server. The cylinder is the service the file asks for: `postgres`. ## What ox detects in a Laravel repo ox sees a Laravel app from `composer.json` (with `laravel/framework`) and `artisan`. Laravel writes to `storage` while it runs, so a Laravel repo always needs an ox.toml with `[storage] keep`: without one, `ox check` stops with `Laravel writes storage/ at runtime, which is read-only in a release`. Inside your ox.toml, ox fills what you leave out: PHP, Composer and the extensions your `composer.json` and `composer.lock` require, from Ubuntu's packages; the install `composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader` (followed by your lockfile's `npm ci --include=dev`); your `build` script; the migrate step `php artisan migrate --force`; and the health page your `bootstrap/app.php` names with `health:` (`/up` in Laravel 11 and later). With no `[app] start`, ox proposes one that serves `public` with FrankenPHP, which no real-server test covers yet. If you set a start and `packages` has no PHP, `ox check` says so. The ox.toml below lists PHP's packages and runs your app with PHP's built-in web server, the same one `php artisan serve` uses. ox's real-server tests deploy a plain PHP app this way, but no Laravel app yet. ## The ox.toml for Laravel Put this file at the root of your repo, next to `artisan`. Change the domain to yours. ox.toml for a Laravel app: ```toml domains = ["example.com"] packages = ["php-cli", "php-mbstring", "php-xml", "php-curl", "php-zip", "php-intl", "php-bcmath", "php-pgsql", "unzip", "composer"] [app] start = "cd public && exec php -S 127.0.0.1:$PORT ../vendor/laravel/framework/src/Illuminate/Foundation/resources/server.php" health = "/up" [build] install = "composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader && npm ci --include=dev" commands = ["npm run build", "php artisan config:cache", "php artisan route:cache"] migrate = "php artisan migrate --force" [workers] queue = "php artisan queue:work --tries=3" [cron] scheduler = { schedule = "* * * * *", run = "php artisan schedule:run" } [services] postgres = {} [storage] keep = ["storage"] [tools] node = "24" ``` What each key does: - `domains` is the name Caddy serves your app on, with HTTPS. - `packages` installs PHP, the extensions Laravel needs, and Composer from Ubuntu. - `[app] start` makes the folders Laravel writes to, then starts PHP's web server on the port ox gives it. - `[app] health` is Laravel's built-in `/up` page, which must answer before your app gets traffic. - `[build] install` installs your PHP and JavaScript packages. - `[build] commands` builds your CSS and JavaScript, then caches the config and routes. - `[build] migrate` runs your migrations after ox takes a snapshot of the database. - `[workers] queue` runs the queue worker as its own process next to the app ([[workers]](https://console.t58.sorv.dev/docs/config#workers)). - `[cron] scheduler` runs Laravel's scheduler every minute ([[cron]](https://console.t58.sorv.dev/docs/config#cron)). - `[services] postgres` gives your app its own PostgreSQL database. - `[storage] keep` makes `storage` writable and keeps it across deploys, because a PHP release's own files are read-only while it runs. - When the repo has `storage/app/public`, ox's build links it to `public/storage` (what `php artisan storage:link` does), so files on Laravel's public disk are served at `/storage`. A `[build] commands` list in your ox.toml replaces the detected build steps, this link included: add `ln -sfn ../storage/app/public public/storage` to it if your app uses the public disk. - `[tools] node` installs Node.js for the Vite build. PHP's built-in server answers one request at a time. Set the variable `PHP_CLI_SERVER_WORKERS` (for example to `4`) to run more at once. ## PostgreSQL, MySQL and Redis for Laravel `postgres = {}` gives your app `DATABASE_URL`. Laravel reads its database from `DB_CONNECTION` and `DB_URL` instead, so ox sets both from the service for you, once your `config/database.php` shows the app reads them. Your own value for either one wins. - For MySQL, use `mysql = {}` instead. ox gives you `MYSQL_URL`, served by MariaDB, and sets `DB_CONNECTION=mysql` and the URL key your config reads (`DB_URL`, or `DATABASE_URL` in a Laravel 10 config). ox adds `php-mysql` to `packages` for you, as it adds `php-pgsql` for `postgres = {}`. - For Redis, add `redis = {}`. ox gives you `REDIS_URL`, which Laravel reads as is. Add `php-redis` to `packages`. ## APP_KEY, the .env values and trusted proxies ox never reads your `.env` file. You set each value on the dashboard's Variables tab, or with `ox vars set`, which asks for the value so it never lands in your shell history. Variables for this ox.toml: ```text APP_KEY=base64:... # from php artisan key:generate --show APP_ENV=production APP_DEBUG=false APP_URL=${PUBLIC_URL} LOG_CHANNEL=stderr ``` Make the `APP_KEY` on your own computer with `php artisan key:generate --show`, then run `ox vars set APP_KEY` and paste it. `${PUBLIC_URL}` points at a value ox provides. `LOG_CHANNEL=stderr` sends Laravel's log to `ox logs`. Every key in your `.env.example` with an empty value, a secret name or a placeholder must have a value before the first deploy, even an empty one. A key with a real value (`APP_DEBUG=false`) keeps Laravel's own default. With `postgres` or `mysql` in `[services]`, ox sets `DB_CONNECTION` and the key your `config/database.php` reads the URL from, because Laravel 11 never reads `DATABASE_URL`: `DB_URL`, or `DATABASE_URL` in a Laravel 10 config. ox reads that file and sets nothing it already covers: `DB_CONNECTION` when it defaults to `pgsql` (or `mysql`), and the url key when the connection's `url` falls back to the URL ox provides (`env('DATABASE_URL')` for postgres, `env('MYSQL_URL')` for mysql). If your `.env.example` lists `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME` and `DB_PASSWORD`, ox sets them from the service too, and `ox check` lists them under "From postgres". A value that names your own machine (`localhost`) in another key is asked. The config cache is made during the build, so a changed variable needs a new deploy. **Save & deploy** on the Variables tab does that. Caddy sits in front of your app. Tell Laravel to trust it, so links and redirects use HTTPS: bootstrap/app.php: ```php ->withMiddleware(function (Middleware $middleware) { $middleware->trustProxies(at: '127.0.0.1'); }) ``` ## Check and deploy the Laravel app Run `ox check` in the repo. It works offline and lists the plan, then the variables to set, then `Ready to deploy.` Here are its last lines for ox's own Laravel test app with the ox.toml above and a stock Laravel 11 `config/database.php`. Your `.env.example` decides which keys it asks for, and your `config/database.php` decides which keys come from postgres. ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, PUBLIC_SCHEME, PUBLIC_PORT, DATABASE_URL From postgres (a value of yours replaces one): DB_CONNECTION, DB_URL Set on the dashboard before the first deploy: APP_KEY (App key: base64: and 32 random bytes, Generate makes it) hint: [build] commands replaces the detected build step "ln -sfn ../storage/app/public public/storage" (from artisan); add it to the list if the release still needs it hint: the example file's 1 key with a real value (APP_NAME) is not asked and not copied, so the app sees it unset: set on the Variables screen any the app reads with no default of its own Ready to deploy. ``` Then ship it and watch the app's log: Ship from a terminal: ```sh ox check ox deploy --wait ox logs --follow ``` ## If the Laravel deploy fails - `set these before deploying: APP_KEY`: a key from `.env.example` has no value. Set it with `ox vars set APP_KEY` and deploy again. - `Read-only file system` in the app's log for a path under `storage`: `storage` is missing from `[storage] keep`. Add it as in the file above. - `GET http://127.0.0.1:/up did not answer within 120s`: the app did not answer on its health page. Laravel 10 and older have no `/up` page, so add a route that returns a 200, or point `health` at a page you have. Your visitors never see a failed deploy: the previous release keeps serving. [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting) explains each message, and the [config reference](https://console.t58.sorv.dev/docs/config) lists every key. ## Next steps - [Set APP_KEY and the other variables](https://console.t58.sorv.dev/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://console.t58.sorv.dev/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://console.t58.sorv.dev/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://console.t58.sorv.dev/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://console.t58.sorv.dev/docs/operate/backups) and restore one. Last updated 2026-10-17. The page as HTML: https://console.t58.sorv.dev/docs/guides/laravel # Deploy Ruby on Rails to a VPS with Puma and Postgres https://console.t58.sorv.dev/docs/guides/rails Deploy Rails 8 to your own VPS without Docker or Kamal: precompiled Ruby, gems, assets, db:prepare and Solid Cache, Queue and Cable on PostgreSQL. To deploy Ruby on Rails to a VPS with ox, add the repo as a project, let ox detect Puma, the gems, the asset build and `db:prepare`, set `RAILS_MASTER_KEY`, and deploy. ox installs a precompiled Ruby, runs Puma as a systemd service behind Caddy, with no Docker, no Kamal and no nginx to configure, and gives each of Rails 8's databases its own PostgreSQL database. Migrations run after a snapshot of every database, and traffic moves to the new release only after `/up` answers. ox reads the ox.toml on this page, makes the plan on the card, and starts `bundle exec rails server -b 127.0.0.1 -p $PORT -P /tmp/server.pid` on your server. The cylinders are 2 of the 4 services the file asks for: `postgres` and `cable`. ## What ox detects in a Rails repo An app made with `rails new --database=postgresql` deploys with no ox.toml at all. From `Gemfile.lock`, `config/` and the Ruby version file, ox proposes: - Ruby from `.ruby-version`, the `ruby` line in the `Gemfile`, or `RUBY VERSION` in `Gemfile.lock`, installed as a precompiled build, so nothing compiles Ruby on your server. - `bundle config set --local deployment true && bundle config set --local without development:test && bundle install` as the install step. Gems go into the release's own `vendor/bundle`, so two releases never share or break each other's gems. - `RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile` as a build command, when the app uses Propshaft, Sprockets, jsbundling, cssbundling or Vite Ruby. - `RAILS_ENV=production bundle exec rails db:prepare` as the migrate step. It runs pending migrations and loads the schema of a database that is still empty, which the Solid Cache, Queue and Cable databases need on the first deploy. - `RAILS_ENV=production bundle exec rails server -b 127.0.0.1 -p $PORT -P /tmp/server.pid` as the start command when the app has `bin/rails` (Rails loads the app before Puma reads `config/puma.rb`, so a `puma.rb` that calls the app works; without `bin/rails` ox starts Puma directly). When a Ruby file under `config/` (`application.rb`, `environments/production.rb`, an initializer, `puma.rb`) reads `RAILS_LOG_TO_STDOUT` or `RAILS_SERVE_STATIC_FILES`, as apps made before Rails 7.1 do, the start sets that variable to `true` unless you set it yourself, so the log reaches ox's log view and Puma serves the files in `public/`; a value you set, such as an empty one to turn a switch off, reaches the app unchanged. ox also proposes the folders `tmp/cache`, `tmp/pids` and `tmp/sockets`, which each release gets whatever `[build]` says (Puma writes its pid file into `tmp/pids` without making it, and a repo that ignores `tmp` ships none), and `/up` as the health check when `config/routes.rb` routes `/up` to `rails/health#show`, as Rails 7.1 and later generate. - PostgreSQL when `pg` is in the lock, plus one more PostgreSQL database for each extra production database in `config/database.yml` (`cache`, `queue`, `cable`), and Redis when `redis` or `sidekiq` is. - The apt packages that native gems build against, such as `libvips-dev` for `ruby-vips` or `libpq-dev` for a `pg` without a Linux build in the lock. The C compilers (`gcc`, `make`) always come with Ruby. - `RAILS_MASTER_KEY` as a required variable when `config/credentials.yml.enc` is in the repo. ## The ox.toml for Rails Write one when you want a domain, a Solid Queue worker or uploads that survive deploys. Once a repo has an ox.toml, its services are exactly the ones it lists, so the four databases are declared here. ox names each database URL after its entry, so `cache` gives `CACHE_DATABASE_URL`, which Rails reads for its `cache` database. ox.toml for a Rails 8 app: ```toml domains = ["example.com"] [app] start = "RAILS_ENV=production bundle exec rails server -b 127.0.0.1 -p $PORT -P /tmp/server.pid" health = "/up" [build] install = "bundle config set --local deployment true && bundle config set --local without development:test && bundle install" commands = ["RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile"] migrate = "RAILS_ENV=production bundle exec rails db:prepare" [workers] jobs = "RAILS_ENV=production bin/jobs" [services] postgres = {} cache = { type = "postgres" } queue = { type = "postgres" } cable = { type = "postgres" } [storage] keep = ["storage"] ``` ox sets no framework variables of its own, so each command carries `RAILS_ENV=production`. The `-b` flag makes Puma listen only on the port ox gives it, behind Caddy. `[workers] jobs` runs Solid Queue as its own service, and `[storage] keep` links `storage` to a folder that survives deploys, because each deploy starts a new release from the commit, without what the app wrote in the last one. Ruby and the packages still come from detection, and `ox check` shows them. ## Rails credentials and variables for production ox provides `DATABASE_URL`, one `*_DATABASE_URL` per extra database, `PUBLIC_URL` and `PUBLIC_HOST`. Rails reads `DATABASE_URL` for the primary database and `CACHE_DATABASE_URL`, `QUEUE_DATABASE_URL` and `CABLE_DATABASE_URL` for the others, and they win over the names and passwords in `config/database.yml`, so the file `rails new` wrote works unchanged. Set `RAILS_MASTER_KEY` to the contents of `config/master.key` on the dashboard or with `ox vars set RAILS_MASTER_KEY`, which asks for the value. Never commit `master.key`. Until the key is set, a deploy stops before it builds and names the missing key. ## Check and deploy the Rails app Run `ox check` in the repo. For a `rails new` app with the ox.toml above it prints the plan, then the variables, then `Ready to deploy.` ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, PUBLIC_SCHEME, PUBLIC_PORT, CABLE_DATABASE_URL, CACHE_DATABASE_URL, DATABASE_URL, QUEUE_DATABASE_URL Set on the dashboard before the first deploy: RAILS_MASTER_KEY Ready to deploy. ``` Then deploy with `ox deploy --wait`. The first deploy installs Ruby and every gem, so it takes a few minutes. If the health check fails, [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed) explains the message. ## If ox check says no These are the messages `ox check` prints for a Rails repo it cannot deploy yet, and what to do about each. - `Gemfile.lock is missing: run bundle install and commit it (ox installs gems in deployment mode, which needs the lock)`. Commit the lock. - `a Ruby app needs a pinned Ruby, and ox never guesses one: add .ruby-version (or [tools] ruby = "3.4")`. Commit a `.ruby-version`; `rails new` writes one. - `Gemfile.lock has no Linux platform, so bundle install will refuse it on the server: run bundle lock --add-platform x86_64-linux and commit the lock`. This happens when the lock was made on a Mac. - `Rails with SQLite keeps its database in storage/, and the next release starts from the commit without it: add an ox.toml with [storage] keep = ["storage"]`. With that line the database lives in a folder that survives deploys. When `config/database.yml` names every production database as `rails new` writes it, a plain `storage/.sqlite3`, ox keeps `storage` on its own and `ox check` shows `storage.keep` from detection, so this shows only for a file it cannot read for certain (ERB, a `url`, a database elsewhere). - `Solid Queue runs jobs only in a worker: add [workers] jobs = "RAILS_ENV=production bin/jobs"`. This is a hint: jobs wait in the queue until a worker runs. It does not show when `config/puma.rb` loads `plugin :solid_queue` in every environment, since Puma then runs the jobs itself. Sidekiq and Resque get the same hint with their own command (`resque-pool` when the lock has it). ## Next steps - [Set RAILS_MASTER_KEY and other variables](https://console.t58.sorv.dev/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://console.t58.sorv.dev/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://console.t58.sorv.dev/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://console.t58.sorv.dev/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://console.t58.sorv.dev/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://console.t58.sorv.dev/docs/guides/rails # Deploy a static site to a VPS: Vite SPA or plain HTML https://console.t58.sorv.dev/docs/guides/static-site 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. ox reads the ox.toml on this page, makes the plan on the card, and builds the site and serves `dist` as files on your server. ## 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. - **Jekyll:** a `Gemfile` with `jekyll` (or `github-pages`), its `Gemfile.lock`, a `_config.yml`, and a pinned Ruby (`.ruby-version`). ox installs the gems, runs `jekyll build` in production and serves `_site`, or the `destination` your config names. It is the plan only when the repo holds no other stack's app. With the `github-pages` gem and no `repository:` in `_config.yml`, set `PAGES_REPO_NWO` to `owner/repo`: a release has no `.git` to read the name from. - **Other generators:** Docusaurus (`build`), Eleventy (`_site`), Hexo (`public`, with a `_config.yml` and a `source` folder), VitePress (`.vitepress/dist`), 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; VitePress needs a `build` script that is just `vitepress build` (or `vitepress build docs`, which serves `docs/.vitepress/dist`). Hexo and VitePress are used only when no web server library is a dependency, so an Express app that also builds docs stays the Express app. A Nuxt app whose `build` script runs `nuxt build` is a server, not a static site, unless its config sets `ssr: false`. For a build, the install comes from your lockfile (`npm ci --include=dev` for `package-lock.json`, `pnpm install --frozen-lockfile` (with `--prod=false` before pnpm 10), `yarn install --frozen-lockfile --production=false` or `bun install --frozen-lockfile`: the flags keep the `devDependencies` the build needs), 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 page is also served without its `.html`: `guide/intro.html` answers at `/guide/intro` (VitePress' `cleanUrls` needs this), unless `/guide/intro` is a file or a folder of its own. 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. A start script that only runs a development server, like `vite`, `react-scripts start` or `ng serve`, is never run. ## 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`. If your build prerenders pages (vite-ssg with `dirStyle: 'nested'`), each page in a folder, like `story/index.html`, answers at `/story`, and a `200.html` beside them is sent instead of `index.html` for every other path. A page named `story.html` is not served at `/story` in a single-page app. Leave it out for Astro and plain HTML, where each page is its own file. A missing file like `/favicon.ico` is a 404, never `index.html`, and a `404.html` in the built folder is shown for it. - `[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://console.t58.sorv.dev/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 --include=dev 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, PUBLIC_SCHEME, PUBLIC_PORT 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 folder`: 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://console.t58.sorv.dev/docs/troubleshooting#build-failed) explains each message, and the [config reference](https://console.t58.sorv.dev/docs/config#static) lists every `[static]` key. ## Next steps - [Set build-time variables](https://console.t58.sorv.dev/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://console.t58.sorv.dev/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read a deploy's build log](https://console.t58.sorv.dev/docs/operate/logs#run-logs) with `ox logs --run `. - [Roll back a bad deploy](https://console.t58.sorv.dev/docs/operate/rollback) to a kept release without a rebuild. - [Turn on a preview per branch](https://console.t58.sorv.dev/docs/operate/previews) to check a change before it goes live. Last updated 2026-10-08. The page as HTML: https://console.t58.sorv.dev/docs/guides/static-site # Deploy a React app built with Vite to your own VPS https://console.t58.sorv.dev/docs/guides/react-vite Deploy a React app built with Vite to your own VPS with no Dockerfile: ox runs the build from your lockfile and Caddy serves dist over HTTPS, no ox.toml. To deploy a React Vite app to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `package.json`, runs `npm install --include=dev` and `npm run build`, and Caddy serves `dist` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A React with Vite app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `package.json`, builds the plan on the card, and serves `dist` as files on your server. ## What ox detects in a React with Vite repo This is the plan `ox check` prints for a minimal a React app built with Vite, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `dist` | `detected:package.json` | | `static.spa` | `true` | `detected:package.json` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for React with Vite None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["www.example.com"] [static] dir = "dist" ``` ## Check and deploy React with Vite Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for React with Vite None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the React with Vite deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). - `package.json's start script (vite) runs a development server, not a production one; set [app] start to the command that serves the built app`: ox never runs a development server in production. Remove the start script, or point it at the built app. [More](https://console.t58.sorv.dev/docs/config#app). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The static site guide](https://console.t58.sorv.dev/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## React with Vite FAQ ### Do I need a Dockerfile or an ox.toml to deploy a React Vite app to a VPS? No. ox does not use Docker, and `ox check` on a React with Vite repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for React with Vite? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Can the app call an API in another project? Yes. Build the API URL in at build time: your variables are there during the build, so Vite reads `VITE_API_URL` from `import.meta.env`. Everything in the build is public, so never put a secret in it. ## Related guides - [**Create React App** Deploy a Create React App project to your own VPS: ox runs react-scripts build and Caddy serves the build folder over HTTPS. No Docker, no ox.toml.](https://console.t58.sorv.dev/docs/guides/create-react-app) - [**Vue CLI** Deploy a Vue CLI app to your own VPS: ox installs from yarn.lock, runs your build, and Caddy serves dist over HTTPS. The plan comes from the repo alone.](https://console.t58.sorv.dev/docs/guides/vue) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/react-vite # Deploy a Create React App build to your own VPS https://console.t58.sorv.dev/docs/guides/create-react-app Deploy a Create React App project to your own VPS: ox runs react-scripts build and Caddy serves the build folder over HTTPS. No Docker, no ox.toml. To deploy a Create React App to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `package.json`, runs `npm install --include=dev` and `npm run build`, and Caddy serves `build` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Create React App app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `package.json`, builds the plan on the card, and serves `build` as files on your server. ## What ox detects in a Create React App repo This is the plan `ox check` prints for a minimal a Create React App project, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `build` | `detected:package.json` | | `static.spa` | `true` | `detected:package.json` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Create React App None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["www.example.com"] [static] dir = "build" ``` ## Check and deploy Create React App Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for Create React App None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Create React App deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). - `package.json's start script (vite) runs a development server, not a production one; set [app] start to the command that serves the built app`: ox never runs a development server in production. Remove the start script, or point it at the built app. [More](https://console.t58.sorv.dev/docs/config#app). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The static site guide](https://console.t58.sorv.dev/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## Create React App FAQ ### Do I need a Dockerfile or an ox.toml to deploy a Create React App to a VPS? No. ox does not use Docker, and `ox check` on a Create React App repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Create React App? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Does ox run react-scripts start? No. `react-scripts start` is a development server, and ox never runs one in production. ox runs your `build` script and serves the `build` folder as files. ## Related guides - [**Vue CLI** Deploy a Vue CLI app to your own VPS: ox installs from yarn.lock, runs your build, and Caddy serves dist over HTTPS. The plan comes from the repo alone.](https://console.t58.sorv.dev/docs/guides/vue) - [**Angular** Deploy an Angular single-page app to your own VPS: ox reads the output folder from angular.json, runs ng build, and Caddy serves it, with no Docker.](https://console.t58.sorv.dev/docs/guides/angular) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/create-react-app # Deploy a Vue app to a VPS: build, HTTPS, no Docker https://console.t58.sorv.dev/docs/guides/vue Deploy a Vue CLI app to your own VPS: ox installs from yarn.lock, runs your build, and Caddy serves dist over HTTPS. The plan comes from the repo alone. To deploy a Vue app to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `vue.config.js`, `yarn.lock` and `package.json`, runs `yarn install --frozen-lockfile --production=false` and `yarn run build`, and Caddy serves `dist` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Vue CLI app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `vue.config.js`, `yarn.lock` and `package.json`, builds the plan on the card, and serves `dist` as files on your server. ## What ox detects in a Vue CLI repo This is the plan `ox check` prints for a minimal a Vue app made with Vue CLI, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `dist` | `detected:vue.config.js` | | `static.spa` | `true` | `detected:vue.config.js` | | `build.install` | `yarn install --frozen-lockfile --production=false` | `detected:yarn.lock` | | `build.commands[0]` | `yarn run build` | `detected:package.json` | | `tools.node` | `24` | `default` | | `tools.yarn` | `1` | `default` | ## The ox.toml for Vue CLI None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["www.example.com"] [static] dir = "dist" ``` ## Check and deploy Vue CLI Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for Vue CLI None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Vue CLI deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The static site guide](https://console.t58.sorv.dev/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## Vue CLI FAQ ### Do I need a Dockerfile or an ox.toml to deploy a Vue app to a VPS? No. ox does not use Docker, and `ox check` on a Vue CLI repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Vue CLI? Only what the plan names: `node 24` (ox's default) and `yarn 1` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Does ox use yarn when my repo has yarn.lock? Yes. The install comes from your lockfile, so a `yarn.lock` gives `yarn install --frozen-lockfile --production=false` (Yarn 1; the flag keeps the build tools when you set `NODE_ENV=production`) and the build runs with yarn too, as the plan above shows. ## Related guides - [**Angular** Deploy an Angular single-page app to your own VPS: ox reads the output folder from angular.json, runs ng build, and Caddy serves it, with no Docker.](https://console.t58.sorv.dev/docs/guides/angular) - [**Angular SSR** Deploy an Angular app with server-side rendering to your own VPS: ox builds it, runs the server.mjs Angular writes under systemd, and Caddy adds HTTPS.](https://console.t58.sorv.dev/docs/guides/angular-ssr) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/vue # Deploy an Angular app to a VPS with Caddy and HTTPS https://console.t58.sorv.dev/docs/guides/angular Deploy an Angular single-page app to your own VPS: ox reads the output folder from angular.json, runs ng build, and Caddy serves it, with no Docker. To deploy an Angular app to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `angular.json`, `package-lock.json` and `package.json`, runs `npm ci --include=dev` and `npm run build`, and Caddy serves `dist/shop/browser` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Angular app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `angular.json`, `package-lock.json` and `package.json`, builds the plan on the card, and serves `dist/shop/browser` as files on your server. ## What ox detects in a Angular repo This is the plan `ox check` prints for a minimal an Angular single-page app, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `dist/shop/browser` | `detected:angular.json` | | `static.spa` | `true` | `detected:angular.json` | | `build.install` | `npm ci --include=dev` | `detected:package-lock.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Angular None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["www.example.com"] [static] dir = "dist/shop/browser" ``` ## Check and deploy Angular Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for Angular None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Angular deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [Angular SSR](https://console.t58.sorv.dev/docs/guides/angular-ssr): Deploy an Angular app with server-side rendering to your own VPS. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## Angular FAQ ### Do I need a Dockerfile or an ox.toml to deploy an Angular app to a VPS? No. ox does not use Docker, and `ox check` on a Angular repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Angular? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Which folder does ox serve for Angular? The `browser` folder under the `outputPath` in `angular.json`, here `dist/shop/browser`. Angular's application builder writes the files there. ## Related guides - [**Angular SSR** Deploy an Angular app with server-side rendering to your own VPS: ox builds it, runs the server.mjs Angular writes under systemd, and Caddy adds HTTPS.](https://console.t58.sorv.dev/docs/guides/angular-ssr) - [**Next.js static export** Deploy a Next.js app with output: 'export' to your own VPS: ox runs next build and Caddy serves the out folder over HTTPS, with no Node process running.](https://console.t58.sorv.dev/docs/guides/nextjs-static-export) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/angular # Deploy Angular SSR to your own VPS with Node.js 24 https://console.t58.sorv.dev/docs/guides/angular-ssr Deploy an Angular app with server-side rendering to your own VPS: ox builds it, runs the server.mjs Angular writes under systemd, and Caddy adds HTTPS. To deploy Angular SSR to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `angular.json` and `package.json`, runs `npm install --include=dev` and `npm run build`, then starts `node dist/store/server/server.mjs` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Angular SSR app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `angular.json` and `package.json`, builds the plan on the card, and starts `node dist/store/server/server.mjs` on your server. ## What ox detects in a Angular SSR repo This is the plan `ox check` prints for a minimal an Angular app with server-side rendering, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `node dist/store/server/server.mjs` | `detected:angular.json` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Angular SSR None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy Angular SSR Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Angular SSR None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Angular SSR deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [Angular](https://console.t58.sorv.dev/docs/guides/angular): Deploy an Angular single-page app to your own VPS. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Angular SSR FAQ ### Do I need a Dockerfile or an ox.toml to deploy Angular SSR to a VPS? No. ox does not use Docker, and `ox check` on a Angular SSR repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Angular SSR? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### How does ox know my Angular app renders on the server? From `angular.json`: with server rendering on, ox runs the `server.mjs` the build writes under the output path, here `dist/store/server/server.mjs`. ## Related guides - [**Next.js static export** Deploy a Next.js app with output: 'export' to your own VPS: ox runs next build and Caddy serves the out folder over HTTPS, with no Node process running.](https://console.t58.sorv.dev/docs/guides/nextjs-static-export) - [**Gatsby** Deploy a Gatsby site to your own VPS: ox reads gatsby-config.js, runs gatsby build from package.json, and Caddy serves the public folder over HTTPS.](https://console.t58.sorv.dev/docs/guides/gatsby) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/angular-ssr # Deploy a Next.js static export to your own VPS https://console.t58.sorv.dev/docs/guides/nextjs-static-export Deploy a Next.js app with output: 'export' to your own VPS: ox runs next build and Caddy serves the out folder over HTTPS, with no Node process running. To deploy a Next.js static export to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `next.config.mjs` and `package.json`, runs `npm install --include=dev` and `npm run build`, and Caddy serves `out` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Next.js static export app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `next.config.mjs` and `package.json`, builds the plan on the card, and serves `out` as files on your server. ## What ox detects in a Next.js static export repo This is the plan `ox check` prints for a minimal a Next.js app with a static export, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `out` | `detected:next.config.mjs` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Next.js static export None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["www.example.com"] [static] dir = "out" ``` ## Check and deploy Next.js static export Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for Next.js static export None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Next.js static export deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Next.js guide](https://console.t58.sorv.dev/docs/guides/nextjs): `next start`, PostgreSQL and Prisma. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## Next.js static export FAQ ### Do I need a Dockerfile or an ox.toml to deploy a Next.js static export to a VPS? No. ox does not use Docker, and `ox check` on a Next.js static export repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Next.js static export? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### What if my Next.js app needs a server? Then leave out `output: 'export'`. ox runs `next start` for a Next.js server app; the [Next.js guide](https://console.t58.sorv.dev/docs/guides/nextjs) covers it. ## Related guides - [**Gatsby** Deploy a Gatsby site to your own VPS: ox reads gatsby-config.js, runs gatsby build from package.json, and Caddy serves the public folder over HTTPS.](https://console.t58.sorv.dev/docs/guides/gatsby) - [**Docusaurus** Deploy a Docusaurus documentation site to your own VPS: ox runs the build from package.json and Caddy serves the build folder over HTTPS, no ox.toml.](https://console.t58.sorv.dev/docs/guides/docusaurus) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/nextjs-static-export # Deploy a Gatsby site to your own VPS with HTTPS https://console.t58.sorv.dev/docs/guides/gatsby Deploy a Gatsby site to your own VPS: ox reads gatsby-config.js, runs gatsby build from package.json, and Caddy serves the public folder over HTTPS. To deploy a Gatsby site to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `gatsby-config.js` and `package.json`, runs `npm install --include=dev` and `npm run build`, and Caddy serves `public` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Gatsby app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `gatsby-config.js` and `package.json`, builds the plan on the card, and serves `public` as files on your server. ## What ox detects in a Gatsby repo This is the plan `ox check` prints for a minimal a Gatsby site, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `public` | `detected:gatsby-config.js` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Gatsby None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["www.example.com"] [static] dir = "public" ``` ## Check and deploy Gatsby Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for Gatsby None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Gatsby deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The static site guide](https://console.t58.sorv.dev/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## Gatsby FAQ ### Do I need a Dockerfile or an ox.toml to deploy a Gatsby site to a VPS? No. ox does not use Docker, and `ox check` on a Gatsby repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Gatsby? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Which folder does ox serve for Gatsby? `public`, the folder `gatsby build` writes. ox reads it from `gatsby-config.js` being there, as the plan above shows. ## Related guides - [**Docusaurus** Deploy a Docusaurus documentation site to your own VPS: ox runs the build from package.json and Caddy serves the build folder over HTTPS, no ox.toml.](https://console.t58.sorv.dev/docs/guides/docusaurus) - [**Astro** Deploy an Astro static site to your own VPS: ox reads astro.config.mjs, runs astro build, and Caddy serves dist over HTTPS with no app process running.](https://console.t58.sorv.dev/docs/guides/astro) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/gatsby # Deploy a Docusaurus docs site to your own VPS https://console.t58.sorv.dev/docs/guides/docusaurus Deploy a Docusaurus documentation site to your own VPS: ox runs the build from package.json and Caddy serves the build folder over HTTPS, no ox.toml. To deploy a Docusaurus site to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `docusaurus.config.js` and `package.json`, runs `npm install --include=dev` and `npm run build`, and Caddy serves `build` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Docusaurus app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `docusaurus.config.js` and `package.json`, builds the plan on the card, and serves `build` as files on your server. ## What ox detects in a Docusaurus repo This is the plan `ox check` prints for a minimal a Docusaurus documentation site, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `build` | `detected:docusaurus.config.js` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Docusaurus None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["docs.example.com"] [static] dir = "build" ``` ## Check and deploy Docusaurus Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for Docusaurus None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Docusaurus deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The static site guide](https://console.t58.sorv.dev/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## Docusaurus FAQ ### Do I need a Dockerfile or an ox.toml to deploy a Docusaurus site to a VPS? No. ox does not use Docker, and `ox check` on a Docusaurus repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Docusaurus? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Does Docusaurus search work on my own server? Docusaurus builds plain files, and Caddy serves them as they are. Search that runs in the browser works the same; a hosted search service needs its keys in your build variables. ## Related guides - [**Astro** Deploy an Astro static site to your own VPS: ox reads astro.config.mjs, runs astro build, and Caddy serves dist over HTTPS with no app process running.](https://console.t58.sorv.dev/docs/guides/astro) - [**Astro SSR** Deploy an Astro app with the @astrojs/node adapter to your own VPS: ox builds it, runs dist/server/entry.mjs under systemd, with Caddy and HTTPS in front.](https://console.t58.sorv.dev/docs/guides/astro-ssr) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/docusaurus # Deploy an Astro static site to your own VPS server https://console.t58.sorv.dev/docs/guides/astro Deploy an Astro static site to your own VPS: ox reads astro.config.mjs, runs astro build, and Caddy serves dist over HTTPS with no app process running. To deploy an Astro static site to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `astro.config.mjs` and `package.json`, runs `npm install --include=dev` and `npm run build`, and Caddy serves `dist` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Astro app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `astro.config.mjs` and `package.json`, builds the plan on the card, and serves `dist` as files on your server. ## What ox detects in a Astro repo This is the plan `ox check` prints for a minimal an Astro static site, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `dist` | `detected:astro.config.mjs` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Astro None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["www.example.com"] [static] dir = "dist" ``` ## Check and deploy Astro Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for Astro None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Astro deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). - `nothing to run: ox found no start command, static site, worker, or cron job; set [app] start (or declare [workers])`: `ox check` found nothing to serve, such as a config file it needs that is missing. Set `[app] start` or `[static] dir` in an ox.toml. [More](https://console.t58.sorv.dev/docs/config#app). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The static site guide](https://console.t58.sorv.dev/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## Astro FAQ ### Do I need a Dockerfile or an ox.toml to deploy an Astro static site to a VPS? No. ox does not use Docker, and `ox check` on a Astro repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Astro? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### What changes when I add a server adapter? The plan does. With `@astrojs/node` in `astro.config.mjs`, ox runs `node dist/server/entry.mjs` instead of serving files; the [Astro SSR guide](https://console.t58.sorv.dev/docs/guides/astro-ssr) shows that plan. ## Related guides - [**Astro SSR** Deploy an Astro app with the @astrojs/node adapter to your own VPS: ox builds it, runs dist/server/entry.mjs under systemd, with Caddy and HTTPS in front.](https://console.t58.sorv.dev/docs/guides/astro-ssr) - [**SvelteKit** Deploy a SvelteKit app with adapter-node to your own VPS: ox reads svelte.config.js, builds it, runs node build under systemd, and Caddy adds HTTPS.](https://console.t58.sorv.dev/docs/guides/sveltekit) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/astro # Deploy Astro SSR with the Node adapter to a VPS https://console.t58.sorv.dev/docs/guides/astro-ssr Deploy an Astro app with the @astrojs/node adapter to your own VPS: ox builds it, runs dist/server/entry.mjs under systemd, with Caddy and HTTPS in front. To deploy Astro SSR to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `astro.config.mjs` and `package.json`, runs `npm install --include=dev` and `npm run build`, then starts `node dist/server/entry.mjs` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Astro SSR app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `astro.config.mjs` and `package.json`, builds the plan on the card, and starts `node dist/server/entry.mjs` on your server. ## What ox detects in a Astro SSR repo This is the plan `ox check` prints for a minimal an Astro app with the Node adapter, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `node dist/server/entry.mjs` | `detected:astro.config.mjs` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Astro SSR None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy Astro SSR Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Astro SSR None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Astro SSR deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Astro SSR FAQ ### Do I need a Dockerfile or an ox.toml to deploy Astro SSR to a VPS? No. ox does not use Docker, and `ox check` on a Astro SSR repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Astro SSR? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Which port does the Astro server listen on? The one ox gives it in `PORT`, on the address in `HOST`. The Node adapter's standalone server reads both, so the app needs no code for it. ## Related guides - [**SvelteKit** Deploy a SvelteKit app with adapter-node to your own VPS: ox reads svelte.config.js, builds it, runs node build under systemd, and Caddy adds HTTPS.](https://console.t58.sorv.dev/docs/guides/sveltekit) - [**SvelteKit static** Deploy a SvelteKit site built with adapter-static to your own VPS: ox reads the pages folder from svelte.config and Caddy serves it over HTTPS, no Docker.](https://console.t58.sorv.dev/docs/guides/sveltekit-static) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/astro-ssr # Deploy SvelteKit with adapter-node to your own VPS https://console.t58.sorv.dev/docs/guides/sveltekit Deploy a SvelteKit app with adapter-node to your own VPS: ox reads svelte.config.js, builds it, runs node build under systemd, and Caddy adds HTTPS. To deploy SvelteKit to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `svelte.config.js` and `package.json`, runs `npm install --include=dev` and `npm run build`, then starts `node build` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A SvelteKit app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `svelte.config.js` and `package.json`, builds the plan on the card, and starts `node build` on your server. ## What ox detects in a SvelteKit repo This is the plan `ox check` prints for a minimal a SvelteKit app with adapter-node, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `node build` | `detected:svelte.config.js` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for SvelteKit None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy SvelteKit Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for SvelteKit None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the SvelteKit deploy fails - `svelte.config.js uses @sveltejs/adapter-auto, which builds for another host; install @sveltejs/adapter-node (a server) or @sveltejs/adapter-static (a static site) and use it in svelte.config.js`: adapter-auto is the SvelteKit default and builds for other hosts. Switch to adapter-node or adapter-static. [More](https://console.t58.sorv.dev/docs/config#app). - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## SvelteKit FAQ ### Do I need a Dockerfile or an ox.toml to deploy SvelteKit to a VPS? No. ox does not use Docker, and `ox check` on a SvelteKit repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for SvelteKit? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Why does ox not deploy my app with adapter-auto? adapter-auto builds for other hosts, so there is nothing for ox to run. `ox check` says to install `@sveltejs/adapter-node` for a server or `@sveltejs/adapter-static` for a site. ## Related guides - [**SvelteKit static** Deploy a SvelteKit site built with adapter-static to your own VPS: ox reads the pages folder from svelte.config and Caddy serves it over HTTPS, no Docker.](https://console.t58.sorv.dev/docs/guides/sveltekit-static) - [**Nuxt** Deploy a Nuxt app with server-side rendering to your own VPS: ox runs nuxt build, starts .output/server/index.mjs under systemd, and Caddy adds HTTPS.](https://console.t58.sorv.dev/docs/guides/nuxt) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/sveltekit # Deploy a static SvelteKit site to your own VPS https://console.t58.sorv.dev/docs/guides/sveltekit-static Deploy a SvelteKit site built with adapter-static to your own VPS: ox reads the pages folder from svelte.config and Caddy serves it over HTTPS, no Docker. To deploy a static SvelteKit site to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `svelte.config.ts` and `package.json`, runs `npm install --include=dev` and `npm run build`, and Caddy serves `site` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A SvelteKit static app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `svelte.config.ts` and `package.json`, builds the plan on the card, and serves `site` as files on your server. ## What ox detects in a SvelteKit static repo This is the plan `ox check` prints for a minimal a SvelteKit site with adapter-static, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `site` | `detected:svelte.config.ts` | | `static.spa` | `true` | `detected:svelte.config.ts` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for SvelteKit static None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["www.example.com"] [static] dir = "site" ``` ## Check and deploy SvelteKit static Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for SvelteKit static None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the SvelteKit static deploy fails - `svelte.config.js uses @sveltejs/adapter-auto, which builds for another host; install @sveltejs/adapter-node (a server) or @sveltejs/adapter-static (a static site) and use it in svelte.config.js`: adapter-auto is the SvelteKit default and builds for other hosts. Switch to adapter-node or adapter-static. [More](https://console.t58.sorv.dev/docs/config#app). - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The static site guide](https://console.t58.sorv.dev/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## SvelteKit static FAQ ### Do I need a Dockerfile or an ox.toml to deploy a static SvelteKit site to a VPS? No. ox does not use Docker, and `ox check` on a SvelteKit static repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for SvelteKit static? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### How do I make it a single-page app? Set `fallback: '200.html'` (or `'index.html'`) in the adapter's options, as this example does. ox reads it and sets `[static] spa = true` for you: Caddy serves each prerendered page at its own path and sends every other path to the fallback page. ## Related guides - [**Nuxt** Deploy a Nuxt app with server-side rendering to your own VPS: ox runs nuxt build, starts .output/server/index.mjs under systemd, and Caddy adds HTTPS.](https://console.t58.sorv.dev/docs/guides/nuxt) - [**React Router** Deploy a React Router 7 app (the Remix successor) with server rendering to your own VPS: ox builds it, runs npm start under systemd, and Caddy adds HTTPS.](https://console.t58.sorv.dev/docs/guides/react-router) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/sveltekit-static # Deploy Nuxt to a VPS: SSR on Node with no Docker https://console.t58.sorv.dev/docs/guides/nuxt Deploy a Nuxt app with server-side rendering to your own VPS: ox runs nuxt build, starts .output/server/index.mjs under systemd, and Caddy adds HTTPS. To deploy Nuxt to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `nuxt.config.ts` and `package.json`, runs `npm install --include=dev` and `npm run build`, then starts `node .output/server/index.mjs` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Nuxt app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `nuxt.config.ts` and `package.json`, builds the plan on the card, and starts `node .output/server/index.mjs` on your server. ## What ox detects in a Nuxt repo This is the plan `ox check` prints for a minimal a Nuxt app with server-side rendering, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `node .output/server/index.mjs` | `detected:nuxt.config.ts` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Nuxt None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy Nuxt Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Nuxt None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Nuxt deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Nuxt FAQ ### Do I need a Dockerfile or an ox.toml to deploy Nuxt to a VPS? No. ox does not use Docker, and `ox check` on a Nuxt repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Nuxt? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Can I deploy a static Nuxt site instead? Yes. When `package.json` has a `generate` script and no `build` script, or the config sets `ssr: false`, ox runs `generate` and serves `.output/public` as files, with no server. ## Related guides - [**React Router** Deploy a React Router 7 app (the Remix successor) with server rendering to your own VPS: ox builds it, runs npm start under systemd, and Caddy adds HTTPS.](https://console.t58.sorv.dev/docs/guides/react-router) - [**React Router SPA** Deploy a React Router 7 app with ssr: false to your own VPS: ox reads react-router.config.ts, builds it, and Caddy serves build/client over HTTPS, no Node.](https://console.t58.sorv.dev/docs/guides/react-router-spa) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/nuxt # Deploy React Router 7 or Remix to your own VPS https://console.t58.sorv.dev/docs/guides/react-router Deploy a React Router 7 app (the Remix successor) with server rendering to your own VPS: ox builds it, runs npm start under systemd, and Caddy adds HTTPS. To deploy React Router 7 to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `package.json`, runs `npm install --include=dev` and `npm run build`, then starts `npm run start` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A React Router app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `package.json`, builds the plan on the card, and starts `npm run start` on your server. ## What ox detects in a React Router repo This is the plan `ox check` prints for a minimal a React Router 7 app with server rendering, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `npm run start` | `detected:package.json` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for React Router None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy React Router Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for React Router None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the React Router deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## React Router FAQ ### Do I need a Dockerfile or an ox.toml to deploy React Router 7 to a VPS? No. ox does not use Docker, and `ox check` on a React Router repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for React Router? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### What if I turned server rendering off? With `ssr: false` in `react-router.config.ts`, ox serves `build/client` as a static site instead; the [React Router SPA guide](https://console.t58.sorv.dev/docs/guides/react-router-spa) shows that plan. ## Related guides - [**React Router SPA** Deploy a React Router 7 app with ssr: false to your own VPS: ox reads react-router.config.ts, builds it, and Caddy serves build/client over HTTPS, no Node.](https://console.t58.sorv.dev/docs/guides/react-router-spa) - [**SolidStart** Deploy a SolidStart app to your own VPS: ox reads the Node version from package.json, runs the build, starts it under systemd and adds HTTPS with Caddy.](https://console.t58.sorv.dev/docs/guides/solidstart) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/react-router # Deploy a React Router SPA (ssr: false) to a VPS https://console.t58.sorv.dev/docs/guides/react-router-spa Deploy a React Router 7 app with ssr: false to your own VPS: ox reads react-router.config.ts, builds it, and Caddy serves build/client over HTTPS, no Node. To deploy a React Router SPA to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `react-router.config.ts` and `package.json`, runs `npm install --include=dev` and `npm run build`, and Caddy serves `build/client` over HTTPS with no app process. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A React Router SPA app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `react-router.config.ts` and `package.json`, builds the plan on the card, and serves `build/client` as files on your server. ## What ox detects in a React Router SPA repo This is the plan `ox check` prints for a minimal a React Router 7 app with server rendering off, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `static.dir` | `build/client` | `detected:react-router.config.ts` | | `static.spa` | `true` | `detected:react-router.config.ts` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for React Router SPA None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[static]` needs its `dir`, and the build stays detected. ox.toml, optional: ```toml domains = ["www.example.com"] [static] dir = "build/client" ``` ## Check and deploy React Router SPA Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` A static site has no app process, so its log is the deploy's own: `ox runs` lists them and `--run` reads one. ## Variables to set for React Router SPA None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the React Router SPA deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the build left no dist directory`: The build wrote its files somewhere else. Set `[static] dir` to the folder your build makes. [More](https://console.t58.sorv.dev/docs/config#static). - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [React Router](https://console.t58.sorv.dev/docs/guides/react-router): Deploy a React Router 7 app (the Remix successor) with server rendering to your own VPS. - [The `[static]` keys](https://console.t58.sorv.dev/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Roll back](https://console.t58.sorv.dev/docs/operate/rollback) to an earlier release without a rebuild. ## React Router SPA FAQ ### Do I need a Dockerfile or an ox.toml to deploy a React Router SPA to a VPS? No. ox does not use Docker, and `ox check` on a React Router SPA repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for React Router SPA? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Does any Node.js process run? No. With server rendering off the build is plain files, so ox serves `build/client` through Caddy and runs no app process. ## Related guides - [**SolidStart** Deploy a SolidStart app to your own VPS: ox reads the Node version from package.json, runs the build, starts it under systemd and adds HTTPS with Caddy.](https://console.t58.sorv.dev/docs/guides/solidstart) - [**Qwik City** Deploy a Qwik City app with its Express adapter to your own VPS: ox finds the adapter, builds it, runs npm run serve under systemd, and Caddy serves HTTPS.](https://console.t58.sorv.dev/docs/guides/qwik) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/react-router-spa # Deploy SolidStart to your own VPS with Node.js https://console.t58.sorv.dev/docs/guides/solidstart Deploy a SolidStart app to your own VPS: ox reads the Node version from package.json, runs the build, starts it under systemd and adds HTTPS with Caddy. To deploy SolidStart to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `package.json`, runs `npm install --include=dev` and `npm run build`, then starts `npm run start` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A SolidStart app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `package.json`, builds the plan on the card, and starts `npm run start` on your server. ## What ox detects in a SolidStart repo This is the plan `ox check` prints for a minimal a SolidStart app, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `npm run start` | `detected:package.json` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for SolidStart None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy SolidStart Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for SolidStart None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the SolidStart deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## SolidStart FAQ ### Do I need a Dockerfile or an ox.toml to deploy SolidStart to a VPS? No. ox does not use Docker, and `ox check` on a SolidStart repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for SolidStart? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Why does ox use Node.js 22 here? Because this repo's `package.json` asks for it in `engines`, and the version your repo pins wins over ox's default of Node.js 24. ## Related guides - [**Qwik City** Deploy a Qwik City app with its Express adapter to your own VPS: ox finds the adapter, builds it, runs npm run serve under systemd, and Caddy serves HTTPS.](https://console.t58.sorv.dev/docs/guides/qwik) - [**NestJS** Deploy a NestJS API to your own VPS: ox installs from package-lock.json, runs nest build and start:prod under systemd, and sets up PostgreSQL for the app.](https://console.t58.sorv.dev/docs/guides/nestjs) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/solidstart # Deploy Qwik City with the Express adapter to a VPS https://console.t58.sorv.dev/docs/guides/qwik Deploy a Qwik City app with its Express adapter to your own VPS: ox finds the adapter, builds it, runs npm run serve under systemd, and Caddy serves HTTPS. To deploy Qwik City to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `adapters/express/vite.config.ts` and `package.json`, runs `npm install --include=dev` and `npm run build`, then starts `npm run serve` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Qwik City app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `adapters/express/vite.config.ts` and `package.json`, builds the plan on the card, and starts `npm run serve` on your server. ## What ox detects in a Qwik City repo This is the plan `ox check` prints for a minimal a Qwik City app with the Express adapter, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `npm run serve` | `detected:adapters/express/vite.config.ts` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | ## The ox.toml for Qwik City None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy Qwik City Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Qwik City None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Qwik City deploy fails - `Qwik City found, but no single Node adapter with its serve script (Qwik's start script runs the dev server); run npm run qwik add express, whose serve script ox then runs, or set [app] start`: Add the Express adapter with `npm run qwik add express`, or set `[app] start`. [More](https://console.t58.sorv.dev/docs/config#app). - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Qwik City FAQ ### Do I need a Dockerfile or an ox.toml to deploy Qwik City to a VPS? No. ox does not use Docker, and `ox check` on a Qwik City repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Qwik City? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Why not npm start? Qwik's own `start` script runs the Vite development server. ox runs the Express adapter's `serve` script instead, which serves the built app. ## Related guides - [**NestJS** Deploy a NestJS API to your own VPS: ox installs from package-lock.json, runs nest build and start:prod under systemd, and sets up PostgreSQL for the app.](https://console.t58.sorv.dev/docs/guides/nestjs) - [**Fastify** Deploy a Fastify API to your own VPS: ox finds server.js, runs node server.js under systemd behind Caddy, and adds PostgreSQL with DATABASE_URL set.](https://console.t58.sorv.dev/docs/guides/fastify) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/qwik # Deploy NestJS to a VPS with PostgreSQL, no Docker https://console.t58.sorv.dev/docs/guides/nestjs Deploy a NestJS API to your own VPS: ox installs from package-lock.json, runs nest build and start:prod under systemd, and sets up PostgreSQL for the app. To deploy NestJS to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `package.json` and `package-lock.json`, runs `npm ci --include=dev` and `npm run build`, then starts `npm run start:prod` under systemd behind Caddy with HTTPS, and sets up postgres 18 for it. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A NestJS app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `package.json` and `package-lock.json`, builds the plan on the card, and starts `npm run start:prod` on your server. The cylinders are the services the repo asked for. ## What ox detects in a NestJS repo This is the plan `ox check` prints for a minimal a NestJS API, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `npm run start:prod` | `detected:package.json` | | `build.install` | `npm ci --include=dev` | `detected:package-lock.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | | `services.postgres` | `postgres 18 (shared)` | `detected:package.json` | ## The ox.toml for NestJS None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. Once a repo has an ox.toml, ox adds no service itself, so list each one under `[services]`. ox.toml, optional: ```toml domains = ["www.example.com"] [app] [services] postgres = {} ``` ## Check and deploy NestJS Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for NestJS None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME`, `PUBLIC_PORT` and `DATABASE_URL`. ## If the NestJS deploy fails - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://console.t58.sorv.dev/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## NestJS FAQ ### Do I need a Dockerfile or an ox.toml to deploy NestJS to a VPS? No. ox does not use Docker, and `ox check` on a NestJS repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for NestJS? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Which start script does ox run for Nest? `start:prod` when `package.json` has one, as the Nest template does. Without it ox runs the built entry, such as `node dist/main`. ## Related guides - [**Fastify** Deploy a Fastify API to your own VPS: ox finds server.js, runs node server.js under systemd behind Caddy, and adds PostgreSQL with DATABASE_URL set.](https://console.t58.sorv.dev/docs/guides/fastify) - [**Hono on Bun** Deploy a Hono API on Bun to your own VPS: ox installs Bun, runs bun install from bun.lock, and starts src/index.ts under systemd behind Caddy.](https://console.t58.sorv.dev/docs/guides/hono-bun) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/nestjs # Deploy a Fastify API with PostgreSQL to a VPS https://console.t58.sorv.dev/docs/guides/fastify Deploy a Fastify API to your own VPS: ox finds server.js, runs node server.js under systemd behind Caddy, and adds PostgreSQL with DATABASE_URL set. To deploy Fastify to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `server.js` and `package.json`, runs `npm install --include=dev`, then starts `node server.js` under systemd behind Caddy with HTTPS, and sets up postgres 18 for it. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Fastify app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `server.js` and `package.json`, builds the plan on the card, and starts `node server.js` on your server. The cylinders are the services the repo asked for. ## What ox detects in a Fastify repo This is the plan `ox check` prints for a minimal a Fastify API with PostgreSQL, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `node server.js` | `detected:server.js` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `tools.node` | `24` | `default` | | `services.postgres` | `postgres 18 (shared)` | `detected:package.json` | ## The ox.toml for Fastify None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. Once a repo has an ox.toml, ox adds no service itself, so list each one under `[services]`. ox.toml, optional: ```toml domains = ["www.example.com"] [app] [services] postgres = {} ``` ## Check and deploy Fastify Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Fastify None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME`, `PUBLIC_PORT` and `DATABASE_URL`. ## If the Fastify deploy fails - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `package.json has no start script, and no web framework ox knows (express, fastify) is a dependency; add a start script or set [app] start`: Give `package.json` a `start` script that runs your server. [More](https://console.t58.sorv.dev/docs/guides/node). - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://console.t58.sorv.dev/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Fastify FAQ ### Do I need a Dockerfile or an ox.toml to deploy Fastify to a VPS? No. ox does not use Docker, and `ox check` on a Fastify repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Fastify? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Does Fastify need a start script? Not for ox. With Fastify as a dependency and one `server.js` at the root, ox runs `node server.js`. A `start` script, when you add one, wins. ## Related guides - [**Hono on Bun** Deploy a Hono API on Bun to your own VPS: ox installs Bun, runs bun install from bun.lock, and starts src/index.ts under systemd behind Caddy.](https://console.t58.sorv.dev/docs/guides/hono-bun) - [**BullMQ worker** Deploy a Node app with a BullMQ queue to your own VPS: ox adds Redis with REDIS_URL set, runs your server, and the [workers] line runs worker.js beside it.](https://console.t58.sorv.dev/docs/guides/bullmq-worker) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/fastify # Deploy a Hono API on Bun to your own VPS, no Docker https://console.t58.sorv.dev/docs/guides/hono-bun Deploy a Hono API on Bun to your own VPS: ox installs Bun, runs bun install from bun.lock, and starts src/index.ts under systemd behind Caddy. To deploy Hono on Bun to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `src/index.ts` and `bun.lock`, runs `bun install --frozen-lockfile`, then starts `bun run src/index.ts` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Hono on Bun app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `src/index.ts` and `bun.lock`, builds the plan on the card, and starts `bun run src/index.ts` on your server. ## What ox detects in a Hono on Bun repo This is the plan `ox check` prints for a minimal a Hono API running on Bun, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `bun run src/index.ts` | `detected:src/index.ts` | | `build.install` | `bun install --frozen-lockfile` | `detected:bun.lock` | | `tools.bun` | `1.3` | `default` | ## The ox.toml for Hono on Bun None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["api.example.com"] [app] ``` ## Check and deploy Hono on Bun Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Hono on Bun None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Hono on Bun deploy fails - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). - `the app exited while starting (systemd restarted it 3 times)`: The start command ran and stopped, three times in a row. The log shows the app's last lines, most often a missing variable or a wrong path to the built file; run the start command on your computer to see it. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Hono on Bun FAQ ### Do I need a Dockerfile or an ox.toml to deploy Hono on Bun to a VPS? No. ox does not use Docker, and `ox check` on a Hono on Bun repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Hono on Bun? Only what the plan names: `bun 1.3` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Does Bun need a build step? No. Bun runs TypeScript directly, so ox installs from `bun.lock` and starts `bun run src/index.ts`, with no build. ## Related guides - [**BullMQ worker** Deploy a Node app with a BullMQ queue to your own VPS: ox adds Redis with REDIS_URL set, runs your server, and the [workers] line runs worker.js beside it.](https://console.t58.sorv.dev/docs/guides/bullmq-worker) - [**Deno Fresh** Deploy a Deno Fresh app to your own VPS: ox installs Deno 2, runs deno install and deno task build, then deno serve under systemd with Caddy for HTTPS.](https://console.t58.sorv.dev/docs/guides/deno-fresh) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/hono-bun # Deploy a BullMQ worker and Redis queue to a VPS https://console.t58.sorv.dev/docs/guides/bullmq-worker Deploy a Node app with a BullMQ queue to your own VPS: ox adds Redis with REDIS_URL set, runs your server, and the [workers] line runs worker.js beside it. To deploy a BullMQ worker to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `package.json`, runs `npm install --include=dev`, then starts `npm run start` under systemd behind Caddy with HTTPS, and sets up redis 8 for it. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A BullMQ worker app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `package.json`, builds the plan on the card, and starts `npm run start` on your server. The cylinders are the services the repo asked for. ## What ox detects in a BullMQ worker repo This is the plan `ox check` prints for a minimal a Node app with a BullMQ queue, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `npm run start` | `detected:package.json` | | `build.install` | `npm install --include=dev` | `detected:package.json` | | `tools.node` | `24` | `default` | | `services.redis` | `redis 8 (only for this project)` | `detected:package.json` | `ox check` also prints these hints for it: - `worker.js starts a BullMQ Worker, which processes jobs only while it runs: add [workers] worker = "node worker.js"` ## The ox.toml for BullMQ worker None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected, and to run the worker, as the hint says: ox runs only the processes the plan names. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. Once a repo has an ox.toml, ox adds no service itself, so list each one under `[services]`. ox.toml, optional: ```toml domains = ["www.example.com"] [app] [workers] worker = "node worker.js" [services] redis = {} ``` ## Check and deploy BullMQ worker Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for BullMQ worker None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME`, `PUBLIC_PORT` and `REDIS_URL`. ## If the BullMQ worker deploy fails - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `the app exited while starting (systemd restarted it 3 times)`: The start command ran and stopped, three times in a row. The log shows the app's last lines, most often a missing variable or a wrong path to the built file; run the start command on your computer to see it. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The `[workers]` keys](https://console.t58.sorv.dev/docs/config#workers): processes that run next to the app. - [The Node.js guide](https://console.t58.sorv.dev/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://console.t58.sorv.dev/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. ## BullMQ worker FAQ ### Do I need a Dockerfile or an ox.toml to deploy a BullMQ worker to a VPS? No. ox does not use Docker, and `ox check` on a BullMQ worker repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for BullMQ worker? Only what the plan names: `node 24` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Why add a [workers] entry? A BullMQ Worker processes jobs only while it runs, and ox runs only what the plan names. `[workers] worker = "node worker.js"` makes it a systemd service next to the app. ## Related guides - [**Deno Fresh** Deploy a Deno Fresh app to your own VPS: ox installs Deno 2, runs deno install and deno task build, then deno serve under systemd with Caddy for HTTPS.](https://console.t58.sorv.dev/docs/guides/deno-fresh) - [**Flask** Deploy a Flask app to your own VPS: ox installs with uv from requirements.txt, runs gunicorn app:app under systemd behind Caddy, and adds PostgreSQL.](https://console.t58.sorv.dev/docs/guides/flask) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/bullmq-worker # Deploy Deno Fresh to your own VPS with deno serve https://console.t58.sorv.dev/docs/guides/deno-fresh Deploy a Deno Fresh app to your own VPS: ox installs Deno 2, runs deno install and deno task build, then deno serve under systemd with Caddy for HTTPS. To deploy Deno Fresh to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `deno.json` and `deno.lock`, runs `deno install --frozen` and `deno task build`, then starts `deno serve --host 127.0.0.1 --port $PORT -A _fresh/server.js` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Deno Fresh app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `deno.json` and `deno.lock`, builds the plan on the card, and starts `deno serve --host 127.0.0.1 --port $PORT -A _fresh/server.js` on your server. ## What ox detects in a Deno Fresh repo This is the plan `ox check` prints for a minimal a Deno Fresh app, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `deno serve --host 127.0.0.1 --port $PORT -A _fresh/server.js` | `detected:deno.json` | | `build.install` | `deno install --frozen` | `detected:deno.lock` | | `build.commands[0]` | `deno task build` | `detected:deno.json` | | `tools.deno` | `2` | `default` | ## The ox.toml for Deno Fresh None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy Deno Fresh Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Deno Fresh None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Deno Fresh deploy fails - `ox starts a Deno app only from a start task that runs deno serve; set [app] start to a command that listens on 127.0.0.1:$PORT`: Make the start task in `deno.json` run `deno serve`, or set `[app] start`. [More](https://console.t58.sorv.dev/docs/config#app). - `build 1/1: npm run build: exit status 1`: Your build failed. The lines above it are the build's own output; run the same command on your computer, fix it, and push. [More](https://console.t58.sorv.dev/docs/troubleshooting#build-failed). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The `[tools]` keys](https://console.t58.sorv.dev/docs/config#tools): pin a runtime version. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Deno Fresh FAQ ### Do I need a Dockerfile or an ox.toml to deploy Deno Fresh to a VPS? No. ox does not use Docker, and `ox check` on a Deno Fresh repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Deno Fresh? Only what the plan names: `deno 2` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Which start does ox run for Deno? A start task that runs `deno serve`. ox puts its own host and port flags in, so the app listens on `127.0.0.1` at `$PORT`. ## Related guides - [**Flask** Deploy a Flask app to your own VPS: ox installs with uv from requirements.txt, runs gunicorn app:app under systemd behind Caddy, and adds PostgreSQL.](https://console.t58.sorv.dev/docs/guides/flask) - [**Symfony** Deploy Symfony to your own VPS: ox runs composer install for prod, Doctrine migrations and FrankenPHP under systemd, and asks for APP_SECRET first.](https://console.t58.sorv.dev/docs/guides/symfony) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/deno-fresh # Deploy Flask to a VPS with gunicorn and PostgreSQL https://console.t58.sorv.dev/docs/guides/flask Deploy a Flask app to your own VPS: ox installs with uv from requirements.txt, runs gunicorn app:app under systemd behind Caddy, and adds PostgreSQL. To deploy Flask to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `app.py` and `requirements.txt`, runs `uv venv --allow-existing && uv pip install -r requirements.txt`, then starts `.venv/bin/gunicorn app:app --bind 127.0.0.1:$PORT` under systemd behind Caddy with HTTPS, and sets up postgres 18 for it. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Flask app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `app.py` and `requirements.txt`, builds the plan on the card, and starts `.venv/bin/gunicorn app:app --bind 127.0.0.1:$PORT` on your server. The cylinders are the services the repo asked for. ## What ox detects in a Flask repo This is the plan `ox check` prints for a minimal a Flask app with PostgreSQL, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `.venv/bin/gunicorn app:app --bind 127.0.0.1:$PORT` | `detected:app.py` | | `build.install` | `uv venv --allow-existing && uv pip install -r requirements.txt` | `detected:requirements.txt` | | `tools.python` | `3.13` | `default` | | `tools.uv` | `0.11` | `default` | | `packages` | `gcc` | `detected:requirements.txt` | | `packages` | `g++` | `detected:requirements.txt` | | `packages` | `make` | `detected:requirements.txt` | | `packages` | `libc6-dev` | `detected:requirements.txt` | | `packages` | `libcrypt-dev` | `detected:requirements.txt` | | `services.postgres` | `postgres 18 (shared)` | `detected:requirements.txt` | ## The ox.toml for Flask None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. Once a repo has an ox.toml, ox adds no service itself, so list each one under `[services]`. ox.toml, optional: ```toml domains = ["www.example.com"] [app] [services] postgres = {} ``` ## Check and deploy Flask Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Flask None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME`, `PUBLIC_PORT` and `DATABASE_URL`. ## If the Flask deploy fails - `Flask found in app.py, but gunicorn is not a dependency; add gunicorn or set [app] start`: Add `gunicorn` to `requirements.txt` so ox can run the app. [More](https://console.t58.sorv.dev/docs/config#app). - `` Flask found in app.py, but no top-level ` = Flask(...)`, so ox cannot tell which object to serve; set [app] start ``: Create the app at the top level of the file, or set `[app] start` to your gunicorn command. [More](https://console.t58.sorv.dev/docs/config#app). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Django guide](https://console.t58.sorv.dev/docs/guides/django): gunicorn, migrations and static files. - [The FastAPI guide](https://console.t58.sorv.dev/docs/guides/fastapi): uvicorn, uv and Alembic. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://console.t58.sorv.dev/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. ## Flask FAQ ### Do I need a Dockerfile or an ox.toml to deploy Flask to a VPS? No. ox does not use Docker, and `ox check` on a Flask repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Flask? Only what the plan names: `python 3.13` (ox's default), `uv 0.11` (ox's default) and the apt packages `gcc`, `g++`, `make`, `libc6-dev` and `libcrypt-dev`. Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Does ox work with a Flask app factory? Yes. With `create_app()` in `app/__init__.py` and no top-level app object, ox runs `gunicorn 'app:create_app()'`. ## Related guides - [**Symfony** Deploy Symfony to your own VPS: ox runs composer install for prod, Doctrine migrations and FrankenPHP under systemd, and asks for APP_SECRET first.](https://console.t58.sorv.dev/docs/guides/symfony) - [**Plain PHP** Deploy a plain PHP site to your own VPS: ox finds index.php, installs FrankenPHP, and serves the site under systemd behind Caddy with HTTPS, no ox.toml.](https://console.t58.sorv.dev/docs/guides/php) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/flask # Deploy Symfony to a VPS with FrankenPHP and Doctrine https://console.t58.sorv.dev/docs/guides/symfony Deploy Symfony to your own VPS: ox runs composer install for prod, Doctrine migrations and FrankenPHP under systemd, and asks for APP_SECRET first. To deploy Symfony to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `bin/console`, `composer.json` and `.env`, runs `composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader` and `php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration` for the migrations, then starts `frankenphp php-server --root public --listen 127.0.0.1:$PORT` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Symfony app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `bin/console`, `composer.json` and `.env`, builds the plan on the card, and starts `frankenphp php-server --root public --listen 127.0.0.1:$PORT` on your server. ## What ox detects in a Symfony repo This is the plan `ox check` prints for a minimal a Symfony app, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `APP_ENV=prod APP_DEBUG=0 XDG_CONFIG_HOME=/tmp XDG_DATA_HOME=/tmp exec frankenphp php-server --root public --listen 127.0.0.1:$PORT` | `detected:bin/console` | | `build.install` | `APP_ENV=prod composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader` | `detected:composer.json` | | `build.migrate` | `APP_ENV=prod php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration` | `detected:composer.json` | | `tools.github:php/frankenphp` | `1.12.7` | `default` | | `packages` | `composer` | `detected:composer.json` | | `packages` | `php-cli` | `detected:composer.json` | | `packages` | `php-curl` | `detected:composer.json` | | `packages` | `php-xml` | `detected:composer.json` | | `packages` | `unzip` | `detected:composer.json` | | `scratch` | `var/cache/prod/pools` | `detected:bin/console` | | `scratch` | `var/log` | `detected:bin/console` | | `scratch` | `var/sessions` | `detected:bin/console` | | `scratch` | `var/share` | `detected:.env` | ## The ox.toml for Symfony None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy Symfony Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Symfony Set `APP_SECRET (App secret: 32 hex characters` and `Generate makes it)` before the first deploy: `ox check` asks for it, and the deploy stops until it has a value. Use the dashboard's Variables tab or `ox vars set KEY`, which asks for the value. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Symfony deploy fails - `set these before deploying: APP_SECRET (App secret: 32 hex characters, Generate makes it)`: A variable the app needs has no value. Set it on the dashboard's Variables tab, or with `ox vars set KEY`. [More](https://console.t58.sorv.dev/docs/operate/variables#set). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `no index.php at the root or in public/, web/ or docroot/; set [app] start = "frankenphp php-server --root --listen 127.0.0.1:$PORT"`: Put `index.php` at the root or in `public/`, `web/` or `docroot/`, or set `[app] start` with your document root. [More](https://console.t58.sorv.dev/docs/config#app). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Laravel guide](https://console.t58.sorv.dev/docs/guides/laravel): Composer, a queue worker and the scheduler. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Symfony FAQ ### Do I need a Dockerfile or an ox.toml to deploy Symfony to a VPS? No. ox does not use Docker, and `ox check` on a Symfony repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Symfony? Only what the plan names: `github:php/frankenphp 1.12.7` (ox's default) and the apt packages `composer`, `php-cli`, `php-curl`, `php-xml` and `unzip`. Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Where do Symfony logs go? To `php://stderr`, Monolog's prod default, which ox keeps in the app's log. A file log in `var/log` works too, but it is a scratch folder of its release, so it ends with the release. ## Related guides - [**Plain PHP** Deploy a plain PHP site to your own VPS: ox finds index.php, installs FrankenPHP, and serves the site under systemd behind Caddy with HTTPS, no ox.toml.](https://console.t58.sorv.dev/docs/guides/php) - [**Phoenix** Deploy an Elixir Phoenix app to your own VPS: ox builds a mix release with its assets, runs Release.migrate, starts it under systemd, and adds PostgreSQL.](https://console.t58.sorv.dev/docs/guides/phoenix) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/symfony # Deploy a plain PHP site to a VPS with FrankenPHP https://console.t58.sorv.dev/docs/guides/php Deploy a plain PHP site to your own VPS: ox finds index.php, installs FrankenPHP, and serves the site under systemd behind Caddy with HTTPS, no ox.toml. To deploy a PHP site to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `index.php` and `.htaccess`, then starts `frankenphp php-server --root . --listen 127.0.0.1:$PORT` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Plain PHP app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `index.php` and `.htaccess`, builds the plan on the card, and starts `frankenphp php-server --root . --listen 127.0.0.1:$PORT` on your server. ## What ox detects in a Plain PHP repo This is the plan `ox check` prints for a minimal a plain PHP site, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `XDG_CONFIG_HOME=/tmp XDG_DATA_HOME=/tmp exec frankenphp php-server --root . --listen 127.0.0.1:$PORT` | `detected:index.php` | | `tools.github:php/frankenphp` | `1.12.7` | `default` | | `closed` | `data/` | `detected:.htaccess` | ## The ox.toml for Plain PHP None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy Plain PHP Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Plain PHP None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Plain PHP deploy fails - `no index.php at the root or in public/, web/ or docroot/; set [app] start = "frankenphp php-server --root --listen 127.0.0.1:$PORT"`: Put `index.php` at the root or in `public/`, `web/` or `docroot/`, or set `[app] start` with your document root. [More](https://console.t58.sorv.dev/docs/config#app). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The Laravel guide](https://console.t58.sorv.dev/docs/guides/laravel): Composer, a queue worker and the scheduler. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Plain PHP FAQ ### Do I need a Dockerfile or an ox.toml to deploy a PHP site to a VPS? No. ox does not use Docker, and `ox check` on a Plain PHP repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Plain PHP? Only what the plan names: `github:php/frankenphp 1.12.7` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Do I need Composer? Not for a site with no `composer.json`: the plan above installs only FrankenPHP and serves the folder that holds `index.php`. ## Related guides - [**Phoenix** Deploy an Elixir Phoenix app to your own VPS: ox builds a mix release with its assets, runs Release.migrate, starts it under systemd, and adds PostgreSQL.](https://console.t58.sorv.dev/docs/guides/phoenix) - [**Spring Boot (Maven)** Deploy a Spring Boot app built with Maven to your own VPS: ox builds the jar with mvnw, installs Java 21, runs it under systemd and adds PostgreSQL for it.](https://console.t58.sorv.dev/docs/guides/spring-boot) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/php # Deploy Phoenix to a VPS: mix release and Postgres https://console.t58.sorv.dev/docs/guides/phoenix Deploy an Elixir Phoenix app to your own VPS: ox builds a mix release with its assets, runs Release.migrate, starts it under systemd, and adds PostgreSQL. To deploy Phoenix to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `mix.exs`, `lib/hello/release.ex` and `.tool-versions`, runs `mix local.hex --force && mix local.rebar --force && MIX_ENV=prod mix deps.get --only prod`, `mix compile`, `mix assets.deploy`, `mix release` and `_build/prod/rel/hello/bin/hello eval 'Hello.Release.migrate()'` for the migrations, then starts `_build/prod/rel/hello/bin/hello start` under systemd behind Caddy with HTTPS, and sets up postgres 18 for it. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Phoenix app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `mix.exs`, `lib/hello/release.ex` and `.tool-versions`, builds the plan on the card, and starts `_build/prod/rel/hello/bin/hello start` on your server. The cylinders are the services the repo asked for. ## What ox detects in a Phoenix repo This is the plan `ox check` prints for a minimal an Elixir Phoenix app, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `PHX_SERVER=true RELEASE_TMP=/tmp RELEASE_DISTRIBUTION=none PHX_HOST=$PUBLIC_HOST _build/prod/rel/hello/bin/hello start` | `detected:mix.exs` | | `build.install` | `mix local.hex --force && mix local.rebar --force && MIX_ENV=prod mix deps.get --only prod` | `detected:mix.exs` | | `build.commands[0]` | `MIX_ENV=prod mix compile` | `detected:mix.exs` | | `build.commands[1]` | `MIX_ENV=prod mix assets.deploy` | `detected:mix.exs` | | `build.commands[2]` | `MIX_ENV=prod mix release` | `detected:mix.exs` | | `build.migrate` | `RELEASE_TMP=/tmp _build/prod/rel/hello/bin/hello eval 'Hello.Release.migrate()'` | `detected:lib/hello/release.ex` | | `tools.elixir` | `1.18.4-otp-27` | `detected:.tool-versions` | | `packages` | `erlang-dev` | `tools.elixir` | | `packages` | `erlang-nox` | `tools.elixir` | | `services.postgres` | `postgres 18 (shared)` | `detected:mix.exs` | `ox check` also prints these hints for it: - `not installed: erlang (from .tool-versions): it is never a mise tool in ox: mise compiles Erlang/OTP from source on the server, and that fails there. Every app with Elixir gets Ubuntu's Erlang/OTP 27 from ox (packages = ["erlang-nox"] for one without)` ## The ox.toml for Phoenix None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. Once a repo has an ox.toml, ox adds no service itself, so list each one under `[services]`. ox.toml, optional: ```toml domains = ["www.example.com"] [app] [services] postgres = {} ``` ## Check and deploy Phoenix Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Phoenix Set `SECRET_KEY_BASE (Rails or Phoenix key base: 128 hex characters` and `Generate makes it)` before the first deploy: `ox check` asks for it, and the deploy stops until it has a value. Use the dashboard's Variables tab or `ox vars set KEY`, which asks for the value. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME`, `PUBLIC_PORT` and `DATABASE_URL`. ## If the Phoenix deploy fails - `set these before deploying: SECRET_KEY_BASE (Rails or Phoenix key base: 128 hex characters, Generate makes it)`: A variable the app needs has no value. Set it on the dashboard's Variables tab, or with `ox vars set KEY`. [More](https://console.t58.sorv.dev/docs/operate/variables#set). - `config/runtime.exs does not read PORT: set the endpoint's http port to String.to_integer(System.get_env("PORT")), as mix phx.new does`: Read the port from `PORT` in `config/runtime.exs`, as a new Phoenix app does. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `no Release.migrate in lib/, so ox runs no migrations: run mix phx.gen.release to add one`: Run `mix phx.gen.release` once and commit the module it writes. [More](https://console.t58.sorv.dev/docs/config#build). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables#set) on the dashboard or with `ox vars`. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://console.t58.sorv.dev/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Phoenix FAQ ### Do I need a Dockerfile or an ox.toml to deploy Phoenix to a VPS? No. ox does not use Docker, and `ox check` on a Phoenix repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Phoenix? Only what the plan names: `elixir 1.18.4-otp-27` (read from `.tool-versions`) and the apt packages `erlang-dev` and `erlang-nox`. Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Which Erlang and Elixir does ox install? Erlang/OTP 27 from Ubuntu's archive, and the build for OTP 27 of the Elixir your `.tool-versions` names, as the plan above shows (1.18, or one `mix.exs` allows, without it). An Elixir older than 1.17 does not run on OTP 27, and ox installs no other Erlang. ## Related guides - [**Spring Boot (Maven)** Deploy a Spring Boot app built with Maven to your own VPS: ox builds the jar with mvnw, installs Java 21, runs it under systemd and adds PostgreSQL for it.](https://console.t58.sorv.dev/docs/guides/spring-boot) - [**Spring Boot (Gradle)** Deploy a Spring Boot app built with Gradle to your own VPS: ox runs gradlew bootJar, reads the Java version from build.gradle.kts, and runs the jar.](https://console.t58.sorv.dev/docs/guides/spring-boot-gradle) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/phoenix # Deploy Spring Boot to a VPS with Maven and Postgres https://console.t58.sorv.dev/docs/guides/spring-boot Deploy a Spring Boot app built with Maven to your own VPS: ox builds the jar with mvnw, installs Java 21, runs it under systemd and adds PostgreSQL for it. To deploy Spring Boot to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `pom.xml` and `mvnw`, runs `./mvnw -B -DskipTests package`, then starts `java -jar target/demo-0.0.1-SNAPSHOT.jar` under systemd behind Caddy with HTTPS, and sets up postgres 18 for it. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Spring Boot (Maven) app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `pom.xml` and `mvnw`, builds the plan on the card, and starts `java -jar target/demo-0.0.1-SNAPSHOT.jar` on your server. The cylinders are the services the repo asked for. ## What ox detects in a Spring Boot (Maven) repo This is the plan `ox check` prints for a minimal a Spring Boot app built with Maven, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `SERVER_ADDRESS=127.0.0.1 SERVER_PORT=$PORT java -jar target/demo-0.0.1-SNAPSHOT.jar` | `detected:pom.xml` | | `app.health` | `/actuator/health` | `detected:pom.xml` | | `build.commands[0]` | `./mvnw -B -DskipTests package` | `detected:mvnw` | | `tools.java` | `temurin-21` | `detected:pom.xml` | | `packages` | `fontconfig` | `tools.java` | | `services.postgres` | `postgres 18 (shared)` | `detected:pom.xml` | `ox check` also prints these hints for it: - `the JVM sizes its heap to a quarter of the server's memory: set [app] memory (or [limits] memory) in ox.toml, and ox passes -Xmx at three quarters of it` - `Spring Boot reads SPRING_DATASOURCE_URL, not DATABASE_URL: set SPRING_DATASOURCE_URL to jdbc:postgresql://${postgres.host}:${postgres.port}/${postgres.database}, SPRING_DATASOURCE_USERNAME to ${postgres.user} and SPRING_DATASOURCE_PASSWORD to ${postgres.password}` ## The ox.toml for Spring Boot (Maven) None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected, and to size the JVM's heap, as the hint says: ox passes -Xmx at three quarters of [app] memory. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. Once a repo has an ox.toml, ox adds no service itself, so list each one under `[services]`. ox.toml, optional: ```toml domains = ["www.example.com"] [app] memory = "1G" [services] postgres = {} ``` ## Check and deploy Spring Boot (Maven) Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Spring Boot (Maven) None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME`, `PUBLIC_PORT` and `DATABASE_URL`. ## If the Spring Boot (Maven) deploy fails - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `Java project found (pom.xml) without Spring Boot or Quarkus: ox does not know how to build and start it; add an ox.toml with [build] commands and [app] start`: ox plans Spring Boot and Quarkus. For another Java app, write `[build] commands` and `[app] start`. [More](https://console.t58.sorv.dev/docs/config#build). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables#set) on the dashboard or with `ox vars`. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://console.t58.sorv.dev/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Spring Boot (Maven) FAQ ### Do I need a Dockerfile or an ox.toml to deploy Spring Boot to a VPS? No. ox does not use Docker, and `ox check` on a Spring Boot (Maven) repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Spring Boot (Maven)? Only what the plan names: `java temurin-21` (read from `pom.xml`) and the apt packages `fontconfig`. Your app's own dependencies are installed by the build into the release folder, not system-wide. ### How do I give the JVM more memory? Set `[app] memory` in an ox.toml, as the hint above says. ox then passes `-Xmx` at three quarters of it; without it the JVM takes a quarter of the server's memory. ## Related guides - [**Spring Boot (Gradle)** Deploy a Spring Boot app built with Gradle to your own VPS: ox runs gradlew bootJar, reads the Java version from build.gradle.kts, and runs the jar.](https://console.t58.sorv.dev/docs/guides/spring-boot-gradle) - [**Quarkus** Deploy a Quarkus app to your own VPS in JVM mode: ox builds it with mvnw, runs quarkus-run.jar under systemd, checks /q/health and adds PostgreSQL for it.](https://console.t58.sorv.dev/docs/guides/quarkus) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/spring-boot # Deploy Spring Boot built with Gradle to your own VPS https://console.t58.sorv.dev/docs/guides/spring-boot-gradle Deploy a Spring Boot app built with Gradle to your own VPS: ox runs gradlew bootJar, reads the Java version from build.gradle.kts, and runs the jar. To deploy Spring Boot with Gradle to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `build.gradle.kts` and `gradlew`, runs `./gradlew --no-daemon bootJar`, then starts `java -jar build/libs/demo-0.0.1-SNAPSHOT.jar` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Spring Boot (Gradle) app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `build.gradle.kts` and `gradlew`, builds the plan on the card, and starts `java -jar build/libs/demo-0.0.1-SNAPSHOT.jar` on your server. ## What ox detects in a Spring Boot (Gradle) repo This is the plan `ox check` prints for a minimal a Spring Boot app built with Gradle, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `SERVER_ADDRESS=127.0.0.1 SERVER_PORT=$PORT java -jar build/libs/demo-0.0.1-SNAPSHOT.jar` | `detected:build.gradle.kts` | | `build.commands[0]` | `./gradlew --no-daemon bootJar` | `detected:gradlew` | | `tools.java` | `temurin-17` | `detected:build.gradle.kts` | | `packages` | `fontconfig` | `tools.java` | `ox check` also prints these hints for it: - `the JVM sizes its heap to a quarter of the server's memory: set [app] memory (or [limits] memory) in ox.toml, and ox passes -Xmx at three quarters of it` ## The ox.toml for Spring Boot (Gradle) None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected, and to size the JVM's heap, as the hint says: ox passes -Xmx at three quarters of [app] memory. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] memory = "1G" ``` ## Check and deploy Spring Boot (Gradle) Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Spring Boot (Gradle) None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Spring Boot (Gradle) deploy fails - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `Java project found (pom.xml) without Spring Boot or Quarkus: ox does not know how to build and start it; add an ox.toml with [build] commands and [app] start`: ox plans Spring Boot and Quarkus. For another Java app, write `[build] commands` and `[app] start`. [More](https://console.t58.sorv.dev/docs/config#build). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [Spring Boot (Maven)](https://console.t58.sorv.dev/docs/guides/spring-boot): Deploy a Spring Boot app built with Maven to your own VPS. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Spring Boot (Gradle) FAQ ### Do I need a Dockerfile or an ox.toml to deploy Spring Boot with Gradle to a VPS? No. ox does not use Docker, and `ox check` on a Spring Boot (Gradle) repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Spring Boot (Gradle)? Only what the plan names: `java temurin-17` (read from `build.gradle.kts`) and the apt packages `fontconfig`. Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Which Java version does ox install? The one your build asks for. Here `build.gradle.kts` sets a toolchain of 17, so ox installs Temurin 17, as the plan above shows. ## Related guides - [**Quarkus** Deploy a Quarkus app to your own VPS in JVM mode: ox builds it with mvnw, runs quarkus-run.jar under systemd, checks /q/health and adds PostgreSQL for it.](https://console.t58.sorv.dev/docs/guides/quarkus) - [**ASP.NET Core** Deploy an ASP.NET Core app to your own Linux VPS: ox installs the .NET 10 SDK, runs dotnet publish, starts the DLL under systemd, and adds PostgreSQL.](https://console.t58.sorv.dev/docs/guides/aspnet-core) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/spring-boot-gradle # Deploy Quarkus to a VPS in JVM mode with Postgres https://console.t58.sorv.dev/docs/guides/quarkus Deploy a Quarkus app to your own VPS in JVM mode: ox builds it with mvnw, runs quarkus-run.jar under systemd, checks /q/health and adds PostgreSQL for it. To deploy Quarkus to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `pom.xml` and `mvnw`, runs `./mvnw -B -DskipTests package`, then starts `java -jar target/quarkus-app/quarkus-run.jar` under systemd behind Caddy with HTTPS, and sets up postgres 18 for it. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Quarkus app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `pom.xml` and `mvnw`, builds the plan on the card, and starts `java -jar target/quarkus-app/quarkus-run.jar` on your server. The cylinders are the services the repo asked for. ## What ox detects in a Quarkus repo This is the plan `ox check` prints for a minimal a Quarkus app in JVM mode, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `QUARKUS_HTTP_HOST=127.0.0.1 QUARKUS_HTTP_PORT=$PORT QUARKUS_HTTP_HOST_VALIDATION_REQUIRE_LOCALHOST=false java -jar target/quarkus-app/quarkus-run.jar` | `detected:pom.xml` | | `app.health` | `/q/health` | `detected:pom.xml` | | `build.commands[0]` | `./mvnw -B -DskipTests package` | `detected:mvnw` | | `tools.java` | `temurin-21` | `detected:pom.xml` | | `packages` | `fontconfig` | `tools.java` | | `services.postgres` | `postgres 18 (shared)` | `detected:pom.xml` | `ox check` also prints these hints for it: - `the JVM sizes its heap to a quarter of the server's memory: set [app] memory (or [limits] memory) in ox.toml, and ox passes -Xmx at three quarters of it` - `Quarkus reads QUARKUS_DATASOURCE_JDBC_URL, not DATABASE_URL: set it to jdbc:postgresql://${postgres.host}:${postgres.port}/${postgres.database}, QUARKUS_DATASOURCE_USERNAME to ${postgres.user} and QUARKUS_DATASOURCE_PASSWORD to ${postgres.password}` ## The ox.toml for Quarkus None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected, and to size the JVM's heap, as the hint says: ox passes -Xmx at three quarters of [app] memory. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. Once a repo has an ox.toml, ox adds no service itself, so list each one under `[services]`. ox.toml, optional: ```toml domains = ["www.example.com"] [app] memory = "1G" [services] postgres = {} ``` ## Check and deploy Quarkus Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Quarkus None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME`, `PUBLIC_PORT` and `DATABASE_URL`. ## If the Quarkus deploy fails - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `Java project found (pom.xml) without Spring Boot or Quarkus: ox does not know how to build and start it; add an ox.toml with [build] commands and [app] start`: ox plans Spring Boot and Quarkus. For another Java app, write `[build] commands` and `[app] start`. [More](https://console.t58.sorv.dev/docs/config#build). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables#set) on the dashboard or with `ox vars`. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://console.t58.sorv.dev/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Quarkus FAQ ### Do I need a Dockerfile or an ox.toml to deploy Quarkus to a VPS? No. ox does not use Docker, and `ox check` on a Quarkus repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Quarkus? Only what the plan names: `java temurin-21` (read from `pom.xml`) and the apt packages `fontconfig`. Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Why does Quarkus not see DATABASE_URL? Quarkus reads `QUARKUS_DATASOURCE_JDBC_URL`. Set it, with the user and password, from the service's values as the hint above shows. ## Related guides - [**ASP.NET Core** Deploy an ASP.NET Core app to your own Linux VPS: ox installs the .NET 10 SDK, runs dotnet publish, starts the DLL under systemd, and adds PostgreSQL.](https://console.t58.sorv.dev/docs/guides/aspnet-core) - [**Rust** Deploy a Rust web app from a Cargo workspace to your own VPS: ox installs stable Rust, runs cargo build --release --locked, and starts the binary.](https://console.t58.sorv.dev/docs/guides/rust) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/quarkus # Deploy ASP.NET Core to a Linux VPS with PostgreSQL https://console.t58.sorv.dev/docs/guides/aspnet-core Deploy an ASP.NET Core app to your own Linux VPS: ox installs the .NET 10 SDK, runs dotnet publish, starts the DLL under systemd, and adds PostgreSQL. To deploy ASP.NET Core to a Linux VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `src/Api/Api.csproj` and `src/Api/Program.cs`, runs `dotnet publish src/Api/Api.csproj -c Release -o out`, then starts `cd out && ASPNETCORE_URLS=http://127.0.0.1:$PORT exec dotnet Api.dll` under systemd behind Caddy with HTTPS, and sets up postgres 18 for it. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A ASP.NET Core app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `src/Api/Api.csproj` and `src/Api/Program.cs`, builds the plan on the card, and starts `cd out && ASPNETCORE_URLS=http://127.0.0.1:$PORT exec dotnet Api.dll` on your server. The cylinders are the services the repo asked for. ## What ox detects in a ASP.NET Core repo This is the plan `ox check` prints for a minimal an ASP.NET Core app, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `cd out && ASPNETCORE_URLS=http://127.0.0.1:$PORT exec dotnet Api.dll` | `detected:src/Api/Api.csproj` | | `app.health` | `/healthz` | `detected:src/Api/Program.cs` | | `build.commands[0]` | `dotnet publish src/Api/Api.csproj -c Release -o out` | `detected:src/Api/Api.csproj` | | `packages` | `dotnet-sdk-10.0` | `detected:src/Api/Api.csproj` | | `services.postgres` | `postgres 18 (shared)` | `detected:src/Api/Api.csproj` | `ox check` also prints these hints for it: - `Npgsql reads a connection string, not DATABASE_URL: set ConnectionStrings__Default to Host=${postgres.host};Port=${postgres.port};Database=${postgres.database};Username=${postgres.user};Password=${postgres.password}` ## The ox.toml for ASP.NET Core None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. Once a repo has an ox.toml, ox adds no service itself, so list each one under `[services]`. ox.toml, optional: ```toml domains = ["www.example.com"] [app] [services] postgres = {} ``` ## Check and deploy ASP.NET Core Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for ASP.NET Core None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME`, `PUBLIC_PORT` and `DATABASE_URL`. ## If the ASP.NET Core deploy fails - `several ASP.NET Core projects (src/Api/Api.csproj, src/Admin/Admin.csproj); set [build] commands to dotnet publish -c Release -o out and [app] start to the one to run`: Name the project to publish in `[build] commands` and the DLL to run in `[app] start`. [More](https://console.t58.sorv.dev/docs/config#build). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: it restarted 5 times in the last 10 minutes. The Logs tab shows why.`: The release went live and then kept dying. Read the Logs tab for the error and deploy a fix, or roll back. [More](https://console.t58.sorv.dev/docs/troubleshooting#crash-loop). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables#set) on the dashboard or with `ox vars`. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://console.t58.sorv.dev/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## ASP.NET Core FAQ ### Do I need a Dockerfile or an ox.toml to deploy ASP.NET Core to a Linux VPS? No. ox does not use Docker, and `ox check` on a ASP.NET Core repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for ASP.NET Core? Only what the plan names: the apt packages `dotnet-sdk-10.0`. Your app's own dependencies are installed by the build into the release folder, not system-wide. ### How does the app connect to PostgreSQL? Npgsql reads a connection string, not `DATABASE_URL`. Set `ConnectionStrings__Default` from the service's values, as the hint above shows. ## Related guides - [**Rust** Deploy a Rust web app from a Cargo workspace to your own VPS: ox installs stable Rust, runs cargo build --release --locked, and starts the binary.](https://console.t58.sorv.dev/docs/guides/rust) - [**React with Vite** Deploy a React app built with Vite to your own VPS with no Dockerfile: ox runs the build from your lockfile and Caddy serves dist over HTTPS, no ox.toml.](https://console.t58.sorv.dev/docs/guides/react-vite) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/aspnet-core # Deploy a Rust web app to a VPS with cargo, no Docker https://console.t58.sorv.dev/docs/guides/rust Deploy a Rust web app from a Cargo workspace to your own VPS: ox installs stable Rust, runs cargo build --release --locked, and starts the binary. To deploy a Rust web app to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `crates/api/Cargo.toml` and `Cargo.lock`, runs `cargo build --release --locked`, then starts `target/release/api` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Tested on a real server.** A Rust app deployed and served on a real Ubuntu server in ox's test lab, and the plan below is what `ox check` prints for a minimal one. ox reads `crates/api/Cargo.toml` and `Cargo.lock`, builds the plan on the card, and starts `target/release/api` on your server. ## What ox detects in a Rust repo This is the plan `ox check` prints for a minimal a Rust web app in a Cargo workspace, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `target/release/api` | `detected:crates/api/Cargo.toml` | | `build.commands[0]` | `cargo build --release --locked` | `detected:Cargo.lock` | | `tools.rust` | `stable` | `default` | | `packages` | `gcc` | `tools.rust` | | `packages` | `g++` | `tools.rust` | | `packages` | `make` | `tools.rust` | | `packages` | `libc6-dev` | `tools.rust` | | `packages` | `libcrypt-dev` | `tools.rust` | ## The ox.toml for Rust None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["api.example.com"] [app] ``` ## Check and deploy Rust Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Rust None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`. ## If the Rust deploy fails - `several binaries in the Cargo workspace (api, worker); set [app] start = "target/release/" to the one to run`: Set `[app] start` to the binary to run; run another one as a worker. [More](https://console.t58.sorv.dev/docs/config#workers). - `killed by the out-of-memory killer (the build needs more memory than the project or host allows)`: The build ran out of memory. A build gets at most three quarters of the server's memory: raise memory in ox.toml if it sets one, or use a bigger server. When the message says the whole server ran out, what runs on it held the rest: use a bigger server, or stop a project, staging or preview you do not need. [More](https://console.t58.sorv.dev/docs/troubleshooting#out-of-memory). - `the app on http://127.0.0.1:8001/ did not answer within 120s: Get "http://127.0.0.1:8001/": dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://console.t58.sorv.dev/docs/troubleshooting#health-check-failed). Visitors never see a failed deploy: the old release serves until the new one is healthy. ## Next steps - [The `[tools]` keys](https://console.t58.sorv.dev/docs/config#tools): pin a runtime version. - [The `[app]` keys](https://console.t58.sorv.dev/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://console.t58.sorv.dev/docs/operate/domains); Caddy gets its certificate. - [Read the logs](https://console.t58.sorv.dev/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://console.t58.sorv.dev/docs/operate/variables) on the dashboard or with `ox vars`. ## Rust FAQ ### Do I need a Dockerfile or an ox.toml to deploy a Rust web app to a VPS? No. ox does not use Docker, and `ox check` on a Rust repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Rust? Only what the plan names: `rust stable` (ox's default) and the apt packages `gcc`, `g++`, `make`, `libc6-dev` and `libcrypt-dev`. Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Which binary does ox run in a workspace? The one binary in the workspace's members, here `target/release/api`. With several, `ox check` asks you to set `[app] start` to one. ## Related guides - [**React with Vite** Deploy a React app built with Vite to your own VPS with no Dockerfile: ox runs the build from your lockfile and Caddy serves dist over HTTPS, no ox.toml.](https://console.t58.sorv.dev/docs/guides/react-vite) - [**Create React App** Deploy a Create React App project to your own VPS: ox runs react-scripts build and Caddy serves the build folder over HTTPS. No Docker, no ox.toml.](https://console.t58.sorv.dev/docs/guides/create-react-app) The [Stacks page](https://console.t58.sorv.dev/docs/guides) lists every stack, and the ones ox does not deploy yet. Last updated 2026-10-09. The page as HTML: https://console.t58.sorv.dev/docs/guides/rust # ox.toml reference: every key, default and example https://console.t58.sorv.dev/docs/config The ox.toml reference: every key with its default, what ox detects from your repo, services, variables, and working examples for Next.js, Python, Go, SPAs. The deploy config is a file named `ox.toml` at your repo root. It names the web process, workers, cron jobs, services, and build. Most repos need none at first, because ox detects these from your files; write one for what detection got wrong. Unknown keys, wrong types, and bad values are errors, and every problem is listed at once. Secrets never live in this file; they go in the dashboard's Variables tab (see [Variables](https://console.t58.sorv.dev/docs/operate/variables)). For a complete file for one framework, start from a guide: [Django](https://console.t58.sorv.dev/docs/guides/django), [Next.js](https://console.t58.sorv.dev/docs/guides/nextjs), [FastAPI](https://console.t58.sorv.dev/docs/guides/fastapi), [Node](https://console.t58.sorv.dev/docs/guides/node), [Laravel](https://console.t58.sorv.dev/docs/guides/laravel), [Rails](https://console.t58.sorv.dev/docs/guides/rails) or a [static site](https://console.t58.sorv.dev/docs/guides/static-site). When a deploy fails on a key, [Troubleshooting](https://console.t58.sorv.dev/docs/troubleshooting) explains the message. ox.toml: the whole file at a glance: ```toml domains = ["example.com"] # served by [app], or by [static] when there is no [app] packages = ["ffmpeg"] # apt system libraries only (no toolchains) [app] # the one web process; gets $PORT enabled = false # no web process and no detected static site (workers only); default: the detected app start = "serve" # default: detected health = "/healthz" # HTTP 2xx/3xx on this path; default: any answer to GET / but 502-504 port = 9034 # pin; default: allocated and recorded memory = "512M" # this process's cap sandbox = "relaxed" # the only value; default is strict max_upload = "2G" # a PHP app's largest upload; default: 512M stop_before_start = true # an app that cannot run two copies at once: stop the running copy first (a short gap per deploy); default: false [static] # files served by Caddy dir = "dist" # relative to the repo root spa = true # a path with no file or page serves 200.html, else index.html; a missing file (.js, .ico, ...) is a 404 api = ["/api", "/admin"] # these prefixes go to [app]; requires [app] paths = { "/static" = "backend/static" } # more built dirs, each at its URL path [build] # runs as the project user, variables available install = "npm ci --include=dev" # runs first; default: the lockfile's install commands = ["make build"] # runs after install; default: detected migrate = "make migrate" # after build, before the switch; default: detected [workers] worker = "celery -A app worker" # short form bot = { run = "python bot.py", memory = "512M", sandbox = "relaxed", port = true, health = "/health" } [cron] # 5-field cron or @hourly/@daily/@weekly/@monthly, UTC digest = { schedule = "0 7 * * *", run = "python manage.py send_digest" } [services] # postgres, redis, neo4j, qdrant, mysql, clickhouse, meilisearch, rabbitmq, or your own (run = ...) postgres = { version = "18", extensions = ["vector"] } redis = {} qdrant = { only_for_this_project = true, port = 9200 } [tools] # a mise tool = version (no npm:/cargo:/pipx:-style package backends); default: from repo files node = "24" bun = "1.3" [storage] keep = ["media"] # repo paths, linked to the data dir, survive releases [limits] # the project's whole slice (processes + builds) memory = "1G" cpu = 1.5 # cores [models] huggingface = ["org/repo", "org/repo@revision"] ``` ## Validate locally Run `ox check [dir]` (default `.`) on your machine before pushing. It is offline and reads nothing but the checkout. It prints the resolved plan with each value's source (`declared`, `detected:`, `default`), the variables to set on the dashboard, and every problem at once. Fix them all until it prints `Ready to deploy.` `ox check --json` gives the same data as JSON. These are the same checks the deploy runs. A bad manifest or a missing variable stops the deploy before anything on your server changes, with the exact reason on the run. ## Top-level keys Top-level keys sit above every section. Domains are the names Caddy serves, and packages are Ubuntu libraries; a toolchain there is refused. - `domains`: the names Caddy serves, served by `[app]`, or by `[static]` when there is no app. Domains can also be added per project in the dashboard and apply on the next deploy. - `required_variables`: keys the app cannot start without that no `.env.example` lists (`["JWT_SECRET"]`). They are asked before the first deploy like an example file's keys, and Generate makes one only for a secret the app makes for itself (`JWT_SECRET`, `SESSION_SECRET`, a salt), never for a provider's key or token. Names only: set the values on the Variables tab. - `packages`: Ubuntu system libraries (`ffmpeg`, `libmagic1t64`). Toolchains (`nodejs`, `npm`, `python3-pip`, `golang*`, `rustc`, `cargo`) are refused; declare them in `[tools]`. A package that rewrites sign-in, boot or the firewall (`openssh-server`, `ufw`, `sudo`, `libpam-*` and a few more) is refused: take it out of `packages`. A server a package ships (`memcached`) is installed but kept off; declare the service in `[services]` to run it. This applies on servers whose agent is updated. - Git submodules deploy at the commit they pin, over https only (GitHub ssh URLs are rewritten). A project made from the repository picker also reads a private submodule that is a repository of the same owner that the ox GitHub App can read and your GitHub account can see: add it to the App's installation. Another owner's private repository cannot be a submodule. A project cloned from a plain URL or with a deploy key fetches public submodules only. `ox.toml` and detection read only the main repo. ## [app] A request for one of your domains reaches Caddy, which passes it to the app on its port. The app takes traffic only once its health path answers. The one web process, behind Caddy. An `ox.toml` with neither `[app]` nor `[static]` still gets the app ox detects, so a file that only adds workers keeps the web app when a web framework (or a static site) proves it. When only a generic entry point does (a `package.json` start script with no web framework dependency, a Go main package, a Cargo binary), an `ox.toml` with workers or cron jobs must choose: `ox check` stops until it sets `[app] start` or `[app] enabled = false`. - `enabled`: `false` says the project has no web process, so ox adds none (a bot or a queue consumer), and it drops a detected static site too: the project serves nothing. No other `[app]` key goes with it. - `start`: the command that serves HTTP on `$PORT`. Default: detected from the repo. Commands run with `bash -euo pipefail -c` in the release directory, with the tools on `PATH`; `$PORT` and other variables expand in the shell. A server that does not read `PORT` (granian, uvicorn, hypercorn, daphne, waitress-serve, `flask run`) needs its port option (`--port $PORT`, `--bind 127.0.0.1:$PORT`), or `ox check` refuses the start. - `health`: a path that must return HTTP 2xx/3xx. Default: the app must answer `GET /` within 5 seconds with any status but 502, 503 or 504 (a 404 counts); set a path if `/` is slower. - `port`: leave it out. ox allocates and records a port, and the zero-downtime switch runs two sides at once. A pin (1024–65535, unique on the host) runs one side and restarts in place, with a moment of downtime per deploy. Pin only for an app that hardcodes its port. - `memory`: this process's cap (`K`/`M`/`G`). A value larger than `[limits] memory` fails validation. - `max_upload`: the largest file a PHP app (FrankenPHP) accepts (`K`/`M`/`G`). Default: `512M`. ox sets PHP's `upload_max_filesize` to it and `post_max_size` 8M above it (room for the form's other fields), after any `php.ini` in the repo. Unset, a limit a repo `php.ini` sets stays the repo's and ox writes the other to fit; set, it wins, and `ox check` says which line it replaces. Any other project that sets it fails the plan. - `stop_before_start`: `true` for an app that cannot run two copies at once. ox normally starts the new release beside the running one and switches with no gap, so an app that holds a lock in its data folder (Gitea's queues), a second fixed port (Mailpit's SMTP), one instance per database (Pocket ID) or a port saved in its own database (Calibre-Web) fails every redeploy. With `true`, a deploy stops the running copy and then starts the new release: the site answers 502 from the stop until the new release answers, on every deploy (the run log says so). If the new release does not come up, the running release is started again. If the agent is killed hard in the middle of a deploy, the site keeps serving the new release until the next deploy or a rollback records it. Default: `false`. Needs a current agent (`ox servers update`). - `sandbox`: the only value is `"relaxed"`; the default is strict. ## [static] Caddy serves files from the built folders. A path with no file gets `index.html` when `spa` is on, and the `api` prefixes go to your app. - `dir`: the built files Caddy serves, relative to the repo root. It must exist after the build. Without `[app]`, the project is a static site. - `spa`: paths with no file serve `index.html` (single-page apps), or `200.html` when the build writes one. A prerendered page in a folder, like `story/index.html`, answers at `/story`. A missing path that ends in a file extension (`/favicon.ico`, an old `/assets/app-Abc12345.js`) is a 404, not the app's page; a route like `/users/john.doe` still gets the page, but a client route that ends in one of those extensions (`/files/report.pdf`) is a 404 too. - `api`: URL prefixes that stay proxied to `[app]`. Requires `[app]`. - `paths`: more built directories, each served at its URL path (a Django `collectstatic` output next to an SPA). `dir` may be omitted when only `paths` is set. ox sets the response headers. `index.html`, the SPA fallback and hand-named files get `Cache-Control: no-cache`, so a release or a rollback shows at once. Content-hashed files (under `/_next/static/`, `/_app/immutable/`, `/_nuxt/` or `/_astro/`, a hex hash like `main.3f2a1b9c.js`, Vite's scripts and styles like `/assets/index-BOX4g1k6.js` or `/assets/app-DN-Banja.js`, whose hash holds a digit, `-` or `_`) are cached for a year. Static files may be framed only by the same site. Every response your app or a static file sends gets `nosniff`, a referrer policy and, with a domain, HSTS, each only when the response has none. ox sets no Content-Security-Policy and no caching on your app's responses; a static site sets its policy with a `` tag in `index.html`. A static folder never serves `.env*`, `.git*`, `.ht*`, `.ssh`, `.svn`, `.hg`, `.npmrc`, `.netrc`, `.aws`, `.docker` or `.pgpass` (a request is a 404; `.well-known` is served), and `ox check` names any `.env` it finds in the folder, at any depth. A missing file gets the `404.html` at the top of your static folder, with status 404, when the build writes one, else an empty 404. A 404 your app sends passes through as it is. In a single-page app an unknown route still gets the app's page with status 200: ox can't know your routes, so show a not-found view from your router. ## [build] Each deploy builds a new release beside the live one: install, your build commands, then the migration. Traffic switches only after the new release passes its health check. - `install`: installs the packages, first, as the project user in the new release. Default: the lockfile's install. Declare it only when the install is not the root lockfile's. `install = ""` runs no install. - `commands`: run after `install`, in order. Default: detected from the package scripts. Declaring them replaces only the detected build steps; the install still runs, and `ox check` adds a hint for each detected step the list leaves out (Django's `collectstatic`), so list it again when the release needs it. `commands = []` runs no build: use it for a folder served as it is, in a repo whose `package.json` builds something else. - `migrate`: runs after the build, before traffic switches. A snapshot of the database is taken first when it runs against a database with tables. Default: detected (Django, Prisma). `migrate = ""` runs none. - `skip_submodules`: submodule paths a deploy does not fetch, like `["apps/server/src/ee"]`; each folder stays empty, as after a plain `git clone`. Use it for a private module the app builds without (an enterprise edition only its vendor can read). Default: every submodule is fetched. ## [workers] Every worker runs as its own process next to the app. Each gets a port of its own in `$PORT`. With `port = true` the app reaches a worker at `$BOT_URL`, and its health check calls that port. Background processes, each its own systemd unit, except a public worker the app serves (below). A worker is a command string (short form) or a table: `{ run, memory, sandbox, port, health, stop_timeout, paths, public, domains }`. - Names match `^[a-z][a-z0-9-]{0,27}$`; `app` is reserved. - Every worker and cron job gets a port of its own in `$PORT`. `port = true` means the project's other processes call this worker: ox publishes a `_URL` variable (name upper-cased, `-` becomes `_`) and starts it before the app switches. This is how processes of one project call each other. - `health` is checked on the worker's own `$PORT`. - `paths` (default none, needs `port = true`) are parts of the site the worker serves instead of the app, like `paths = ["/api/v1/streaming"]` for Mastodon's streaming server or `["/cable"]` for AnyCable. Each prefix and everything under it goes to the worker's `$PORT`, websockets included, on every domain and the preview link, with the path kept as the browser asked for it. A prefix starts with `/`, is not `/` itself, has no trailing `/`, and belongs to one worker; the longest prefix wins, and a `[static]` `paths` or `api` prefix under a worker's prefix is refused, as is a worker's prefix at or under a folder the app's `.htaccess` closes. Unlike the app, the worker has no second side: every deploy, even one that changes only the app, restarts it in place, so its paths answer 502 for those seconds and every websocket open on them drops (its clients must reconnect). The prefix is never stripped. - `public = true` (default false, needs `port = true`, or no `run`) gives the worker a web address of its own, for an app that serves a second site on a name of its own, like Logto's Admin Console at `ADMIN_ENDPOINT`. Every request for the worker's names goes to its `$PORT`, every path and websockets included, with the site's password and headers. Its names are its preview link, `--.