Staging and branch preview deployments on a VPS

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.

Last updated 2026-10-25

On this page

To get a preview deployment for every branch, turn on Branch previews in the project's Settings or run ox previews <project> on; each push then deploys that branch to its own address. Staging is one copy of your app where you try changes before you promote them to production. Each copy has its own database and its own variables, on the same server as production.

With previews on, a push to a branch deploys it as its own copy of the app, with its own database and variables, at a free address under ox's preview domain, made from its name and a label of your server's, unless you set a preview domain. 3 previews run at once by default, and one is deleted after 7 days without a deploy.

Turn on branch preview deployments

Previews are off until you turn them on. In the dashboard, open the project's Settings tab and press Turn on in the Branch previews card. From a terminal:

Turn previews on
ox previews shop on

After that, every branch you push gets its own copy, and each push updates it. Branches that were already there wait for their next push, so turning previews on does not build every old branch next to production; to preview one now, pick it in the Previews menu or run ox previews shop start fix/login. Production's branch and staging's branch never get one. A copy is named after the project and its branch, like shop-fix-login. A name longer than 28 characters keeps its first words and ends in a short code made from the project and the branch, like lazy-planner-feat-cloud-21d4, so two long branches get different names. --env takes the branch either way.

Which branches

By default it is Each branch pushed from now on, except bots, so dependabot/*, renovate/* and branches a bot account pushed are skipped. Pick Only branches matching to list patterns instead; the branches they match get a preview even if they were there before. Changing the list takes the branches it leaves out of the line at once. In a pattern, * matches anything, slashes too, and ? matches one character. You can list up to 20.

Choose branches, or preview one now
ox previews shop --branches feature/*
ox previews shop --branches all
ox previews shop start fix/login
ox previews shop remove fix/login

What a preview's variables are

A preview starts with production's variables, copied when it is made; after that they are its own. It gets its own database, its own Redis and address: PUBLIC_URL and PUBLIC_HOST are the preview's host, and OX_ENV is preview. A value of yours that names production, such as a label or an APP_URL, still says production in the preview. For the app's own address, write a reference once in production, and each environment fills in its own, the build included:

Values that follow the environment
ALLOWED_HOSTS=${PUBLIC_HOST}
VITE_API_BASE_URL=${PUBLIC_URL}/api
CSRF_TRUSTED_ORIGINS=${PUBLIC_URL}

The preview's Variables tab and ox vars mark a copied value that still holds production's address as points at production, with the reference to use. Or set the value on the preview:

Give one preview its own value
ox vars set shop --env fix/login RELEASE_LABEL

A live key, such as a payment or email provider's, should never reach a preview. Mark it production only and new previews never get production's current value. Give your staging copy its own test key, and each new preview starts with that one. With tested deploys on, a copy's runs wait while it still holds production's value of such a key.

See every preview at a glance

The Branch previews card in Settings says in plain sentences what previews do, before you turn them on too: which branches get one, the address each is served at, the memory they share, how many run at once, and which variables are never copied into one. Under it is one row per preview with its state, commit, when it last deployed, the memory it uses now, and whether it is kept, with buttons to open it, read its logs, redeploy it, keep it, or remove it. A failed preview says why and how to fix it, with a link to the run; when only its newest deploy failed, it says the previous release still runs. A preview whose server cannot be read says so instead of looking never deployed. A branch waiting for a slot shows its place in line, and a preview being deleted says so until it is gone. The same list, from a terminal:

List the previews
ox previews shop
ox previews shop --json

Preview limits and cleanup

  • 3 at once by default. You can pick 1 to 10 in Settings or with ox previews shop --at-once 5. Each one is a full copy of your app.
  • Previews never take production's memory. All previews on a server share half of its memory, with no swap and a smaller share of the CPU than production. When they need more, a preview is stopped, never production. A preview you stop with ox stop gives its [limits] memory back to that half until you start it again. Its own services stay reserved, and starting it needs room in the half. Staging is not a preview: it runs like production.
  • When the limit is reached, a new branch waits in line, and ox environments shop shows its place, such as waiting: 2nd. ox previews shop remove BRANCH takes a waiting branch out of the line until its next push.
  • 7 days without a deploy, and the preview is deleted. A push or a deploy by hand resets the clock. It comes back on the next push.
  • A preview is also deleted when its pull request is closed, merged or not, its branch is deleted, or your branch list no longer matches it. The branch gets a new preview on its next push, or when the pull request is reopened.
  • On GitHub, the pull request shows View deployment for its preview. A failed deploy shows ox's one-line fix there (or "Deploy failed" when it has none), and the link goes inactive once the preview is deleted.

To keep a preview past the 7 days, press Keep on it, or run:

Keep a preview
ox keep shop --env fix/login on

HTTPS preview URLs on your own domain

Without a preview domain, each copy gets a free address like https://shop-fix-login.k7mq2xw4ab.preview.getox.in, made from its name and a label of your server's. One free certificate covers every preview on a server, and ox gets it for the server soon after its first project: Let's Encrypt gives ox a limited number a week, so a busy week puts a server in line, and its page says when it comes. Each deploy builds the copy for the address it can serve: https:// once the server has its certificate, and http:// until then; ox moves the copy to https:// by itself about a minute after the certificate comes. PUBLIC_URL and the dashboard's link say the same. A browser that opens the http address of an https copy is sent to https, so an app that needs secure cookies can sign in there.

Cookies on a free address. Preview addresses share a site with other ox customers until ox is on the Public Suffix List. Do not trust cookies on a preview address for sign-in to a real account. Use your own domain for that.

For HTTPS addresses under your own domain, set a preview domain in the account's Settings, under Preview domain, or run:

Use your own preview domain
ox settings preview-domain preview.example.com

Then create one DNS record at your DNS provider: *.preview.example.com, type A, pointing at your server's IP. A preview of fix/login is then at https://shop-fix-login.preview.example.com.

Make a staging environment

Production must have deployed once. On the project's Settings tab, fill in a branch and a domain if you want them, and press Create staging. From a terminal:

Make staging
ox staging shop --branch develop --wait

The copy is named shop-staging. It starts at production's live commit, or follows the branch you name. Your variables are copied once, when it is made; after that the two are set apart. Data is never copied: staging starts with an empty database. Without --domain, it gets an address under your preview domain, or the free one under ox's.

Staging can start with values of its own, set before its first deploy. When a test account names a variable, the form shows a field for it; from a terminal, name the keys and ox asks for each value as ox vars set does, or give a .env file with --from-file. A test account's variable you leave empty gets one ox makes: tester@ at staging's domain (at example.com when it has none), and a random 24-character password of letters and digits. They show on staging's Variables tab. Many apps make their admin from these only on their first start, so this is what makes the test account exist.

Staging with its own admin password
ox staging shop ADMIN_PASSWORD

If staging was already running when an account's variable got its value, the app may never have made that account. The Tests card, the Staging card and ox tests shop then say so, with Remake staging: it deletes staging's app and data and makes it again with the same name, domain, branch and own values, deploys it and checks sign-in. You type the copy's name to confirm, since its data is gone for good. A sign-in check that signs the accounts in proves they work, and the line goes away without a remake.

Remake staging
ox staging shop --remake --confirm shop-staging

Password-protect copies

This is off by default. Turn it on for every staging and preview copy in the account's Settings, under Password-protect staging and previews. The site then asks for the username ox and a password. Copy the password when it is shown: ox doesn't keep it, so it can't show it again. One copy can differ from the account's choice:

Protect copies
ox protect --account on
ox protect shop --env staging off

Promote staging to production

Promote deploys production at the commit staging runs. On the staging copy's page, press Review in Promote to production. It shows the commits production would get, and nothing happens until you press Promote to production. From a terminal:

ox promote deploys production at the commit staging runs, after it lists the commits and asks. Only the code moves: production keeps its own variables, data and domains.
Promote
ox promote shop --wait

You can name production or its staging copy. It lists the commits and asks Promote to production? [y/N]. Add --yes where no one can answer, like in CI. If the promote pauses deploy on push for production, it says so first, with the command that turns it back on.

Only the code moves. Production keeps its own variables, data, domains, password setting and previews. If the new code needs a new variable, set it on production first. The migrations run against production's database. A preview cannot be promoted: merge its branch, or promote staging.

If a preview does not work

  • 3 previews already run, the most this project runs at once; remove one first, or raise the number in Settings: remove a preview, or raise the number.
  • this preview ran out of memory: previews on this server share 2048 MB, half of it, so production keeps the rest: remove another preview, lower at once, or move to a bigger server (see Not enough memory).
  • capacity: previews on this server share 3968 MB, half of it, so production keeps the rest; they would need 4096 MB: ...: the deploy was refused before it started, because this preview and the others would not fit in their half. Stop a preview whose [limits] memory is listed, remove one you do not need, lower this preview's [limits] memory, or move to a bigger server.
  • A branch got no preview: check that previews are on and that your branch list matches it. A preview removed after 7 days comes back only on a new push.
  • a preview cannot be promoted; promote staging, or merge the branch: run promote on the staging copy.
  • A preview address does not load over HTTPS: without a preview domain, the server's page says where its certificate is: on its way, its place in line and when, or why it waits (the server offline, not seen at a public address yet, paused by ox, or your account's certificates for the week taken by your other servers), or that the certificate is late and ox is looking into it. The http:// address works meanwhile. A worker's address needs a name longer than 2 characters, and no name may hold --. With a preview domain, check the *. record points at your server. See Domains.