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. There is nothing to turn on. It backs up PostgreSQL with pg_dump, copies SQLite and a project's own Redis or MySQL as files, and saves the folders ox.toml keeps, such as uploads. Each backup stays 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 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
Backups are already on. Every service that can be backed up is backed up from its first deploy. The daily backup runs once a day, soon after 03:00 UTC, and keeps its copy 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 the key never sits on the command line. Without S3, the backup log says no S3 target in Settings; the backup stays on this host.
What each service's backup holds
| Service | How ox backs it up |
|---|---|
| PostgreSQL | With pg_dump while it keeps running, whether the database is shared or only for this project. |
| Redis, MySQL | As files, copied while the service keeps running. The log says it kept running. MySQL's copy needs as much free disk as its data, and the backup says so when the disk is too full. |
| RabbitMQ | An export of its queues, exchanges and bindings, taken while it keeps running. The messages in the queues are not in it. |
| Neo4j, Qdrant, ClickHouse, Meilisearch | As files. Each stops for a moment while its files are copied, and the log says for how long. |
| A custom service | As files. Set backup = false on it in ox.toml to skip it (custom services). |
| SQLite | The first 20 SQLite files ox finds in the project's data folder, in the folder and up to 3 folders below it. |
Each [storage] keep folder | A .tar.gz of the whole folder, made while the app keeps running. A file the app changes during the copy is saved as it was read. |
Redis, Neo4j, Qdrant, MySQL, ClickHouse, Meilisearch and RabbitMQ always run only for their own project, so each one's backup holds that project's data alone.
A kept folder's backup is a whole copy each time, not only what changed, so a big folder takes room. A 50 GB media folder can take up to 500 GB on your server (7 daily and 3 manual copies), within the 20% limit below. When the limit is reached, the biggest folder loses its oldest copies first, so your database keeps its daily backups. A SQLite file inside a kept folder that has its own backup is left out of the folder's copy.
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, and it always keeps the newest backup of each service.
A before migration backup is the copy ox makes of each PostgreSQL database that has tables, right before a deploy runs its migrate step. Restore it when a rollback needs the old tables back. These copies stay on your server and are never 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.
When the disk runs low
When less than 15% of the server's disk is free after a job, ox frees space until 20% is free. It removes, in this order, old preview releases that are not live, a deleted project's trash, older backups on the server that your bucket still holds, build caches, and old releases. Trash always goes before any backup does. Before it removes one, it 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 job's log says why. The newest backup of each service always stays. These checks run in the agent on your server, so they need a current agent. An out-of-date agent shows Update available, and ox updates it for you.
Otherwise a deleted project's trash stays 7 days. The agent on your server empties older trash every day by itself, even while ox 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.
While the agent on your server is stopped, nothing is cleaned and no backup is taken. If it is back before midnight UTC, the day's backup runs then. A server whose agent is out of date shows Update available. To update it now, press Update agent on the server's page, or run ox servers update web-1.
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 the project's data folder outside a
[storage] keepfolder, other than the SQLite databases there (the first 20 found, at most 3 folders down). - A custom service with
backup = false. - Messages waiting in a RabbitMQ queue. A restore brings back the same queues, empty.
- 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.
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 download prints Saved <file> with the size. Choose the file name with --output. ox never writes over a file that exists.
The 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.
Open an encrypted copy from your bucket
Your server encrypts the copy it sends to your bucket with age, and the copy's name ends in .age. This protects the copy if your bucket or its keys leak. It does not hide your backups from ox, which keeps the key sealed so it can restore them for you, and which holds your S3 keys. The copy on your server is not encrypted.
To open a downloaded .age copy yourself, get the key and use the age tool:
ox backups key > backup.key
age -d -i backup.key daily-20261007T030512Z.dump.age > daily-20261007T030512Z.dumpSettings shows the same key under Backup key, behind one click, and writes each showing to the audit log. Keep a copy of the key somewhere other than ox. It is the only way to read your bucket if ox is gone.
A server whose agent is out of date still uploads copies without encryption until its agent is updated, and Settings names those servers. Copies made before encryption stay as they were and age out of the 30 kept.
Restore a database from a backup
A restore replaces the service's data with the backup. Anything written after the backup was made is lost.
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 and press Restore. The box names the project whose page you are on.
To restore from a dump on your computer, use Restore postgres from a file. Pick the file, tick Replace postgres's data in shop with this file and press Upload and restore. The file can be 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 database only for the restore or backup, and the log ends restored postgres, left stopped; ox start to run it.
What ox keeps in case you change your mind
ox keeps a way back. How depends on the service:
- PostgreSQL: the app keeps running, and 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: ox unpacks the backup beside the service's files while the service keeps running. Then it stops the service only to swap the two in one step, and keeps the old files beside the new ones until the next daily backup. If the service does not come back healthy, ox swaps them back. RabbitMQ's export is loaded into the restarted broker, and if it does not load, ox swaps the old files back too.
- SQLite: ox stops the app, its workers and cron jobs, and makes a manual backup first.
- Kept folders: ox unpacks the backup beside the folder while the app keeps running. Then it stops the app, its workers and cron jobs only to swap the two folders, and keeps the old folder beside the new one until the next daily backup. A SQLite database inside the folder stays as it is. If the archive cannot be unpacked, nothing changes. The swap is one step, so if the server stops in the middle, the folder holds either the old files or the restored ones, never a mix.
If your server is gone for good
When a server will not come back, bring its projects up on another server from the backups in your S3 bucket. This covers a failed disk, a deleted droplet, or a lost provider account. You get back your data as of the newest backup in your bucket, at most a day old with daily backups.
Three things cannot come back this way. Backups kept only on the lost server are gone with it, so turn on S3 backups before you need them. Your variables lived on the lost server only, and ox keeps no copy, so save your own with ox vars pull shop -o shop.env after you change them. A custom service with backup = false loses its data 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 moves with its staging copy and previews, its settings, its 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 a link to the repository's deploy keys page. Add it there. You can remove the old ox key.
- For each project, add its variables again. Its page 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 backup marked newest before the move on the project's Backups tab. The bucket's backups show there once that deploy has succeeded. The new server's daily backups go on meanwhile and may list above it, but ox never removes the backups from before the move to make room for them.
- 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-boxIf 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: you confirmed 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 dump needs the postgres extension "vector", which this database does not have: something in the dump uses the extension. Add it to the service inox.toml(postgres = { extensions = ["vector"] }), deploy, then restore again.the dump creates the postgres extension "vector", which this database does not have, but the dump's tables and schema load without it: the database you dumped had the extension installed, but no table or column needs it. Make the dump again without it (pg_dump --exclude-extension=vector, PostgreSQL 17 or later) and restore that file. Adding the extension inox.tomlworks too. ox does not check function and trigger bodies, so if one of yours calls the extension, add it.the dump holds a psql command: a plain SQL file has a backslash command (\!,\i,\o) outside quoted text and comments, and a restore refuses it. A dump made bypg_dumphas none, so make the dump again withpg_dumpand restore that file. A restore loads the file as your project's own database user, which cannot harm the server.- 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 kept folder's card says Not backed up yet, and the daily backup fails naming the folders it skipped: the server's agent is too old to back up kept folders. Update it with
ox servers update web-1. The rest of the backup still ran. - A service has no backups: a custom service with
backup = falseis skipped.