How to back up a VPS to an S3 bucket with Cipi
By Andrea Pollastri · Last updated: · free to read, no paywall
A dump that lives on the same disk as the app is a snapshot, not a backup. This guide walks through the whole off-site loop with Cipi: create a bucket, store credentials once, run the first upload, schedule it, prune the old ones, and restore onto a clean VPS so you know the plan works.
Why the same-disk dump is not a backup
Provider snapshots, a .sql.gz in /var/log, a tarball next to storage/ — they all die with the machine. Disk full, ransomware, a fat-fingered rm -rf, a region outage, a stolen VPS: the copy you need is the one that was already somewhere else.
The rule that still holds is 3-2-1: three copies, two media, one off-site. Cipi covers the last hop. cipi db backup gives you a fast local rollback; cipi backup run ships the database and the shared/ folder to Amazon S3 or any S3-compatible bucket. That is the copy you restore from when the VPS is gone.
A backup you have never restored is a hope, not a plan. Budget 20 minutes after the first successful upload and do the drill in Restore. Repeat it every quarter.
What Cipi actually uploads
Each run creates a timestamped prefix on the bucket:
Inside that folder you get two archives:
db.sql.gz— a compressed dump of the app database (mariadb-dump --single-transaction, or the PostgreSQL equivalent if the app uses that engine).shared.tar.gz— the entire/home/<app>/shared/directory:.env,storage/, uploaded files, and anything else that survives a Deployer release swap.
Code is not in the archive. Releases live in Git; if you need the last good tree, clone the repo. What you cannot clone is the database and the files users uploaded after go-live — those two files are the restore point.
Since v4.7.14 staging happens on disk in /var/tmp, not in a RAM-backed /tmp. Large apps no longer die halfway through because tmpfs filled up. Override with tmpdir in backup.json or the CIPI_BACKUP_TMPDIR environment variable.
What you need
- A VPS already managed by Cipi. If you are starting from zero, run
wget -O - https://cipi.sh/setup.sh | bashon a fresh Ubuntu 24.04/26.04 box, thencipi app create. The getting started guide covers the install. - At least one app on that server.
cipi backup runwithout a name backs up every app;cipi backup run myapptargets one. - An S3 or S3-compatible bucket and an access key that can write to it. Create a dedicated IAM user — do not reuse your root cloud credentials.
Create the bucket
Pick a provider, create a private bucket in a region that is not the same datacenter as the VPS, and mint a key pair scoped to that bucket only. Enable server-side encryption if the provider offers it. Versioning is optional but cheap insurance against an accidental overwrite.
Minimum IAM actions on that bucket: s3:PutObject, s3:GetObject, s3:ListBucket, s3:DeleteObject. The last one is only required if you want cipi backup prune to clean old prefixes.
| Provider | Endpoint URL |
|---|---|
| AWS S3 | leave empty |
| Hetzner Object Storage | https://<datacenter>.your-objectstorage.com |
| DigitalOcean Spaces | https://<region>.digitaloceanspaces.com |
| Backblaze B2 | https://s3.<region>.backblazeb2.com |
| MinIO | https://your-minio-host |
| Johnny (self-hosted S3) | your Johnny public URL |
Any S3-compatible store works. If you want the off-site copy on a second VPS you own — not another SaaS invoice — Johnny is the open-source option already mentioned in the self-hosted developer stack.
Configure Cipi once
SSH in as the admin user, become root, and run the wizard. It writes /etc/cipi/backup.conf and you should not have to touch it again unless the key or the bucket changes.
# as root on the VPS
$ cipi backup configure
# → AWS Access Key ID
# → AWS Secret Access Key
# → Bucket name
# → Region
# → Endpoint URL (empty for AWS; required for everyone else)Keep /etc/cipi/backup.conf mode 600 and owned by root. Those keys open every backup you will ever take. If a key leaks, rotate it at the provider and run cipi backup configure again — old objects stay in the bucket; only new uploads use the new key.
cipi db backup works with no configuration. cipi backup run does not. If the S3 command fails with a credentials error, you skipped this step.
First backup and verify
Do not put anything in cron until a manual run has landed in the bucket. Start with a single app:
$ cipi backup run myapp
# → s3://your-bucket/cipi/myapp/2026-08-22_143015/db.sql.gz
# → s3://your-bucket/cipi/myapp/2026-08-22_143015/shared.tar.gz
$ cipi backup list myapp
$ cipi backup run
# no app name = every app on the serverThen confirm from the provider console — or with the AWS CLI, which also talks to compatible endpoints if you pass --endpoint-url:
$ aws s3 ls s3://your-bucket/cipi/myapp/
$ aws s3 ls s3://your-bucket/cipi/myapp/2026-08-22_143015/You should see both objects, with sizes that match a compressed dump plus the shared/ tree. A 200-byte object usually means an empty dump or a failed upload — fix that before you schedule anything.
Cron, retention and prune
Daily at 02:00 is the boring default. Prune at 03:00 so yesterday's run is never deleted by a race. Four weeks of history is enough for most Laravel apps; drop to two if the bucket bill hurts, raise to eight if you need to unwind a slow-burn data bug.
# crontab -e as root
0 2 * * * /usr/local/bin/cipi backup run myapp >> /var/log/cipi/backup.log 2>&1
0 3 * * * /usr/local/bin/cipi backup prune myapp --weeks=4 >> /var/log/cipi/backup-prune.log 2>&1To back up every app on the box, drop the app name from both commands. cipi backup prune reads the same /etc/cipi/backup.conf and works with any compatible provider.
$ cipi backup prune myapp --weeks=4
$ cipi backup prune myapp --weeks=2Watch /var/log/cipi/backup.log for a week. A silent cron that failed on day one is how people discover they have no backups the morning after a disk death.
Restore drill
Do this on a staging app, or on a spare VPS, not on live traffic the first time. The happy path from an S3 prefix back to a running app:
$ cipi backup list myapp
$ aws s3 cp s3://your-bucket/cipi/myapp/2026-08-22_143015/db.sql.gz /tmp/db.sql.gz
$ cipi db restore myapp /tmp/db.sql.gz
$ aws s3 cp s3://your-bucket/cipi/myapp/2026-08-22_143015/shared.tar.gz /tmp/shared.tar.gz
$ tar -xzf /tmp/shared.tar.gz -C /home/myapp/If the VPS itself is gone, the sequence is: install Cipi on a new Ubuntu box, cipi app create with the same Git remote, then the two restores above, then cipi ssl install and a DNS cut. The infrastructure docs list the same commands; this drill is the part most people skip.
For a same-server rollback — bad migration, bad deploy — the local dump is faster:
$ ls -lh /var/log/cipi/backups/myapp_*.sql.gz
$ cipi db restore myapp /var/log/cipi/backups/myapp_20260822_143012.sql.gz
$ cipi deploy myapp --rollbackLocal dumps in /var/log/cipi/backups/ are never deleted automatically. On a busy deploy schedule they fill the disk. Keep the last five and drop the rest: ls -t /var/log/cipi/backups/myapp_*.sql.gz | tail -n +6 | xargs rm -f.
Backup before every release
Webhook auto-deploy cannot pause for a backup — the push fires cipi deploy immediately. For production, put a backup stage in CI so a failed dump blocks the release. The full GitHub Actions and GitLab examples live in Safe deploy — backup before release. The two commands you need in that stage:
$ cipi db backup myapp
# fast local rollback
$ cipi backup run myapp
# off-site copy of DB + shared/Used together you get a same-box restore that takes seconds and an S3 prefix you can still open if the box is gone. If either command fails, do not deploy.
Mistakes that make backups useless
- Same region as the VPS, same provider account, no second copy. A suspended account or a regional outage takes both. Put the bucket in another region, or on another vendor.
- Root cloud keys on the server. A dedicated IAM user with four S3 actions on one bucket is enough. If the VPS is compromised, the blast radius stays that bucket.
- Never testing restore. Compression flags, empty dumps, a
shared/extract that overwrites the wrong tree — you only meet those bugs during an incident unless you drill. - No prune, then a surprise bill. Daily 2 GB dumps become 60 GB a month. Set
--weeksthe same day you set cron. - Trusting provider snapshots alone. They are convenient. They are also on the same account, often the same region, and they do not give you a portable
db.sql.gz. - Skipping
shared/. A database without uploaded files is half an app.cipi backup runalready packs both; do not invent a dump-only cron "to keep it simple".
Run it on a Cipi VPS
Cipi is the free, open-source Laravel deploy CLI used throughout this guide. One command turns a fresh Ubuntu VPS into a hardened production server — and cipi backup configure is the step that makes the server survivable.
Frequently asked questions
Does Cipi work only with Amazon S3?
No. cipi backup configure accepts any S3-compatible endpoint: Hetzner Object Storage, DigitalOcean Spaces, Backblaze B2, MinIO, or a self-hosted store such as Johnny. Leave the endpoint empty for AWS; fill it for every other provider.
What is the difference between cipi db backup and cipi backup run?
cipi db backup writes a compressed SQL dump on the same VPS under /var/log/cipi/backups/. It is always available and needs no configuration. cipi backup run dumps the database and the app shared/ folder, then uploads both archives to your S3 bucket. Use the local dump for a fast rollback; use the S3 copy for a real off-site restore.
How often should I back up a Laravel VPS?
Daily is the sensible default for production. Add cipi backup run to the root crontab at a quiet hour, then prune older than four weeks. If the database changes constantly, run it twice a day. Always run a backup before a risky migration or a production deploy.
Can I restore a Cipi S3 backup onto a new VPS?
Yes — that is the point of an off-site copy. Install Cipi on a fresh Ubuntu VPS, recreate the app, download db.sql.gz and shared.tar.gz from the bucket, restore the database with cipi db restore, and extract shared/ into /home/<app>/.
Where does Cipi store S3 credentials?
cipi backup configure writes them to /etc/cipi/backup.conf. Keep that file root-only. The same credentials are used by cipi backup run, list and prune. Staging of large archives defaults to /var/tmp so a small RAM-backed /tmp does not abort the job.