Daily database backups and restore on your VPS
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.
Last updated 2026-10-11
On this page
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.
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_dumpwhile 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. Redis keeps running: it writes a snapshot of itself, and the log says it kept running. The others stop for a moment while their 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 = falseon it in ox.toml to skip it (custom services). - SQLite files in the project's data folder are backed up as well: the first 20 ox finds, in the folder and up to 3 folders below it.
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:
ox settings s3 --endpoint https://<account>.r2.cloudflarestorage.com --bucket ox-backups --access-key <key-id> --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.
When less than 15% of the server's disk is free after a job, ox frees space until 20% is free. Among what it removes are older backups on the server that your bucket still holds: before it removes one, ox asks the bucket, and it removes the backup only if the bucket lists that exact copy, at the same size, right then. Without an S3 bucket, or when the bucket does not answer, every backup stays and the log of that job says why. The newest backup of each service always stays. A deleted project's trash goes before any backup does. Otherwise it stays 7 days: the server's agent empties older trash every day on its own, even while the ox control plane is unreachable. Once you forget the server its agent leaves, and the trash stays in /var/lib/ox/trash until you run sudo ox uninstall there.
Both rules run in the agent on your server, so they hold only on a current agent. A server whose agent is older shows Update available. Such an agent, when the disk runs low, removes backups the bucket once confirmed without asking whether the bucket still has them, empties no trash on its own, and no longer removes services no project uses. Press Update agent on the server's page, or run ox servers update web-1. While the agent is stopped, nothing is cleaned and no backup is taken; if it is back before midnight UTC, the day's backup runs then.
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 needs the old tables back.
What is not backed up
A backup holds a service's data and nothing else. These are in no backup and no S3 copy, so copy them yourself if you need them:
- Files in a
[storage] keepfolder, such as uploads, a DuckDB file or logs. Only the SQLite databases in the project's data folder are backed up (the first 20 found, at most 3 folders down); every other file there is not. - A shared Redis or Neo4j, and a custom service with
backup = false. - Your variables. They live only on your server. ox keeps their last 20 versions there (variable history), but no backup holds them.
- Releases and build caches. ox keeps 3 releases on the server and builds again from your repository.
Before migrate backups are kept on your server only; they are not sent to S3. Unless you add an S3 bucket, every backup is on the same server as the app, so losing the server loses them too. What ox backs up, and how to leave says where each copy lives, what pauses when ox is down, and how to take everything with you.
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:
ox backups shop now postgres --waitWithout --wait, it prints Started backups on shop: run <id> 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:
ox backups shop
ox backups shop download postgres backup:daily-20261007T030512Z.dumpThe SOURCE column of the list is what the other commands take:
backup:<file>is a backup on your server.s3:<key>is a copy in your S3 bucket.trash:<time>/<file>is from a deleted project of the same name, kept in the trash for 7 days.
A download prints Saved <file> with the size. Choose the file with --output; ox never writes over a file that exists.
Restore a database from a backup
A restore replaces the service's data with the backup. Anything written after the backup was made is replaced.
ox restore refuses until you type the project's name again after --confirm. With --wait it streams the run's log and ends with the result.In the dashboard, press Restore on a backup's row. It asks Replace postgres's data with this backup? Tick Replace postgres's data in shop (the box names the project whose page you are on) and press Restore. To restore from a dump on your computer instead, use Restore postgres from a file: pick the file, tick Replace postgres's data in shop with this file and press Upload and restore. It takes up to 95 MB. ox loads a PostgreSQL dump into a copy first, so the live database changes only when the load works.
From a terminal, type the project's name again after --confirm. For staging or a preview, add --env and type that environment's own name, so a restore meant for staging can never be confirmed for production. ox backups shop --env staging lists staging's backups, under a line naming shop-staging:
ox restore shop postgres --from backup:daily-20261007T030512Z.dump --confirm shop --wait
ox restore shop postgres --file shop.dump --confirm shop --wait
ox restore shop postgres --env staging --file shop.dump --confirm shop-staging --waitWhen it is done, the log says restored postgres from <backup>. ox then runs your migrate step and restarts the app. A project you stopped with ox stop shop stays stopped: ox starts its own database for the restore or backup only, and the log ends restored postgres, left stopped; ox start to run it.
What ox keeps in case you change your mind
- PostgreSQL: the app keeps running, but its open database connections are closed. The old database is kept under a new name until the next daily backup, and the log says
the replaced database is kept as <name> until the next daily backup. - File-backed services: the service stops during the restore. The old files are kept beside the new ones until the next daily backup, and if the service does not come back healthy, ox puts them back.
- SQLite: ox stops the app, its workers and cron jobs, and makes a manual backup first.
If your server is gone for good
When a server will not come back (its disk failed, or the droplet or the provider account is gone), bring its projects up on another server from the backups in your S3 bucket. Backups kept only on the lost server are gone with it, so turn on S3 backups before you need them. Your variables are on the lost server only: ox keeps no copy of them, so keep your own with ox vars pull shop -o shop.env after you change them. A shared Redis, Neo4j, Qdrant or MySQL is never backed up, so its data is lost with the server.
- Add a new server (Servers, Add server) and run its command there. It must be connected before you move.
- Open the lost server's page. While it is offline it shows Server gone for good?: choose the new server, type the lost server's name and press Move projects. Every project on it, with its staging copy and previews, moves with its settings, domains and the backups in your bucket.
- A project that clones over ssh gets a new deploy key on the new server; the note after the move prints it with the link to its repository's deploy keys page. Add it there (you can remove the old ox key).
- For each project: add its variables again (they lived on the lost server only, and ox keeps no copy). Its page says so and asks for each one the project needs: fill them in and press deploy, or run
ox review shop --from-file shop.envwith the file you saved. Then restore each database from its newests3:backup on its Backups tab. The bucket's backups show there once that deploy succeeded. - Point each domain's A record at the new server's address.
- Forget the lost server on its page.
ox servers move old-box --to new-box --confirm old-box
ox review shop --from-file shop.env --wait
ox backups shop
ox restore shop postgres --from s3:<key> --confirm shop --wait
ox servers forget old-box --confirm old-boxWhat you get back is your data as of the newest backup in your bucket, at most a day old with daily backups. Uploaded files in a folder your app keeps are not in a backup.
If a backup or restore does not work
restoring replaces the data of postgres in shop; to go ahead retype its name: --confirm shop: add--confirm shop.--confirm "shp" does not match "shop"; nothing was done: type the project's name exactly.this restore was confirmed for "shop", not shop-staging; nothing was done: the confirmation named a different project than the one the restore would change. Type the name of the one you mean.- The dashboard says
tick the box to confirm that the restore replaces postgres's data: tick the box, then press Restore again. - The disk is too small for the restore: the message says how much is free and how much it needs. Free some space, then restore again. Disk full has more.
- A service has no backups: a shared Redis or Neo4j is not backed up, and a custom service with
backup = falseis skipped.