Backups · S3 · VPS

How to back up a VPS to an S3 bucket with Cipi

By · 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.

In this guide
  1. Why the same-disk dump is not a backup
  2. What Cipi actually uploads
  3. What you need
  4. Create the bucket
  5. Configure Cipi once
  6. First backup and verify
  7. Cron, retention and prune
  8. Restore drill
  9. Backup before every release
  10. Mistakes that make backups useless
  11. FAQ

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:

s3://your-bucket/cipi/myapp/2026-08-22_020015/

Inside that folder you get two archives:

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

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.

ProviderEndpoint URL
AWS S3leave empty
Hetzner Object Storagehttps://<datacenter>.your-objectstorage.com
DigitalOcean Spaceshttps://<region>.digitaloceanspaces.com
Backblaze B2https://s3.<region>.backblazeb2.com
MinIOhttps://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 server

Then 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>&1

To 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=2

Watch /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 --rollback

Local 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

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.

wget -O - https://cipi.sh/setup.sh | bash

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.

Keep reading