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-08

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 sslip.io address made from its name and your server's IP, 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, Redis index 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 start without it; set the preview's own test key if it needs one.

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. 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 http://shop-fix-login.203-0-113-7.sslip.io, made from its name and your server's IP. It is plain HTTP, with no certificate.

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 sslip.io one.

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 keeps only a hash and cannot 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).
  • 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 address is HTTP only. With one, check the *. record points at your server. See Domains.