# ox: the full docs > ox deploys a GitHub repository onto the user's 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. Every ox docs page, stack guide and the story as Markdown, then the blog's post summaries, in the order below. Each part opens with its title and its address. The list of links with a note each is https://deploywithox.com/llms.txt Contents: - [ox docs: deploy a GitHub repo to your own Ubuntu VPS](https://deploywithox.com/docs) - [Quickstart: deploy a GitHub repo to a VPS in 5 steps](https://deploywithox.com/docs/quickstart) - [Stacks: deploy guides for Node, Python, PHP and Java](https://deploywithox.com/docs/guides) - [Deploy Django to a VPS with gunicorn and Postgres](https://deploywithox.com/docs/guides/django) - [Deploy Next.js to a VPS: self-host without Vercel](https://deploywithox.com/docs/guides/nextjs) - [Deploy FastAPI to a VPS with uvicorn and Postgres](https://deploywithox.com/docs/guides/fastapi) - [Deploy a Node.js or Express API to your own VPS](https://deploywithox.com/docs/guides/node) - [Deploy Laravel to a VPS with a queue and scheduler](https://deploywithox.com/docs/guides/laravel) - [Deploy Ruby on Rails to a VPS with Puma and Postgres](https://deploywithox.com/docs/guides/rails) - [Deploy a static site to a VPS: Vite, Astro, HTML](https://deploywithox.com/docs/guides/static-site) - [Deploy a React app built with Vite to your own VPS](https://deploywithox.com/docs/guides/react-vite) - [Deploy a Create React App build to your own VPS](https://deploywithox.com/docs/guides/create-react-app) - [Deploy a Vue app to a VPS: build, HTTPS, no Docker](https://deploywithox.com/docs/guides/vue) - [Deploy an Angular app to a VPS with Caddy and HTTPS](https://deploywithox.com/docs/guides/angular) - [Deploy Angular SSR to your own VPS with Node.js 24](https://deploywithox.com/docs/guides/angular-ssr) - [Deploy a Next.js static export to your own VPS](https://deploywithox.com/docs/guides/nextjs-static-export) - [Deploy a Gatsby site to your own VPS with HTTPS](https://deploywithox.com/docs/guides/gatsby) - [Deploy a Docusaurus docs site to your own VPS](https://deploywithox.com/docs/guides/docusaurus) - [Deploy an Astro static site to your own VPS server](https://deploywithox.com/docs/guides/astro) - [Deploy Astro SSR with the Node adapter to a VPS](https://deploywithox.com/docs/guides/astro-ssr) - [Deploy SvelteKit with adapter-node to your own VPS](https://deploywithox.com/docs/guides/sveltekit) - [Deploy a static SvelteKit site to your own VPS](https://deploywithox.com/docs/guides/sveltekit-static) - [Deploy Nuxt to a VPS: SSR on Node with no Docker](https://deploywithox.com/docs/guides/nuxt) - [Deploy React Router 7 or Remix to your own VPS](https://deploywithox.com/docs/guides/react-router) - [Deploy a React Router SPA (ssr: false) to a VPS](https://deploywithox.com/docs/guides/react-router-spa) - [Deploy SolidStart to your own VPS with Node.js](https://deploywithox.com/docs/guides/solidstart) - [Deploy Qwik City with the Express adapter to a VPS](https://deploywithox.com/docs/guides/qwik) - [Deploy NestJS to a VPS with PostgreSQL, no Docker](https://deploywithox.com/docs/guides/nestjs) - [Deploy a Fastify API with PostgreSQL to a VPS](https://deploywithox.com/docs/guides/fastify) - [Deploy a Hono API on Bun to your own VPS, no Docker](https://deploywithox.com/docs/guides/hono-bun) - [Deploy a BullMQ worker and Redis queue to a VPS](https://deploywithox.com/docs/guides/bullmq-worker) - [Deploy Deno Fresh to your own VPS with deno serve](https://deploywithox.com/docs/guides/deno-fresh) - [Deploy Flask to a VPS with gunicorn and PostgreSQL](https://deploywithox.com/docs/guides/flask) - [Deploy Symfony to a VPS with FrankenPHP and Doctrine](https://deploywithox.com/docs/guides/symfony) - [Deploy a plain PHP site to a VPS with FrankenPHP](https://deploywithox.com/docs/guides/php) - [Deploy Phoenix to a VPS: mix release and Postgres](https://deploywithox.com/docs/guides/phoenix) - [Deploy Spring Boot to a VPS with Maven and Postgres](https://deploywithox.com/docs/guides/spring-boot) - [Deploy Spring Boot built with Gradle to your own VPS](https://deploywithox.com/docs/guides/spring-boot-gradle) - [Deploy Quarkus to a VPS in JVM mode with Postgres](https://deploywithox.com/docs/guides/quarkus) - [Deploy ASP.NET Core to a Linux VPS with PostgreSQL](https://deploywithox.com/docs/guides/aspnet-core) - [Deploy a Rust web app to a VPS with cargo, no Docker](https://deploywithox.com/docs/guides/rust) - [ox.toml reference: every key, default and example](https://deploywithox.com/docs/config) - [ox CLI reference: deploy, logs and vars from a shell](https://deploywithox.com/docs/cli) - [Operate your app: logs, rollbacks, backups, domains](https://deploywithox.com/docs/operate) - [View app and deploy logs: live, search, download](https://deploywithox.com/docs/operate/logs) - [Roll back a deployment to the previous release](https://deploywithox.com/docs/operate/rollback) - [Daily database backups and restore on your VPS](https://deploywithox.com/docs/operate/backups) - [Environment variables and secrets for your app](https://deploywithox.com/docs/operate/variables) - [Staging and branch preview deployments on a VPS](https://deploywithox.com/docs/operate/previews) - [Service dashboards: Postgres, Redis, NATS and more](https://deploywithox.com/docs/operate/services) - [Add a custom domain to your app with automatic HTTPS](https://deploywithox.com/docs/operate/domains) - [Troubleshooting failed deploys: errors and fixes](https://deploywithox.com/docs/troubleshooting) - [Why I built ox](https://deploywithox.com/story) - [Blog](https://deploywithox.com/blog) # ox docs: deploy a GitHub repo to your own Ubuntu VPS https://deploywithox.com/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 deploys a GitHub repository to an Ubuntu server you own, as a plain systemd service behind Caddy, with 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://deploywithox.com/docs/quickstart) - [**Guides**A complete, checked ox.toml for a specific kind of app.](https://deploywithox.com/docs/guides) - [**Config reference**Every ox.toml key, what ox detects, services, and variables.](https://deploywithox.com/docs/config) - [**Troubleshooting**A failed health check, a failed build, or a domain that does not work.](https://deploywithox.com/docs/troubleshooting) - [**Operate**Logs, rollbacks, backups, variables, domains, staging and previews.](https://deploywithox.com/docs/operate) - [**CLI reference**Every ox command, with `--json` for scripts and agents.](https://deploywithox.com/docs/cli) ## 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. The same text is at [/ox-skill.md](https://deploywithox.com/ox-skill.md). Every docs page also comes as Markdown: add `.md` to its address, or press **Copy page as Markdown**. [/llms.txt](https://deploywithox.com/llms.txt) lists them all with a note each, and [/llms-full.txt](https://deploywithox.com/llms-full.txt) is every page and guide in one Markdown file. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs # Quickstart: deploy a GitHub repo to a VPS in 5 steps https://deploywithox.com/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. Most repos need no config file. 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://deploywithox.com/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 console 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 console each time. The script installs the ox agent and Caddy, then the server shows up in the console. ## 3. Add the repository as a project Press **New project**, pick the server, pick the repository, and press **Continue**. From a terminal, `ox new --server ` does the same. ## 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://deploywithox.com/docs/config) only for what detection got wrong. The [framework guides](https://deploywithox.com/docs/guides) show a complete one for Django, Next.js, FastAPI, Node, Laravel and static sites, and [Variables](https://deploywithox.com/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 curl -fsSL https://deploywithox.com/install.sh | sh ox login 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://deploywithox.com/docs/troubleshooting) covers the common ones. ## Next steps after your first deploy - [Add a custom domain with HTTPS](https://deploywithox.com/docs/operate/domains): create one A record and Caddy gets the certificate. - [Read your app's logs](https://deploywithox.com/docs/operate/logs) live, or search them. - [Roll back a deploy](https://deploywithox.com/docs/operate/rollback) that went wrong, without a rebuild. - [See your daily backups](https://deploywithox.com/docs/operate/backups) and add an S3 bucket. - [Use the CLI](https://deploywithox.com/docs/cli) for everything the dashboard does. The [Operate](https://deploywithox.com/docs/operate) section covers the rest of running your app day to day. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/quickstart # Stacks: deploy guides for Node, Python, PHP and Java https://deploywithox.com/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. Most repos need no ox.toml at all, so the [Quickstart](https://deploywithox.com/docs/quickstart) is the shortest path for any of them. - [All](https://deploywithox.com/docs/guides#lang-all) - [JavaScript and TypeScript](https://deploywithox.com/docs/guides#lang-js) - [Python](https://deploywithox.com/docs/guides#lang-python) - [Ruby](https://deploywithox.com/docs/guides#lang-ruby) - [PHP](https://deploywithox.com/docs/guides#lang-php) - [Java](https://deploywithox.com/docs/guides#lang-java) - [C# and .NET](https://deploywithox.com/docs/guides#lang-dotnet) - [Elixir](https://deploywithox.com/docs/guides#lang-elixir) - [Rust](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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.*Plan verified, real-server test pending*](https://deploywithox.com/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://deploywithox.com/docs/guides/django) *Tested on a real server* - [Node API and SPA in one repo](https://deploywithox.com/docs/guides/node) *Tested on a real server* - [Vue with Vite on Bun](https://deploywithox.com/docs/guides/static-site) *Tested on a real server* - [Go (net/http or Gin) with Postgres and Redis](https://deploywithox.com/docs/config#detection) *Tested on a real server* - [Python bot or worker with no web process](https://deploywithox.com/docs/config#workers) *Tested on a real server* - [Django ASGI (uvicorn or daphne) with Redis](https://deploywithox.com/docs/guides/django) *Tested on a real server* - [FastAPI with Redis, a worker and cron](https://deploywithox.com/docs/guides/fastapi) *Tested on a real server* - [Django with pgvector](https://deploywithox.com/docs/guides/django) *Tested on a real server* - [Python with Qdrant](https://deploywithox.com/docs/guides/fastapi) *Tested on a real server* - [FastAPI with Neo4j](https://deploywithox.com/docs/guides/fastapi) *Tested on a real server* - [CPU model inference (FastAPI and a Hugging Face model)](https://deploywithox.com/docs/guides/fastapi) *Tested on a real server* - [Go Fiber, Echo or Chi](https://deploywithox.com/docs/config#detection) *Plan verified, real-server test pending* - [WordPress with MariaDB (needs an ox.toml)](https://deploywithox.com/docs/config#services) *Plan verified, real-server test pending* - [Strapi with Postgres](https://deploywithox.com/docs/guides/node) *Plan verified, real-server test pending* - [Hono on Node](https://deploywithox.com/docs/guides/node) *Plan verified, real-server test pending* - [Koa](https://deploywithox.com/docs/guides/node) *Plan verified, real-server test pending* - [Rust Actix Web](https://deploywithox.com/docs/guides/rust) *Plan verified, real-server test pending* - [Rust Rocket](https://deploywithox.com/docs/guides/rust) *Plan verified, real-server test pending* - [Eleventy](https://deploywithox.com/docs/guides/static-site) *Plan verified, real-server test pending* - [Payload CMS on Next.js](https://deploywithox.com/docs/guides/nextjs) *Plan verified, real-server test pending* - [Wagtail](https://deploywithox.com/docs/guides/django) *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://deploywithox.com/docs/config#services) - [Streamlit](https://deploywithox.com/docs/config#app) - [Gradio](https://deploywithox.com/docs/config#app) - [Django with MariaDB (mysqlclient)](https://deploywithox.com/docs/config#services) - [Rails with Sidekiq and Redis](https://deploywithox.com/docs/config#workers) - [Rails with Solid Queue](https://deploywithox.com/docs/config#workers) - [Laravel with a queue worker and the scheduler](https://deploywithox.com/docs/guides/laravel) - [Express in TypeScript (tsc to dist)](https://deploywithox.com/docs/config#app) - [Hugo](https://deploywithox.com/docs/config#static) - [Jekyll](https://deploywithox.com/docs/config#static) - [Telegram or Discord bot in Node](https://deploywithox.com/docs/config#workers) - [Kotlin Ktor](https://deploywithox.com/docs/config#build) - [n8n](https://deploywithox.com/docs/config#app) - [Uptime Kuma](https://deploywithox.com/docs/config#app) - [Express with SQLite (better-sqlite3)](https://deploywithox.com/docs/config#app) - [Flask with SQLite](https://deploywithox.com/docs/config#app) - [Go with SQLite](https://deploywithox.com/docs/config#app) - [Rust full-stack (Leptos or Dioxus)](https://deploywithox.com/docs/config#build) - [Bun with Elysia](https://deploywithox.com/docs/config#app) - [TanStack Start](https://deploywithox.com/docs/config#app) - [MkDocs or VitePress](https://deploywithox.com/docs/config#static) - [Directus](https://deploywithox.com/docs/config#app) - [Medusa](https://deploywithox.com/docs/config#app) - [Sinatra or Roda](https://deploywithox.com/docs/config#app) - [Hanami](https://deploywithox.com/docs/config#app) - [Odoo](https://deploywithox.com/docs/config#app) - [Drupal](https://deploywithox.com/docs/config#app) - [Slim PHP API](https://deploywithox.com/docs/config#app) - [CodeIgniter](https://deploywithox.com/docs/config#app) - [Laravel with Inertia (Vue or React)](https://deploywithox.com/docs/guides/laravel) - [Blazor Server](https://deploywithox.com/docs/config#build) - [Scala Play](https://deploywithox.com/docs/config#build) - [Clojure Ring](https://deploywithox.com/docs/config#build) - [Elixir Plug or Bandit without Phoenix](https://deploywithox.com/docs/config#build) - [Gleam](https://deploywithox.com/docs/config#build) - [Zig HTTP server](https://deploywithox.com/docs/config#build) - [C or C++ binary](https://deploywithox.com/docs/config#build) - [Dart (Shelf or Serverpod)](https://deploywithox.com/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 or Playwright worker:** Chromium is snap-only on Ubuntu 26.04, and its sandbox needs what ox's service sandbox blocks. - **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://deploywithox.com/docs/config#detection) and the [config examples](https://deploywithox.com/docs/config#examples) cover more, and [Troubleshooting](https://deploywithox.com/docs/troubleshooting) lists the messages every deploy can print. Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides # Deploy Django to a VPS with gunicorn and Postgres https://deploywithox.com/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` 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` sets `STATIC_ROOT`. - PostgreSQL, when `psycopg`, `psycopg2` or `asyncpg` is a dependency, and Redis when `redis` or `celery[redis]` is. When neither gunicorn nor uvicorn is a dependency, `ox check` says `Django found, but neither gunicorn nor uvicorn 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://deploywithox.com/docs/config#static)), so whitenoise is not needed. `[storage] keep` links `media` to a writable folder that survives deploys, because a release's own files are read-only while it runs. ## 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 listed in `.env.example` 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, 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://deploywithox.com/docs/troubleshooting#health-check-failed) explains the message. ## Next steps - [Set SECRET_KEY and other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://deploywithox.com/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/django # Deploy Next.js to a VPS: self-host without Vercel https://deploywithox.com/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 `engines.node` in `package.json`, or from `.nvmrc`, `.node-version` or `mise.toml`. With none of them, Node 24. - The install step from the lockfile: `npm ci` for `package-lock.json`, `pnpm install --frozen-lockfile`, `yarn install --frozen-lockfile` or `bun install --frozen-lockfile`. - `npm run build` when there is a `build` script, and `npm run start` when there is a `start` script (with your package manager in place of npm). Without the scripts, `npx next build` and `npx next start -H 127.0.0.1 -p $PORT`. - 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. 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" 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. A release's files are read-only while it runs. Next.js saves optimized images under `.next/cache`, so if you use `next/image`, add this ([[storage] keep](https://deploywithox.com/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 named in `.env.example` must be set before a deploy can start. 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, 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 your start script does not pass a fixed `-p`. - `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://deploywithox.com/docs/troubleshooting), and every key is in the [config reference](https://deploywithox.com/docs/config). ## Next steps - [Set AUTH_SECRET and other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [Turn on a preview per branch](https://deploywithox.com/docs/operate/previews), each with its own database. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/nextjs # Deploy FastAPI to a VPS with uvicorn and Postgres https://deploywithox.com/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`. - 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. Set `[app] start` yourself then. ## 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://deploywithox.com/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 `postgres://`. SQLAlchemy wants the driver in the name, so change the start of it when you read it. When SQLAlchemy is a dependency, `ox check` prints this line for your driver as a hint. app/db.py: ```python import os from sqlalchemy import create_engine url = os.environ["DATABASE_URL"].replace("postgres://", "postgresql+psycopg://", 1) 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 named in `.env.example` must be set before a deploy can start. 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, DATABASE_URL Set on the dashboard before the first deploy: SECRET_KEY hint: SQLAlchemy (from uv.lock) rejects DATABASE_URL's postgres:// scheme; in your code, use os.environ["DATABASE_URL"].replace("postgres://", "postgresql+psycopg://", 1) 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. A wrong module path such as `main:app` instead of `app.main:app`, or a `KeyError` for a missing variable, are the usual causes. - `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://deploywithox.com/docs/troubleshooting), and every key is in the [config reference](https://deploywithox.com/docs/config). ## Next steps - [Set SECRET_KEY and other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://deploywithox.com/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/fastapi # Deploy a Node.js or Express API to your own VPS https://deploywithox.com/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 `engines.node` in `package.json`, or from `.nvmrc`, `.node-version` or `mise.toml`. With none of them, Node 24. - The install step from the lockfile: `npm ci` for `package-lock.json`, `pnpm install --frozen-lockfile`, `yarn install --frozen-lockfile` or `bun install --frozen-lockfile`. With no lockfile, `npm 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). - 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`, when the tool is a dependency. - 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. 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" 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://deploywithox.com/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 named in `.env.example` must be set before a deploy can start. 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, 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://deploywithox.com/docs/troubleshooting), and every key is in the [config reference](https://deploywithox.com/docs/config). ## Next steps - [Set SESSION_SECRET and other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [See the daily database backups](https://deploywithox.com/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/node # Deploy Laravel to a VPS with a queue and scheduler https://deploywithox.com/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`); your `build` script; the migrate step `php artisan migrate --force`; and the health page `/up` when `composer.lock` has Laravel 11 or 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 = "mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views storage/logs && 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" 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://deploywithox.com/docs/config#workers)). - `[cron] scheduler` runs Laravel's scheduler every minute ([[cron]](https://deploywithox.com/docs/config#cron)). - `[services] postgres` gives your app its own PostgreSQL database. - `[storage] keep` makes `storage` writable and keeps it across deploys, because a release's own files are read-only while it runs. - `[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_URL`, so you point one at the other in the next step. - For MySQL, use `mysql = {}` instead. ox gives you `MYSQL_URL`, served by MariaDB. Set `DB_CONNECTION=mysql` and `DB_URL=${MYSQL_URL}`, and swap `php-pgsql` for `php-mysql` in `packages`. - 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 DB_CONNECTION=pgsql DB_URL=${DATABASE_URL} ``` 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}` and `${DATABASE_URL}` point at values ox provides. `LOG_CHANNEL=stderr` sends Laravel's log to `ox logs`. Every key in your `.env.example` must have a value before the first deploy, even an empty one. Laravel's own `.env.example` lists many keys, so trim it to the ones your app uses. 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.` ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, DATABASE_URL Set on the dashboard before the first deploy: APP_ENV, APP_KEY, APP_DEBUG, APP_URL, LOG_CHANNEL, DB_CONNECTION, DB_URL 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. - `the app wrote to storage/logs/laravel.log, and a release's files are read-only while it runs`: `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://deploywithox.com/docs/troubleshooting) explains each message, and the [config reference](https://deploywithox.com/docs/config) lists every key. ## Next steps - [Set APP_KEY and the other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://deploywithox.com/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/laravel # Deploy Ruby on Rails to a VPS with Puma and Postgres https://deploywithox.com/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 puma -C config/puma.rb -b tcp://127.0.0.1:$PORT` 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 puma -C config/puma.rb -b tcp://127.0.0.1:$PORT` as the start command, 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. `build-essential` always comes 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 puma -C config/puma.rb -b tcp://127.0.0.1:$PORT" 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 writable folder that survives deploys, because a release's own files are read-only while it runs. 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, 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://deploywithox.com/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/, which is read-only in a release: add an ox.toml with [storage] keep = ["storage"]`. With that line the database lives in a folder that survives deploys. - `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. Jekyll sites are not supported yet, and ox says so instead of guessing. ## Next steps - [Set RAILS_MASTER_KEY and other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://deploywithox.com/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/rails # Deploy a static site to a VPS: Vite, Astro, HTML https://deploywithox.com/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. - **Other generators:** Docusaurus (`build`), Eleventy (`_site`), SvelteKit with `adapter-static` (`build`), Nuxt with a `generate` script (`.output/public`) and a Next.js static export (`out`). Each needs its dependency and its config file. A Nuxt app 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` for `package-lock.json`, `pnpm install --frozen-lockfile`, `yarn install --frozen-lockfile` or `bun install --frozen-lockfile`), and the build is your `build` script, like `npm run build`. The Node.js version comes from `.nvmrc`, `.node-version`, `mise.toml` or `engines` in `package.json`, else ox uses Node.js 24. A `start` script changes the plan: ox then runs it as an app instead of serving files. Remove it, or write the ox.toml below. 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`. Leave it out for Astro and plain HTML, where each page is its own file. - `[tools] node` pins the Node.js version for the build. The install and build still come from your lockfile and `package.json`. Set `[build] commands` only when your build is not the `build` script. With `dir = "."`, Caddy serves every file in the repo, so keep anything private out of it, or move the site into a folder such as `public` and set `dir = "public"`. ## Services and an API next to the site A static site needs none, so leave `[services]` out. If your site calls an API, that API is its own project, or the same repo with an `[app]` and `api = ["/api"]` under `[static]` (see the [config reference](https://deploywithox.com/docs/config#static)). ## Build-time variables such as VITE_API_URL Your variables are there during the build, so a Vite app can read `VITE_API_URL` with `import.meta.env.VITE_API_URL`. Set it on the dashboard's Variables tab, or with `ox vars set VITE_API_URL`. A changed value needs a new build, which **Save & deploy** starts. Everything the build puts into your files is public, because every visitor downloads them. Never put a secret key in a static site. ## Check and deploy the static site Run `ox check` in the repo. It works offline and lists the plan. For the Vite file above it prints: ox check for a Vite app: ```text static.dir dist declared build.install npm ci detected:package-lock.json build.commands[0] npm run build detected:package.json tools.node 24 declared domains[0] www.example.com declared Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST Ready to deploy. ``` Then ship it: Ship from a terminal: ```sh ox check ox deploy --wait ox runs # past deploys, with their ids ox logs --run # one deploy's log ``` `ox deploy --wait` prints the build as it runs and exits with its result. A static site has no app process, so `ox logs --follow` says so and points you to `ox runs`, and the dashboard's Logs tab points to the Deployments tab. Read a past build with `--run` instead. ## If the static site deploy fails - `the build left no dist directory`: the build wrote its files somewhere else. Set `[static] dir` to the folder your build makes, like `build` for Create React App or `out` for a Next.js static export. - `nothing to run: ox found no start command, static site, worker, or cron job`: ox did not see a site, such as a generator whose config file is missing. Add `[static] dir`; you do not need an `[app] start` for a static site. - `build 1/1: npm run build: exit status 1`: your build failed. The lines above it are your build's own output. Run `npm run build` on your computer, fix it, and push. Your visitors never see a failed deploy: the previous release keeps serving. [Troubleshooting](https://deploywithox.com/docs/troubleshooting#build-failed) explains each message, and the [config reference](https://deploywithox.com/docs/config#static) lists every `[static]` key. ## Next steps - [Set build-time variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read a deploy's build log](https://deploywithox.com/docs/operate/logs#run-logs) with `ox logs --run `. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [Turn on a preview per branch](https://deploywithox.com/docs/operate/previews) to check a change before it goes live. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/static-site # Deploy a React app built with Vite to your own VPS https://deploywithox.com/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` 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/config#app). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The static site guide](https://deploywithox.com/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/vue) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/react-vite # Deploy a Create React App build to your own VPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Create React App repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/config#app). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The static site guide](https://deploywithox.com/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/angular) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/create-react-app # Deploy a Vue app to a VPS: build, HTTPS, no Docker https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Vue CLI repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The static site guide](https://deploywithox.com/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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` 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://deploywithox.com/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://deploywithox.com/docs/guides/angular-ssr) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/vue # Deploy an Angular app to a VPS with Caddy and HTTPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Angular repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [Angular SSR](https://deploywithox.com/docs/guides/angular-ssr): Deploy an Angular app with server-side rendering to your own VPS. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/nextjs-static-export) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/angular # Deploy Angular SSR to your own VPS with Node.js 24 https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Angular SSR repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/docs/troubleshooting#build-failed). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [Angular](https://deploywithox.com/docs/guides/angular): Deploy an Angular single-page app to your own VPS. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/gatsby) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/angular-ssr # Deploy a Next.js static export to your own VPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Next.js static export repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Next.js guide](https://deploywithox.com/docs/guides/nextjs): `next start`, PostgreSQL and Prisma. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/docusaurus) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/nextjs-static-export # Deploy a Gatsby site to your own VPS with HTTPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Gatsby repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/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://deploywithox.com/docs/config#static). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The static site guide](https://deploywithox.com/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/astro) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/gatsby # Deploy a Docusaurus docs site to your own VPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Docusaurus repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The static site guide](https://deploywithox.com/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/astro-ssr) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/docusaurus # Deploy an Astro static site to your own VPS server https://deploywithox.com/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 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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Astro repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/config#app). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The static site guide](https://deploywithox.com/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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 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://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/sveltekit) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/astro # Deploy Astro SSR with the Node adapter to a VPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Astro SSR repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/docs/troubleshooting#build-failed). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/sveltekit-static) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/astro-ssr # Deploy SvelteKit with adapter-node to your own VPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal SvelteKit repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/docs/troubleshooting#build-failed). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/nuxt) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/sveltekit # Deploy a static SvelteKit site to your own VPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal SvelteKit static repo in ox's tests. A deploy on a real server is not recorded yet. 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` | | `build.install` | `npm install` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `24` | `default` | `ox check` also prints these hints for it: - `svelte.config.ts sets fallback: '200.html', and ox's spa mode serves index.html for unknown paths; set fallback: 'index.html' and [static] spa = true for a single-page app` ## 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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/config#static). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The static site guide](https://deploywithox.com/docs/guides/static-site): Vite, Astro and plain HTML with a full ox.toml. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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: 'index.html'` in the adapter's options and `[static] spa = true` in an ox.toml, as the hint above says. Caddy then sends unknown paths to `index.html`. ## 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://deploywithox.com/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://deploywithox.com/docs/guides/react-router) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/sveltekit-static # Deploy Nuxt to a VPS: SSR on Node with no Docker https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Nuxt repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/docs/troubleshooting#build-failed). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/react-router-spa) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/nuxt # Deploy React Router 7 or Remix to your own VPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal React Router repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/docs/troubleshooting#build-failed). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/solidstart) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/react-router # Deploy a React Router SPA (ssr: false) to a VPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal React Router SPA repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [React Router](https://deploywithox.com/docs/guides/react-router): Deploy a React Router 7 app (the Remix successor) with server rendering to your own VPS. - [The `[static]` keys](https://deploywithox.com/docs/config#static): the folder Caddy serves, single-page mode and an API beside it. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Roll back](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/qwik) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/react-router-spa # Deploy SolidStart to your own VPS with Node.js https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal SolidStart repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `detected:package.json` | | `build.commands[0]` | `npm run build` | `detected:package.json` | | `tools.node` | `22` | `detected:package.json` | ## 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` and `PUBLIC_HOST`. ## 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://deploywithox.com/docs/troubleshooting#build-failed). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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 22` (read from `package.json`). 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://deploywithox.com/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://deploywithox.com/docs/guides/nestjs) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/solidstart # Deploy Qwik City with the Express adapter to a VPS https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Qwik City repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/docs/troubleshooting#build-failed). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/fastify) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/qwik # Deploy NestJS to a VPS with PostgreSQL, no Docker https://deploywithox.com/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` 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal NestJS repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` 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://deploywithox.com/docs/troubleshooting#build-failed). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://deploywithox.com/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/hono-bun) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/nestjs # Deploy a Fastify API with PostgreSQL to a VPS https://deploywithox.com/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`, 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Fastify repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `DATABASE_URL`. ## If the Fastify deploy fails - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/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://deploywithox.com/docs/guides/node). - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://deploywithox.com/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/bullmq-worker) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/fastify # Deploy a Hono API on Bun to your own VPS, no Docker https://deploywithox.com/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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Hono on Bun repo in ox's tests. A deploy on a real server is not recorded yet. 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` and `PUBLIC_HOST`. ## If the Hono on Bun deploy fails - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). - `the app exited while starting (failed)`: The start command ran and stopped. The log shows the app's last lines, most often a missing variable or a wrong path to the built file. [More](https://deploywithox.com/docs/troubleshooting#health-check-failed). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/deno-fresh) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/hono-bun # Deploy a BullMQ worker and Redis queue to a VPS https://deploywithox.com/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`, 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal BullMQ worker repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `REDIS_URL`. ## If the BullMQ worker deploy fails - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `the app exited while starting (failed)`: The start command ran and stopped. The log shows the app's last lines, most often a missing variable or a wrong path to the built file. [More](https://deploywithox.com/docs/troubleshooting#health-check-failed). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The `[workers]` keys](https://deploywithox.com/docs/config#workers): processes that run next to the app. - [The Node.js guide](https://deploywithox.com/docs/guides/node): start scripts, migrations, PostgreSQL and Redis. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://deploywithox.com/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/flask) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/bullmq-worker # Deploy Deno Fresh to your own VPS with deno serve https://deploywithox.com/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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Deno Fresh repo in ox's tests. A deploy on a real server is not recorded yet. 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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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://deploywithox.com/docs/troubleshooting#build-failed). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The `[tools]` keys](https://deploywithox.com/docs/config#tools): pin a runtime version. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/symfony) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/deno-fresh # Deploy Flask to a VPS with gunicorn and PostgreSQL https://deploywithox.com/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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Flask repo in ox's tests. A deploy on a real server is not recorded yet. 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.uv` | `0.11` | `default` | | `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` 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://deploywithox.com/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://deploywithox.com/docs/config#app). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Django guide](https://deploywithox.com/docs/guides/django): gunicorn, migrations and static files. - [The FastAPI guide](https://deploywithox.com/docs/guides/fastapi): uvicorn, uv and Alembic. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://deploywithox.com/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/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: `uv 0.11` (ox's default). 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://deploywithox.com/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://deploywithox.com/docs/guides/php) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/flask # Deploy Symfony to a VPS with FrankenPHP and Doctrine https://deploywithox.com/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` and `composer.json`, 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Symfony repo in ox's tests. A deploy on a real server is not recorded yet. ox reads `bin/console` and `composer.json`, 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.13.0` | `default` | | `packages` | `composer` | `detected:composer.json` | | `packages` | `php-cli` | `detected:composer.json` | | `packages` | `php-xml` | `detected:composer.json` | | `packages` | `unzip` | `detected:composer.json` | `ox check` also prints these hints for it: - `Symfony runs with var/ read-only: the build warms its cache, so log to php://stderr (Monolog's prod default) and keep sessions out of var/` ## 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 (Symfony 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` and `PUBLIC_HOST`. ## If the Symfony deploy fails - `set these before deploying: APP_SECRET (Symfony 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://deploywithox.com/docs/operate/variables#set). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `no index.php at the root or in public/; set [app] start = "frankenphp php-server --root --listen 127.0.0.1:$PORT"`: Put `index.php` at the root or in `public/`, or set `[app] start` with your document root. [More](https://deploywithox.com/docs/config#app). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Laravel guide](https://deploywithox.com/docs/guides/laravel): Composer, a queue worker and the scheduler. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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.13.0` (ox's default) and the apt packages `composer`, `php-cli`, `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. `var/` is read-only in a release, so keep logs and sessions out of it. ## 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://deploywithox.com/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://deploywithox.com/docs/guides/phoenix) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/symfony # Deploy a plain PHP site to a VPS with FrankenPHP https://deploywithox.com/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`, 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Plain PHP repo in ox's tests. A deploy on a real server is not recorded yet. ox reads `index.php`, 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` | ## 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` and `PUBLIC_HOST`. ## If the Plain PHP deploy fails - `no index.php at the root or in public/; set [app] start = "frankenphp php-server --root --listen 127.0.0.1:$PORT"`: Put `index.php` at the root or in `public/`, or set `[app] start` with your document root. [More](https://deploywithox.com/docs/config#app). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Laravel guide](https://deploywithox.com/docs/guides/laravel): Composer, a queue worker and the scheduler. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/spring-boot) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/php # Deploy Phoenix to a VPS: mix release and Postgres https://deploywithox.com/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` and `lib/hello/release.ex`, 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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Phoenix repo in ox's tests. A deploy on a real server is not recorded yet. ox reads `mix.exs` and `lib/hello/release.ex`, 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` | | `packages` | `elixir` | `detected:mix.exs` | | `packages` | `erlang-dev` | `detected:mix.exs` | | `packages` | `erlang-nox` | `detected:mix.exs` | | `services.postgres` | `postgres 18 (shared)` | `detected:mix.exs` | `ox check` also prints these hints for it: - `not installed: erlang 27.3.4 (from .tool-versions): ox installs Erlang/OTP 27 and Elixir 1.18.3 from Ubuntu's archive` - `not installed: elixir 1.18.4-otp-27 (from .tool-versions): ox installs Erlang/OTP 27 and Elixir 1.18.3 from Ubuntu's archive` ## 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` 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://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/config#build). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [Set variables and secrets](https://deploywithox.com/docs/operate/variables#set) on the dashboard or with `ox vars`. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://deploywithox.com/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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: the apt packages `elixir`, `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? The ones in Ubuntu's archive, Erlang/OTP 27 and Elixir 1.18.3, as the hints above say. A different version in `.tool-versions` is reported, not installed. ## 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://deploywithox.com/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://deploywithox.com/docs/guides/spring-boot-gradle) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/phoenix # Deploy Spring Boot to a VPS with Maven and Postgres https://deploywithox.com/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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Spring Boot (Maven) repo in ox's tests. A deploy on a real server is not recorded yet. 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` | | `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` 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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/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://deploywithox.com/docs/config#build). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [Set variables and secrets](https://deploywithox.com/docs/operate/variables#set) on the dashboard or with `ox vars`. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://deploywithox.com/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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`). 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://deploywithox.com/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://deploywithox.com/docs/guides/quarkus) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/spring-boot # Deploy Spring Boot built with Gradle to your own VPS https://deploywithox.com/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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Spring Boot (Gradle) repo in ox's tests. A deploy on a real server is not recorded yet. 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` | `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` and `PUBLIC_HOST`. ## 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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/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://deploywithox.com/docs/config#build). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [Spring Boot (Maven)](https://deploywithox.com/docs/guides/spring-boot): Deploy a Spring Boot app built with Maven to your own VPS. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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`). 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://deploywithox.com/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://deploywithox.com/docs/guides/aspnet-core) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/spring-boot-gradle # Deploy Quarkus to a VPS in JVM mode with Postgres https://deploywithox.com/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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Quarkus repo in ox's tests. A deploy on a real server is not recorded yet. 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` | | `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` 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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/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://deploywithox.com/docs/config#build). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [Set variables and secrets](https://deploywithox.com/docs/operate/variables#set) on the dashboard or with `ox vars`. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://deploywithox.com/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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`). 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://deploywithox.com/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://deploywithox.com/docs/guides/rust) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/quarkus # Deploy ASP.NET Core to a Linux VPS with PostgreSQL https://deploywithox.com/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 `dotnet out/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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal ASP.NET Core repo in ox's tests. A deploy on a real server is not recorded yet. ox reads `src/Api/Api.csproj` and `src/Api/Program.cs`, builds the plan on the card, and starts `dotnet out/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` | `ASPNETCORE_URLS=http://127.0.0.1:$PORT dotnet out/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` 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://deploywithox.com/docs/config#build). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). - `web of shop on main keeps crashing: systemd restarted it 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://deploywithox.com/docs/troubleshooting#crash-loop). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [Set variables and secrets](https://deploywithox.com/docs/operate/variables#set) on the dashboard or with `ox vars`. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://deploywithox.com/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/docs/guides/react-vite) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/aspnet-core # Deploy a Rust web app to a VPS with cargo, no Docker https://deploywithox.com/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. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Rust repo in ox's tests. A deploy on a real server is not recorded yet. 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` | ## 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` and `PUBLIC_HOST`. ## 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://deploywithox.com/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. Raise the project's memory, add swap, or use a bigger server. [More](https://deploywithox.com/docs/troubleshooting#out-of-memory). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: 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://deploywithox.com/docs/troubleshooting#health-check-failed). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The `[tools]` keys](https://deploywithox.com/docs/config#tools): pin a runtime version. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/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). 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://deploywithox.com/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://deploywithox.com/docs/guides/create-react-app) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/rust # ox.toml reference: every key, default and example https://deploywithox.com/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://deploywithox.com/docs/operate/variables)). For a complete file for one framework, start from a guide: [Django](https://deploywithox.com/docs/guides/django), [Next.js](https://deploywithox.com/docs/guides/nextjs), [FastAPI](https://deploywithox.com/docs/guides/fastapi), [Node](https://deploywithox.com/docs/guides/node), [Laravel](https://deploywithox.com/docs/guides/laravel), [Rails](https://deploywithox.com/docs/guides/rails) or a [static site](https://deploywithox.com/docs/guides/static-site). When a deploy fails on a key, [Troubleshooting](https://deploywithox.com/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 (workers only); default: the detected app start = "serve" # default: detected health = "/healthz" # HTTP 2xx/3xx on this path; default: TCP connect port = 9034 # pin; default: allocated and recorded memory = "512M" # this process's cap sandbox = "relaxed" # the only value; default is strict [static] # files served by Caddy dir = "dist" # relative to the repo root spa = true # unknown paths serve index.html 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" # 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, 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. - `packages`: Ubuntu system libraries (`ffmpeg`, `libmagic1`). Toolchains (`nodejs`, `npm`, `python3-pip`, `golang*`, `rustc`, `cargo`) are refused; declare them in `[tools]`. - Git submodules are refused: releases are built from `git archive` of the commit. ## [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. - `enabled`: `false` says the project has no web process, so ox adds none (a bot or a queue consumer). 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. - `health`: a path that must return HTTP 2xx/3xx. Default: a TCP connect on the port. - `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. - `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`: unknown paths serve `index.html`. - `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. ## [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. - `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` hints each detected step the list leaves out (Django's `collectstatic`), so list it again when the release needs it. - `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). ## [workers] Every worker runs as its own process next to the app. With `port = true` a worker gets a port, the app reaches it at `$BOT_URL`, and its health check calls that port. Background processes, each its own systemd unit. A worker is a command string (short form) or a table: `{ run, memory, sandbox, port, health }`. - Names match `^[a-z][a-z0-9-]{0,27}$`; `app` is reserved. - `port = true` allocates a port, exposed as that worker's `$PORT` and as a `_URL` variable (name upper-cased, `-` becomes `_`). This is how processes of one project call each other. - `health` needs `port = true`: the health check calls the worker's `$PORT`. ## [cron] Each entry becomes a timer on your server that runs its command on schedule, in UTC. - `name = { schedule = "...", run = "..." }`, rendered as a systemd timer. - `schedule` is a strict 5-field cron expression or `@hourly`/`@daily`/`@weekly`/`@monthly`, in UTC. ## [services] Declare a service and ox installs it on your server, then hands its connection string to your app on every deploy. Five types are built in, and an entry with `run` is a service of your own. Declaring one is all you do: ox installs it, creates the database or instance, and writes the connection keys on every deploy. - Each entry is ` = { ... }`, named like a worker. Its `type` defaults to the name, so `postgres = {}` is a postgres and `analytics = { type = "postgres" }` is a second postgres database for the same project. An entry not named like its type gets its keys prefixed: `ANALYTICS_DATABASE_URL`. - `postgres`: shared by default, one database and role per project. Version 18. Provides `DATABASE_URL`. Extensions, like `vector`, come from the service: `postgres = { extensions = ["vector"] }`. - `redis`: private by default (shared allowed, one database index per project). Version 8. Provides `REDIS_URL`. - `neo4j`: private by default (shared allowed). Version 5. Provides `NEO4J_URI`, `NEO4J_USER`, `NEO4J_PASSWORD`. - `qdrant`: private only. Version 1. Provides `QDRANT_URL`. - `mysql`: private only, served by MariaDB 11.8 (MySQL-compatible). Provides `MYSQL_URL`, with the user `app` and the database `app`. - `only_for_this_project = true` runs a private instance with its own unit, its own data under the project's data dir, its own password, and its own port (allocated, or the `port` pin); `false` shares the host's instance. `port` on a shared service fails validation. - `version` pins are enforced: a version ox cannot install on this OS fails with the available list. - Every entry with data is backed up daily (a shared redis or neo4j is not) after 03:00 UTC (7 kept on the server, or 2 once the S3 bucket set in Settings has them; 30 kept in the bucket), and on demand with Back up now on the Services tab. Postgres is dumped online; a private instance stops for the moment its data dir is archived. Restore takes a kept backup, an object in the bucket, or an uploaded file. - Removing a service never drops data by itself. The Services tab shows it as unused, with its size and a Delete data button. - To use your own external database, remove the service from `ox.toml` and set the URL in Variables. ### Custom services An entry with `run` is a service your repo defines itself, always private: A custom service, defined by the repo: ```toml [services.search] run = "meilisearch --db-path $SERVICE_DATA_DIR --http-addr 127.0.0.1:$PORT --master-key $SERVICE_PASSWORD" tool = "github:meilisearch/meilisearch@1.12.0" # a mise tool; or packages = [...] (apt) health = "/health" # HTTP 2xx/3xx; default: TCP connect memory = "256M" # floor and cap; default 128M port = 9300 # pin; default: allocated provides = { MEILI_URL = "http://${host}:${port}", MEILI_MASTER_KEY = "${password}" } backup = false # default true: its data dir, daily ``` - `run` is required. It runs with `bash -euo pipefail -c` in its data dir, as the project user, in the strict sandbox, with `$PORT`, `$SERVICE_DATA_DIR`, `$SERVICE_PASSWORD`, and the tools on `PATH`. It gets none of the app's variables. - `provides` values are templates over `${host}`, `${port}`, and `${password}`; keys are upper snake case and may not collide with another provided key. Without it, the entry provides `_URL = http://127.0.0.1:`. - It starts, and passes health, before the workers and the app on every deploy, and restarts only when `run`, `tool`, `packages`, `memory`, or its port changes. - `type`, `version`, `extensions`, and `only_for_this_project = false` are refused on it. - A Meilisearch, Typesense, Elasticsearch or OpenSearch, Redis, Valkey, ClickHouse, Chroma, Memcached, NATS, Prometheus, or VictoriaMetrics gets an Explore page, known by its `tool`, `packages`, or `run`; `explore = "meilisearch"` names it when none of those does. Explore sends `$SERVICE_PASSWORD` only when `run` or `provides` use it, and never to Prometheus or VictoriaMetrics. [The 50 most common services](https://deploywithox.com/docs/operate/services#list) lists what each one gets. ## [tools], [storage], [limits], [models] Pinned tools are on the PATH of your build and every process. The exact versions are recorded on the first deploy, so they never drift. - `[tools]`: a mise tool name = version (`node = "24"`, `uv = "0.11"`, or a download-only backend like `aqua:`, `github:`, `ubi:`). Backends that run a package's own code while installing (`npm:`, `cargo:`, `pipx:`, `gem:`, `go:`, `asdf:`, `vfox:`) are refused, because every project's tools share one tree; install those in your build instead. Default: read from `mise.toml`, `.nvmrc`, lockfiles, `go.mod`, and friends, or ox's default table. The first deploy records the resolved versions; a new ox release never silently changes them. - `[storage] keep`: folders in the repo the app writes to, like `["media", "backend/logs"]`. ox links each to the project data dir (`OX_DATA_DIR`), so it is writable and kept across releases. - `[limits]`: `memory` caps the project's whole slice (processes and builds); `cpu` is a number of cores. - `[models] huggingface`: model repos pulled into the project's cache on every deploy (`"org/repo"` or `"org/repo@revision"`). The environment gains `HF_HOME`. Gated models need the Hugging Face token from [Settings](https://deploywithox.com/app/settings). ## Detection Detection reads your repo's files to fill the plan. A value you declare in ox.toml always wins over a detected one. Detection runs on the checked-out commit and only fills keys the manifest leaves empty. It never overrides a declared value. - Version files (`mise.toml`, `.tool-versions`, `.nvmrc`, `.python-version`, `package.json` engines, `go.mod`, `rust-toolchain.toml`) fill `[tools]`, and a one-tool file one folder down (`consumer/.python-version`) pins a tool the root does not; a detected language with no version file gets ox's default table. - Lockfiles fill the package manager, install, build, and start commands: `bun.lock`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`, `uv.lock`, `poetry.lock`, `requirements.txt`. - `manage.py` fills Django migrate, collectstatic, and a gunicorn/uvicorn start. `go.mod` fills `go build` and the start binary. `Cargo.toml` fills `cargo build --release`. A Vite build with no start script fills `[static] dist` with `spa = true`. - Only when the repo has **no** `ox.toml`, dependencies also fill `[services]`: python or node postgres drivers imply postgres; `redis`, `ioredis`, `bullmq`, `celery[redis]` imply redis; Go's direct `go.mod` requirements (pgx, lib/pq, go-sql-driver/mysql, go-redis), Rust's `Cargo.toml` dependencies (sqlx or diesel by their `postgres` or `mysql` feature, tokio-postgres, the redis crate) and Symfony's Doctrine config imply theirs. With an `ox.toml`, `[services]` is exactly what is declared; an implied but undeclared service shows as a hint on the review screen, and the key it would provide is listed to set on the dashboard. - A repo with nothing to run and no detectable start command is asked for one on the review screen before the first deploy. ## Variables Values live in the dashboard, not in ox.toml. Your processes get them as environment variables, and run logs hide them. Every variable is a key, a value, and a source, managed in the dashboard's Variables tab or with `ox vars`. None of them live in `ox.toml`. - **Provided by ox**, on every deploy: `PORT` (per process), `HOST`, `PUBLIC_URL`, `PUBLIC_HOST`, `_URL` for each `port = true` worker, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, the service keys above, and `HF_HOME` with `[models]`. Locked in the UI. Saving your own value under a provided key is refused; remove the service instead to use your own. - **During the build** (`install`, `commands` and `migrate`) every variable is already set except `PORT`: your variables, the service keys, `PUBLIC_URL` and `PUBLIC_HOST` (the address the release will serve), and `OX_RELEASE`, the commit being built. `PORT` exists only in a running process. - **Yours**: API keys and feature config, typed by you. Values are hidden until you reveal one, and values 6 characters or longer are redacted in run logs. **Save & deploy** deploys the current production commit, because build-time variables can change the build; **Save, deploy later** only saves. On a staging copy or preview, a key still equal to production's reads **inherited**, and Sync from production takes production's values. - **Needed**: key names in the repo's `.env.example` (or `.env.sample` / `.env.template`), at the root or one directory deep. The deploy is refused with the list, and the project page shows it with fields, until each one is set. A provided key or a key of yours satisfies one, even empty. - A value may reference another: `${NAME}` for any variable, or `${service.field}` with field `host`, `port`, `user`, `password`, `database`, or `url`, like `CELERY_BROKER_URL=${REDIS_URL}`. `$${` is a literal `${`; a bare `$NAME` is never expanded. Unknown references and cycles are refused at save. `PORT` and `OX_RELEASE` cannot be referenced. - ox never injects framework defaults (`DEBUG`, `SECRET_KEY`, `ALLOWED_HOSTS`, `CORS_*`, `NODE_ENV`), never reads the repo's `.env`, and never edits a value of yours. Set framework variables as yours when the app needs them. - Every save is kept as a version on your server, never on ox's: History lists the last 20 and restores one through the same diff, and a rollback can also restore the variables its release ran with. ## Staging & Promote Promote deploys the exact commit staging runs to production. Production keeps its own variables and data, and the old release serves until the new one is healthy. A staging copy is the same repository on the same server with its own address, variables, and databases. **Promote to production** deploys production at the exact commit staging runs now, as a normal production deploy. 1. **Review.** ox asks your server which commit staging and production run, and lists the commits production does not have yet (newest first, up to 100; it says so when more may differ). 2. **Confirm.** Your confirmation names the commit you saw. If staging moved meanwhile, ox refuses and asks you to look again. 3. **Deploy.** Your server fetches that commit from your repository and builds it, and runs its migrations, with production's variables against production's databases. 4. **Switch.** The new release starts beside the live one and takes traffic only after its health check passes; a failed build, migration, or health check leaves the live release serving. 5. **Auto-deploy.** When the commit is not the newest on production's branch, production's auto-deploy pauses so the next push does not replace it; the review says so, and Settings resumes it. Never moved: variables (production keeps its own; set a key the new code needs on production first), data (the migrations run against production's database), domains, password protection, and previews. To undo, **Roll back to this** on production's previous release; migrations are not reverted. From a terminal or an agent: `ox promote --yes --wait` prints the commits, deploys, and exits with the deploy's result. ## Examples **A built SPA with no backend** (Caddy serves `dist/`, the repo's own build produces it): A built SPA with no backend: ```toml domains = ["www.example.com"] [static] dir = "dist" spa = true ``` **Next.js SSR** (start, build, and the node version come from `package.json` and the lockfile): Next.js SSR: ```toml domains = ["app.example.com"] [app] health = "/" ``` **Python with postgres, redis, a worker, cron, a migration, and persistent uploads**: Python with services, a worker, and cron: ```toml [app] start = "uv run python app.py" health = "/health" [build] migrate = "uv run python migrate.py" [workers] worker = "uv run python worker.py" [cron] tick = { schedule = "* * * * *", run = "uv run python cron.py" } [services] postgres = {} redis = {} [storage] keep = ["uploads"] ``` **Go API with postgres and redis, a migration command, and a React SPA** (Rust is the same shape: `cargo build --release --locked` and start `./target/release/server`): Go API with a React SPA: ```toml domains = ["api.example.com"] [app] start = "./server -port $PORT" health = "/health" [static] dir = "dist" spa = true api = ["/api", "/health"] [build] commands = ["go build -o server ./cmd/server", "npm run build"] migrate = "go run ./cmd/migrate" [services] postgres = {} redis = {} [tools] node = "24" ``` **A bot with no public URL**: a private neo4j, a private qdrant, and Hugging Face models. The worker binds its own port so its health check can call it: A bot with no public URL: ```toml [build] install = "uv venv --python 3.12 .venv && uv pip install --python .venv -r requirements.txt" [workers] bot = { run = "exec .venv/bin/python bot.py", port = true, health = "/health", memory = "1G" } [services] neo4j = {} qdrant = {} [models] huggingface = ["KanariKanaru/nsfw-image-detection-384-onnx"] [tools] uv = "0.11" [limits] memory = "1500M" ``` ## Common mistakes - Pinning `[app] port` when the app reads `$PORT`. Leave it out; a pin costs a short restart per deploy. - Setting a provided key (`DATABASE_URL`, `REDIS_URL`, `PORT`) as your own. Refused at save. Use an external database by removing the service and setting the URL as yours. - Git submodules. Refused; releases come from `git archive`. Vendor the code. - Expecting framework env vars (`DEBUG`, `SECRET_KEY`, `ALLOWED_HOSTS`, `CORS_*`, `NODE_ENV`). ox never injects them. - Toolchains in `packages`. Refused; declare `[tools]` instead. - Legacy keys from the old engine (`runtime`, `package_manager`, `[deploy]`, `[frontend]`, `writable_paths`, and the old array-of-table blocks). Each fails with the ox1 key that replaces it. - `{port}` placeholders. Commands read `$PORT` from the shell. - A worker with `health` but no `port = true`. Fails: the health check calls the worker's `$PORT`. - Cron in local time. Schedules run UTC. - `static.api` without `[app]`, or `domains` with neither `[app]` nor `[static]`. Both fail validation. - Absolute host paths under `/srv`, `/var`, `/etc`, `/opt`, or `/home`. Refused; use paths relative to the repo root. ## Agent instructions The **Copy skill for AI agent** button at the top copies the bundled ox.toml skill for your coding agent. It is the full authoring contract: schema, detection, services, variables, examples, and mistakes. The same text is at [/ox-skill.md](https://deploywithox.com/ox-skill.md) as a plain file. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/config # ox CLI reference: deploy, logs and vars from a shell https://deploywithox.com/docs/cli The ox command line: install it, sign in, and every command for projects, deploys, logs, variables, backups and servers, with --json and its exit codes. The `ox` command does everything the dashboard does, from your terminal: deploy, read logs, roll back, set variables and restore backups. Install it on macOS or Linux, then sign in once with `ox login`. New to ox? The [quickstart](https://deploywithox.com/docs/quickstart) walks through a first deploy. Run `ox help` for the list below, and `ox -h` for one command's flags. What `ox check` prints for a Fastify app with PostgreSQL and no ox.toml, copied from ox's own tests: each part of the plan, where it came from, the keys ox provides, and `Ready to deploy.` It runs offline, before you sign in. Install and sign in: ```sh curl -fsSL https://deploywithox.com/install.sh | sh ox login ``` ## Flags most commands take - `--json`: every ox Cloud command takes it and prints one JSON document with stable field names. On a failure it writes `{"error": "...", "code": "..."}` to standard error. - `--env E`: a command about one project acts on production by default. Add `--env staging` or `--env ` for staging or a preview. - `--server S`: when two of your servers have a project with the same name, say which one. - `--wait`: on a command that starts a run (deploy, rollback, restore and others), stream the run and exit with its result. JSON for scripts: ```sh ox status shop --json ox deploy shop --env staging --wait ``` ### Exit codes | Code | Meaning | | --- | --- | | `0` | It worked. | | `1` | ox refused, the project, run or server was not found, or the run you waited on failed. | | `2` | A flag or an argument is wrong. The message shows the right usage. | | `3` | You are not signed in, or your token expired or was revoked. Run `ox login`. | | `4` | ox Cloud could not be reached. | ## Sign in from the terminal **`ox login`** Sign in to ox Cloud with a code you approve in the browser. It prints a link and a code, then waits until you approve it. Example: `ox login` **`ox logout`** Revoke this machine's token and forget it. Example: `ox logout` ## Project commands: create, list and manage **`ox repos`** The GitHub repositories a new project can be made from. Example: `ox repos` **`ox new [--server S]`** Make a project from a repository on a server. `--server` is needed only when you have more than one. Example: `ox new acme/shop --server web-1` **`ox review [--start CMD] [--generate K] [--skip K] [--from-file F] [--wait]`** What a first deploy still needs; answer it and deploy. `--generate K` makes a random value for a missing variable, `--skip K` saves it empty, and `--from-file F` reads values from a .env file. Example: `ox review shop --generate SECRET_KEY --wait` **`ox projects`** List your projects with their server and status. Example: `ox projects --json` **`ox status `** Live commit, URL, processes, and the last run. Example: `ox status shop` **`ox rename `** Rename a project before its first deploy. Example: `ox rename shop store` **`ox delete --confirm [--wait]`** Delete a project; its files go to the trash for 7 days. Type the name again after `--confirm`. Example: `ox delete shop --confirm shop --wait` **`ox stop [--wait]`** Stop every process of a project. Example: `ox stop shop --wait` **`ox start [--wait]`** Start a stopped project. Example: `ox start shop --wait` **`ox autodeploy [on | off]`** Deploy on every push: show or turn on or off. Example: `ox autodeploy shop off` **`ox domains [add D | remove D | primary D] [--wait]`** A project's domains and their DNS; add or remove one. See [Domains](https://deploywithox.com/docs/operate/domains). Example: `ox domains shop add shop.example.com --wait` **`ox trash [restore]`** The deleted project's variables a new project can take back. Example: `ox trash shop restore` ## Staging and previews These commands are listed under Projects in `ox help`. [Previews and staging](https://deploywithox.com/docs/operate/previews) explains how they fit together. **`ox staging [--domain D] [--branch B] [--wait]`** Make the project's staging copy. `--branch B` deploys a branch instead of production's live commit. Example: `ox staging shop --branch develop --wait` **`ox previews [on | off | start BRANCH | remove BRANCH | --branches all|PATTERNS | --at-once N]`** Branch previews: show, turn on or off, preview a branch now or remove one, choose which branches, or how many run at once. Example: `ox previews shop --branches feature/*` **`ox environments `** A project's production, staging, and previews. Example: `ox environments shop` **`ox promote [--yes] [--wait]`** Deploy production at the commit staging runs. It shows the commits and asks first; `--yes` skips the question, and it is required when no terminal can answer. Example: `ox promote shop --yes --wait` **`ox protect --env E [on | off | default] | protect --account [on | off] [--new-password]`** Password-protect staging and previews, for one environment or as the account's default. `--new-password` makes a new password. Example: `ox protect shop --env staging on` **`ox keep --env [on | off]`** Keep a preview from the 7-day cleanup. Example: `ox keep shop --env feature/cart on` ## Deploy, roll back and read runs **`ox deploy [--ref R] [--wait]`** Deploy a branch or commit; --wait streams the run. Without `--ref` it deploys the project's branch. Example: `ox deploy shop --ref main --wait` **`ox rollback [--release ID] [--restore-variables] [--wait]`** Go back to a kept release, by default the newest one that is not live. `--restore-variables` also brings back the variables that release ran with. See [Roll back](https://deploywithox.com/docs/operate/rollback). Example: `ox rollback shop --wait` **`ox runs `** The project's runs, newest first. Example: `ox runs shop` **`ox crons [run NAME]`** Cron jobs: schedules, next and last runs; run one now. Example: `ox crons shop run clearsessions` **`ox celery `** A Celery worker's app: workers online, tasks, queues. Example: `ox celery shop worker` **`ox logs [--run ID | --follow] [--since T] [--until T] [--grep TEXT | --regex RE] [--query Q] [--process P] [--severity S] [-n N] [-o FILE]`** The processes' logs, their history and search, or one run's log. `--query` takes the log explorer's query, like `'level:error -healthcheck'`. `-n` is 100 lines by default, and `-o FILE` saves everything that matches, up to 100 MB. See [Logs](https://deploywithox.com/docs/operate/logs). Example: `ox logs shop --since 2h --severity error` **`ox metrics [] [--server S] [--range 1h|24h|7d|30d | --at TIME]`** CPU and memory of a project, or of a server, over the last hour to month, with deploys and alerts. The range is 24h by default. Example: `ox metrics shop --range 7d` ## Variables and services commands **`ox vars [--reveal] | vars set [KEY…] [--generate KEY] | vars unset|import|pull|history|restore|sync … [--no-deploy] [--wait]`** Variables: the table, set, unset, import, pull, history, restore, sync. The verb comes before the project. `ox vars set` takes only key names and asks for each value, so no value lands in your shell history; `--generate KEY` has your server make a random value in the key's well-known format (Laravel's `APP_KEY`, `SECRET_KEY_BASE`, and others) and refuses a key that already has one. A save redeploys unless you add `--no-deploy`. See [Variables](https://deploywithox.com/docs/operate/variables). Example: `ox vars set shop STRIPE_KEY` **`ox services [--reveal]`** Service entries: type, mode, version, state, keys, backups. Example: `ox services shop` ## Data commands: backups, restore, explore **`ox backups [now | download [--output F]] [--wait]`** A project's backups; back up now; download one. See [Backups](https://deploywithox.com/docs/operate/backups). Example: `ox backups shop now postgres --wait` **`ox restore (--from SOURCE | --file F) --confirm [--wait]`** Replace a service entry's data from a backup or a file. `--from` takes the SOURCE column of `ox backups`; `--file` uploads a dump of up to 95 MB. Example: `ox restore shop postgres --from backup:daily-20261007T030000Z.dump --confirm shop --wait` **`ox data [drop --confirm ] [--wait]`** Data of services no longer in ox.toml; drop it for good. Example: `ox data shop drop redis --confirm redis` **`ox explore [tables | keys [PATTERN] | table NAME | key KEY | command BUTTON | query [--from-file F] | activity | allow-changes [off] | change ACTION NAME [ARG] --confirm ] [--cursor C]`** Look into a postgres, redis, neo4j, or qdrant service: browse, query, activity (read-only). `--cursor` continues a long listing. Example: `ox explore shop postgres tables` ## Server commands **`ox storage [] [--server S]`** Where a server's disk went, or a project's share of it. Example: `ox storage --server web-1` **`ox servers [add | check | forget --confirm | cancel | update ]`** Your servers; add one, check one now, forget one, cancel an unused add, update its agent. `add` prints the command to run on the new server. Example: `ox servers add` ## Account **`ox tokens [create [--name N] | revoke ]`** Personal tokens; make one, revoke one. `--name` says what the token is for. Example: `ox tokens create --name ci` **`ox account [export [-o FILE] | delete --confirm ]`** Download everything ox holds about you, or delete your account. Example: `ox account export -o ox-account.json` **`ox settings [s3 | huggingface | preview-domain | theme | transparency] ...`** S3 backup target, Hugging Face token, preview domain, and the console's look. Secrets come from a file or standard input with `--from-file`, never as an argument. Example: `ox settings preview-domain preview.example.com` ## Notifications and webhooks These commands are listed under Account in `ox help`. **`ox notifications [--project P] [on EVENT... | off EVENT... [--channel email|ENDPOINT] | email on|off | use-account | test]`** Which events email you, for the account or one project; send a test email. `--channel` picks email or a webhook endpoint. Example: `ox notifications on deploy.succeeded` **`ox webhooks [list | add (- | --from-file F) | edit ID | remove ID --confirm ID | rotate ID | test ID [--event TYPE] | deliveries ID [--failed] | redeliver ID MSG | redeliver-failed ID [--since T] | enable ID | disable ID | event-types]`** Endpoints that get your events as signed webhooks: add, filter, test, rotate, redeliver. The address is read from standard input or a file, never an argument, because it may carry a secret. Example: `echo https://example.com/hook | ox webhooks add - --events deploy.failed` ## On your server Run these on the server itself, as root. They do not take `--json` unless the line says so. **`ox install --agent URL --token T`** Make this host an ox Cloud server (the Add server command runs it). Example: `curl -fsSL https://deploywithox.com/install/ | sudo bash` **`ox install --check`** Only check that this host can take ox: ports 80 and 443 free, 1 GB of memory, 10 GB of disk. Example: `sudo ox install --check` **`ox agent`** Run the ox agent (started by install --agent). You do not run it by hand. Example: `sudo systemctl status ox-agent` **`ox uninstall`** Remove everything ox added to this host. It asks first; `--force` or `-y` skips the question. Example: `sudo ox uninstall` **`ox doctor`** Check the host and name each problem's fix. `--json` prints the findings as JSON, and `--inventory` prints everything ox could own on this host. Example: `sudo ox doctor --json` ## Anywhere **`ox check [dir]`** Validate a repo's ox.toml and print its resolved plan (offline). `--json` prints the result as JSON. Example: `ox check --json .` **`ox version`** Print the ox version. Example: `ox version` **`ox help`** Show the list of commands. Example: `ox help` Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/cli # Operate your app: logs, rollbacks, backups, domains https://deploywithox.com/docs/operate Run your app after its first deploy: read logs, roll back a release, back up and restore data, set variables, add domains with HTTPS, staging and previews. After the first deploy, you run your app from the dashboard or the [ox CLI](https://deploywithox.com/docs/cli): read its logs, roll back a bad release, restore a backup, change a variable, add a domain, or try a change on staging first. Each page below is one of those jobs. The jobs of running an app after its first deploy, one page each, with the command that starts each one from a terminal. The dashboard has a tab or a button for every one. ## Running an app These pages cover a project after its first deploy. Each one is a short recipe: what you want, how to do it in the dashboard and with the CLI, what you will see, and what to do when it does not work. - [**Logs**View your app's logs live in the dashboard or with ox logs: filter by severity and process, search with a regex, pick a time range, and download them.](https://deploywithox.com/docs/operate/logs) - [**Roll back**Roll back a deployment to a kept release without a rebuild. The live release serves until the old one passes its health check. What happens to your data.](https://deploywithox.com/docs/operate/rollback) - [**Backups and restore**ox backs up your app's databases every day with nothing to turn on. Back up now, keep copies in your S3 bucket, download a backup, and restore it safely.](https://deploywithox.com/docs/operate/backups) - [**Variables**Set environment variables and secrets in the dashboard or with ox vars, reference one in another, see the keys ox provides and hides, restore old ones.](https://deploywithox.com/docs/operate/variables) - [**Staging and previews**Turn on a preview deployment per branch and a staging copy, each with its own database, on your own server. Limits, passwords and promote to production.](https://deploywithox.com/docs/operate/previews) - [**Services**See every service your app uses on one card: status, size, backups and its connection URL, then browse Postgres, Redis, NATS or ClickHouse data, read-only.](https://deploywithox.com/docs/operate/services) - [**Domains and HTTPS**Add a custom domain to your app: create the A record, add the name in ox, and Caddy gets and renews the HTTPS certificate. What each domain status means.](https://deploywithox.com/docs/operate/domains) When something fails, [Troubleshooting](https://deploywithox.com/docs/troubleshooting) lists the messages ox prints and their fixes. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/operate # View app and deploy logs: live, search, download https://deploywithox.com/docs/operate/logs View your app's logs live in the dashboard or with ox logs: filter by severity and process, search with a regex, pick a time range, and download them. To view your app's logs, open the project's **Logs** tab in the dashboard, or run `ox logs --follow` in a terminal. The logs are the lines your app, its workers and its cron jobs print. You can watch them live, filter and search them, look at a time range, and download them. Each deploy also has a log of its own, with the build output. The log explorer over the last hour: one bar a minute, colored by level, and a red band where errors spiked. The query keeps the web process and hides health checks. The open line's **Lines around** brings back the 10 lines before and after it, from the same process. ## Read live logs and follow new lines In the dashboard, open the project and pick the **Logs** tab. It shows the last 300 lines and checks for new ones every 5 seconds. Keep **Follow** ticked to stay on the newest line. Scroll up and Follow turns off, so the page stops jumping while you read. From a terminal: Watch the logs: ```sh ox logs shop --follow ``` Without `--follow`, `ox logs` prints the last 100 lines and stops. Change that with `-n`; the [CLI reference](https://deploywithox.com/docs/cli#deploys-and-runs) lists every `ox logs` flag. Secret values show as `[redacted]`; [Variables](https://deploywithox.com/docs/operate/variables#redacted) lists what ox hides. ## Filter by severity and search the logs The bar above the lines has these filters: - **All**, **Errors** and **Warnings** show lines of that severity and worse. - **All processes** is a menu: pick one process, such as the app or one worker. - **Search the logs** finds lines with that text. Case does not matter. - **.*** turns the search into a regular expression. Case matters, unless it starts with `(?i)`. The same filters from a terminal: ```sh ox logs shop --severity error ox logs shop --process web --grep "timeout" ox logs shop --regex "status=5[0-9][0-9]" ``` `--severity` takes `error`, `warn`, `info`, `debug` or `unknown`, a line that says nothing of its level. You can use `--grep` or `--regex`, not both. ### Explore lots of lines with a chart The **Explorer** button above the lines opens the log explorer. A chart shows how many lines of each level arrived over the last 15 minutes, hour, day or week, and marks a burst of errors with a red band. Click a bar, or drag across bars, to look at just that time. The table under it holds up to 100,000 lines, newest first, and **Live** puts new ones on top. Type a query in the bar, and press `/` to jump to it from anywhere on the page. Every word must match. `level:error`, `unit:web` and `user_id:42` (a field of a JSON or `key=value` line) narrow the lines, `"two words"` matches a phrase, and a `-` in front of any of these hides what it matches, as in `-healthcheck`. Click a line to see its fields. From there **Copy for AI** copies the line with the 10 lines around it and what project, process and release it came from, ready to paste into a chat with an AI assistant. The same query from a terminal: ```sh ox logs shop --since 1h --query 'level:error -healthcheck' ``` ### See the lines around a match A filter hides the lines next to an error, and those often say why it happened. When a filter or search is on, a matching line shows two buttons: **↑10** brings back the 10 lines just above it, and **↓10** the 10 lines just below. In the live view you can also click the line, or press Enter on it, to open both sides. The lines that come back have every severity and no search filter, from the same process the page shows. They look dimmer than the matching lines, and a line that is already on the screen is not shown twice. Press a button again to go 10 lines further. If no buttons show on a search, your server's agent is too old to send a line's place: update it with `ox servers update `. The terminal has no such button. Use a time range with no filter to see what came before a line. ## Look at a time range, up to 30 days back The **Time range** menu has **Live**, **Last 15 min**, **Last hour**, **Last 24 hours**, **Last 7 days** and **Custom**. Custom asks for a start and an end time in UTC. **Load earlier** goes further back. A range or a search turns Follow off; tick it again to go back to live. A range from a terminal: ```sh ox logs shop --since 2h ox logs shop --since 2026-10-07T09:00 --until 2026-10-07T10:00 ``` `--since` and `--until` take a length like `2h` or `7d`, a time like `2026-10-07T09:00` (UTC), or an RFC 3339 time. Your server keeps logs for up to 30 days, and less when they fill their share of the disk: 500 MB, or 1 GB on a disk of 40 GB or more, for the whole server. The bottom of the page says how far back this server goes. ## Download logs as .log or ndjson The bottom of the Logs tab says **Download the last 24 hours** in the live view, or **Download** when a range or search is on, with two links: - **.log**: one line per log line, as time, process and text. - **.ndjson**: one JSON object per line, with `time`, `process`, `unit`, `severity` and `text`. A download takes your range and filters and stops at 100 MB. From a terminal, `-o` saves the same thing to a new file, and `--json` makes it ndjson: Save logs to a file: ```sh ox logs shop --since 7d --severity error -o errors.log ``` It prints `Saved errors.log` with the size. It never writes over a file that already exists. ## Read the log of one deploy or build A deploy, rollback, backup or restore has a log of its own. Open it from **Runs** on the **Deployments** tab, or list the runs and print one: One run's log: ```sh ox runs shop ox logs shop --run ``` When a run fails, its log ends with the step, the cause and the fix. [Troubleshooting](https://deploywithox.com/docs/troubleshooting) explains each message. ## If the logs show nothing - **No lines at all:** the app may not be running. Check `ox status shop`, then the last run's log. If it keeps restarting, see [The app keeps crashing](https://deploywithox.com/docs/troubleshooting#crash-loop). - **A download ends with** `ox: the download stopped at the 100 MB limit; narrow the time range or the filters for the rest`: pick a shorter range or add a filter. - **Older lines are gone:** they are past the 30 days or the size limit above, and ox cannot get them back. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/operate/logs # Roll back a deployment to the previous release https://deploywithox.com/docs/operate/rollback Roll back a deployment to a kept release without a rebuild. The live release serves until the old one passes its health check. What happens to your data. To roll back a deployment, press **Roll back to this** on the project's **Deployments** tab, or run `ox rollback --wait`. ox puts an older kept release back in charge without building it again, and your visitors keep using the live release until the old one passes its health check. Use it when a deploy went live and something is wrong. A rollback starts the kept release again beside the live one, without building it. Caddy keeps sending visitors to the live release until the kept one passes its health check, for up to 120 seconds. Then traffic moves to it, and the release it replaced stops 10 seconds later. ## Roll back to the previous release In the dashboard, open the project and pick the **Deployments** tab. The **Releases** card lists the releases ox kept. Press **Roll back to this** on the one you want, read the note, then press **Roll back**. From a terminal, this goes back to the newest release that is not live: Roll back: ```sh ox rollback shop --wait ``` To pick another one, give its id with `--release`. Without `--wait`, it prints `Started rollback on shop: run ` and the command to read its log. ox keeps 3 releases for production, 2 for staging and 1 for a preview. When the disk runs low, ox may delete older ones, but it always keeps the newest release that is not live, so there is something to go back to. ## What happens to traffic during a rollback The old release starts beside the live one. ox waits for it to pass the health check, for up to 120 seconds. Only then does traffic move to it, and the release it replaced stops 10 seconds later. Your visitors keep using the live release the whole time. If the old release fails its health check, ox stops it and nothing changes for your visitors. The run's log says why. One exception: if your app pins a fixed port in [[app]](https://deploywithox.com/docs/config#app), two releases cannot run at once, so the app restarts in place and is down for a moment. ## What happens to the database and migrations **A rollback does not undo migrations.** The database stays as the newest release left it. The dashboard says so too: `Switches back without rebuilding. Database migrations are not reverted.` Both releases use the same database, and a rollback does not undo migrations. Before each deploy that runs a migrate step, ox saves a **before migrate** copy of each PostgreSQL database that has tables, and keeps the last 3. Before each deploy that runs a migrate step, ox saves a copy of every PostgreSQL database that has tables. You find it on the **Backups** tab, marked **before migrate**, and ox keeps the last 3. If the old code cannot work with the new tables, restore that copy as its own step. The restore replaces the data, so anything written since that deploy is lost. See [Restore from a backup](https://deploywithox.com/docs/operate/backups#restore). ## What happens to environment variables By default, the rolled-back release runs with the variables you have now. To bring back the ones it ran with, tick **Also restore the variables this release ran with**, or add `--restore-variables`: Roll back code and variables: ```sh ox rollback shop --restore-variables --wait ``` That saves the old variables as a new version, so `ox vars history shop` shows the change (see [variable history](https://deploywithox.com/docs/operate/variables#history)). ## The next push deploys again A rollback does not turn off deploy on push. The old release stays live until a new commit reaches your branch, and then ox deploys that commit as usual. To hold the rollback while you fix things, run `ox autodeploy shop off`, and turn it back on when the fix is in. ## If the rollback does not work - **`no kept release to roll back to`:** the project has deployed only once, or the release you named is no longer kept. Deploy a known good commit instead, from the Commits drawer or with `ox deploy shop --ref --wait`. - **The health check fails:** the old release does not start with today's variables or database. Try `--restore-variables`, or read [Health check failed](https://deploywithox.com/docs/troubleshooting#health-check-failed). - **The variables of the release were not recorded:** the message says `roll back without them`. Leave out `--restore-variables`. - **Another run is going:** wait for it to finish. See [Another operation is running](https://deploywithox.com/docs/troubleshooting#busy). Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/operate/rollback # Daily database backups and restore on your VPS https://deploywithox.com/docs/operate/backups ox backs up your app's databases every day with nothing to turn on. Back up now, keep copies in your S3 bucket, download a backup, and restore it safely. ox backs up your app's databases every day, soon after 03:00 UTC, with nothing to turn on: PostgreSQL with `pg_dump`, and SQLite and a project's own Redis or MySQL as files. Each backup is kept on your server, and a copy goes to your own S3 bucket when you add one. You can also back up now, download a backup, and restore from one with `ox restore`. Once a day, soon after 03:00 UTC, ox backs up each service it can, PostgreSQL with `pg_dump`. Your server keeps 7 daily backups, or 2 once S3 has a copy, 3 manual ones and 3 from before a migrate. Your S3 bucket keeps 30 daily and manual ones together. The counts are per service. ## Turn on daily backups There is no switch: backups are on for every service that can be backed up, from its first deploy. - **PostgreSQL** is backed up with `pg_dump` while it keeps running, whether the database is shared or only for this project. - **Redis, Neo4j, Qdrant and MySQL** are backed up as files when they run only for this project. The service stops for a moment while its files are copied, and the log says for how long. A shared one is not backed up. - **A custom service** is backed up as files too. Set `backup = false` on it in ox.toml to skip it ([custom services](https://deploywithox.com/docs/config#custom-services)). - **SQLite files** in the project's data folder are backed up as well. The daily backup runs once a day, soon after 03:00 UTC. Each one is kept on your server. To keep a copy off the server too, add an S3 bucket in Settings, or from a terminal: Send backups to S3 as well: ```sh ox settings s3 --endpoint https://.r2.cloudflarestorage.com --bucket ox-backups --access-key --from-file secret.txt ``` `--from-file` reads the secret key from a file, or from standard input with `-`, so it never sits on the command line. Without S3, the backup log says `no S3 target in Settings; the backup stays on this host`. ## How many backups ox keeps | Kind | On your server | In S3 | | --- | --- | --- | | Daily | 7, or 2 once S3 has a copy | 30, daily and manual together | | Manual (Back up now) | 3 | 30, daily and manual together | | Before migrate | 3 | Not sent to S3 | The counts are per service. One project's backups may take up to 20% of the server's disk; past that, ox deletes the oldest ones first, but it always keeps the newest backup of each service. A **before migrate** backup is the copy ox makes of each PostgreSQL database that has tables, right before a deploy runs its migrate step. It is the one to restore when a [rollback](https://deploywithox.com/docs/operate/rollback#database) needs the old tables back. ## Run a database backup now In the dashboard, open the project's **Backups** tab and press **Back up now** on the service. From a terminal, name the service entry as it is in ox.toml: Back up now: ```sh ox backups shop now postgres --wait ``` Without `--wait`, it prints `Started backups on shop: run ` and the command to read its log. ## List and download backups The Backups tab lists each backup with its time, size and kind, and a **Download** button. From a terminal: List and download: ```sh ox backups shop ox backups shop download postgres backup:daily-20261007T030512Z.dump ``` The SOURCE column of the list is what the other commands take: - `backup:` is a backup on your server. - `s3:` is a copy in your S3 bucket. - `trash: