Ubuntu VPS Backup Restore
Recover files into a safe location, rebuild an application with its database, and check the result before returning traffic.
What do you need to recover?
If you cannot access the old VPS
Before rebuilding, confirm you can retrieve the backup and its decryption key without that server. You also need the application release, runtime and database versions, uploads, configuration, separate secrets and access to DNS. A copy or password stored only on the lost VPS cannot help you recover it. Use the SSH recovery checks if the server still exists but login fails.
If you have a provider snapshot or disk image
Check included disks, snapshot time, application consistency, retention after server/account deletion, target disk size, CPU architecture and the provider’s supported restore procedure. A provider snapshot is not automatically exportable to another host. Rebuilding Ubuntu and importing application data is a separate route; this guide’s file archive is not a bootable machine image.
Check Ubuntu image and architecture compatibility →In this guide
Define the small recovery exercise
Use Bash, GNU tar, sha256sum and OpenSSH on Ubuntu or a Linux/WSL client. Run server-labelled commands in one server terminal and client-labelled commands in one terminal on a separate computer. Keep each terminal open so its variables remain available. Stop after any command error.
The exercise uses two disposable files under your home directory. It does not change Nginx, firewall rules or live content, and extraction never runs as root. Start with the fixtures below; the optional substitution uses only the two files from the static HTTPS deployment walkthrough.
For a larger site, inventory its assets, redirects, included configuration, DNS records and dependencies separately. This two-file example is not a complete machine or application backup.
Prepare two files without changing the website
On the server, create a private practice directory. The configuration below is a file to archive, not a service configuration to activate.
umask 077
backup_lab=$(mktemp -d "$HOME/ubuntu-vps-backup.XXXXXX")
mkdir -p "$backup_lab/payload/site" "$backup_lab/payload/nginx" "$backup_lab/export"
printf '%s\n' '<!doctype html><html lang="en"><title>Restore drill</title><h1>Recovered static page</h1></html>' > "$backup_lab/payload/site/index.html"
printf '%s\n' 'server {' ' listen 8080;' ' server_name example.test;' ' root /var/www/first-site;' '}' > "$backup_lab/payload/nginx/first-site.conf"
printf 'Practice directory: %s\n' "$backup_lab"Optional: to back up the tutorial's real page and server block instead, replace only these disposable copies using the commands below. Both source files must be readable by your account. Inspect the configuration for secrets first, and pause deployments while collecting the files so they represent the same release. Skip this block for the fixture exercise.
install -m 600 /var/www/first-site/index.html "$backup_lab/payload/site/index.html"
install -m 600 /etc/nginx/sites-available/first-site "$backup_lab/payload/nginx/first-site.conf"These commands read the live sources without modifying them. They do not copy certificate private keys or entire system directories. A real site may need additional files; add them deliberately to both the inventory and checksum manifest.
Create the archive and two levels of checksums
On the server, record checksums of the selected files, create an archive with relative paths, then checksum the archive. Keep the payload unchanged during this step.
(
cd "$backup_lab/payload" &&
sha256sum site/index.html nginx/first-site.conf > SHA256SUMS
)
tar -czf "$backup_lab/export/static-site.tar.gz" -C "$backup_lab/payload" site nginx SHA256SUMS
(
cd "$backup_lab/export" &&
sha256sum static-site.tar.gz > static-site.tar.gz.sha256
)
tar -tzf "$backup_lab/export/static-site.tar.gz"
printf 'Server export directory: %s\n' "$backup_lab/export"The listing should contain site/, nginx/, the two selected files and SHA256SUMS. The archive checksum detects changes to the transferred archive; the internal manifest checks the extracted files. Neither checksum authenticates a backup if someone can replace both data and the expected checksum.
Copy the backup to a separate computer
On your client computer, set remote_backup to the exact export directory printed by the server. Replace the sample address, account and key filename with your working SSH details. The address 203.0.113.10 is reserved for documentation; REPLACE is a placeholder, not the directory suffix generated by mktemp.
umask 077
recovery_dir=$(mktemp -d "$HOME/ubuntu-vps-restore.XXXXXX")
remote_backup=/home/deploy/ubuntu-vps-backup.REPLACE/export
scp -i ~/.ssh/ubuntu_vps "deploy@203.0.113.10:$remote_backup/static-site.tar.gz" "$recovery_dir/"
scp -i ~/.ssh/ubuntu_vps "deploy@203.0.113.10:$remote_backup/static-site.tar.gz.sha256" "$recovery_dir/"Verify the SSH host fingerprint through a trusted channel before accepting a new host. If copying fails because SSH fails, use the SSH troubleshooting guide. Do not disable host-key checking to make the transfer succeed.
SCP protects the transfer through SSH. The tar.gz file itself is compressed, not encrypted: protect the destination account and storage, and use encryption at rest when the data requires it. A second directory on the same VPS does not provide an off-server copy.
Verify and restore into a new empty directory
On the client, first list this trusted archive and check its SHA-256 against the expected manifest. A mismatch needs investigation or a fresh transfer; never overwrite the expected checksum to conceal it.
(
set -euo pipefail
cd "$recovery_dir"
sha256sum -c static-site.tar.gz.sha256
tar -tzf static-site.tar.gz
)Inspect the listing: it should contain only the intended site/, nginx/ and checksum manifest. For this exercise, use only the archive you just created. After that review, the following block repeats verification and extracts into a new empty directory as your ordinary user. It stops on a command error and does not replace existing files. Do not change the target to / or add sudo.
(
set -euo pipefail
cd "$recovery_dir"
sha256sum -c static-site.tar.gz.sha256
restore_dir=$(mktemp -d "$recovery_dir/restored.XXXXXX")
tar -xzf static-site.tar.gz -C "$restore_dir" \
--keep-old-files --no-same-owner
cd "$restore_dir"
sha256sum -c SHA256SUMS
cat site/index.html
printf 'Recovered files: %s\n' "$restore_dir"
)Expect OK for both site/index.html and nginx/first-site.conf and the recovered page heading. Keep the printed directory path: restore_dir is local to this block. This verifies selected file bytes, not production ownership, ACLs, service availability or every dependency. Stop if a block fails; a partial extraction is not a passed recovery.
Restore an application and its database
A working recovery needs more than a web-directory archive. Before changing a target, identify a trusted recovery point and assemble the following inventory. If a required part is missing, record it as unresolved.
| Recoverable part | What to record and retrieve |
|---|---|
| Application and runtime | Release/commit or image digest, dependency lockfile, runtime version, Ubuntu release and CPU architecture. Rebuild compiled dependencies for the target; an amd64 binary is not an ARM binary. |
| Database and uploads | Engine/version, backup format and consistent data time; all required databases, upload paths and object-storage dependencies. Coordinate database and file capture so records do not refer to missing uploads. |
| Configuration and services | Nginx includes, app settings, systemd units, working directories, users/groups, ownership and required permissions. Include cron, workers and queues in the inventory but keep their external effects disabled during the drill. |
| Secrets and independent access | Backup decryption key, application encryption key, credentials and recovery access from a separately protected location. Record how to retrieve them, never their values in a checklist. |
| Traffic and certificates | Domains, DNS access and records, certificate reissuance or a protected key/certificate copy, firewall rules and external dependencies. These are not supplied by a database dump. |
- Choose the point and isolate the target. Keep the only known-good copy unchanged. Verify checksums against a trusted manifest and confirm that you can retrieve the copy without the old VPS. Prevent the restored app from sending mail, payments, webhooks or queued jobs.
- Recreate the environment. Install recorded compatible runtime/database versions and required extensions. Prepare users, paths and service definitions on the intended replacement machine. Check available space for the archive, extracted files, database and temporary import work.
- Restore data using the right method. Import into an isolated database and recover the matching application release and uploads. Resolve every import error before starting the application.
- Supply secrets and reconcile permissions. Restore the expected application key through its protected channel. Assign deliberate file and database permissions; do not solve errors with world-writable files.
- Validate before exposing traffic. Validate the assembled Nginx configuration on the target, start only the required services and check real application operations. A successful extraction is only one completed stage.
Choose a consistent database route
- PostgreSQL logical dump
- pg_dump captures one database consistently; roles and tablespaces are separate cluster objects. Record extensions and privileges. Use compatible dump/restore tools: pg_dump cannot read a newer server major version than its own, and loading into an older server version is not guaranteed. A logical dump alone is not continuous point-in-time recovery.
- SQLite
- Use the Online Backup API (including a tool that uses it) or VACUUM INTO for a consistent copy. For a cold file copy, stop all writers and follow the application’s documented procedure, including journal/WAL state. Copying only an active .db file can omit committed data. Restore to a separate path before pointing the app at it.
- MySQL / MariaDB
- Use the engine’s supported dump tool and record its version, engines, routines/events, users/grants and other required objects. --single-transaction is suitable for InnoDB, not a consistency guarantee for MyISAM/MEMORY or concurrent schema changes. Coordinate other tables and uploads with a supported lock/quiesce procedure. Inspect the dump’s database-switch/drop statements before importing on an isolated target; do not assume every SQL file stays inside the database named on the command line.
PostgreSQL: a separate-database restore example
This Bash example illustrates the small custom-format logical-dump route. Run it only on your isolated target after confirming the PostgreSQL connection settings and installing matching extensions. The existing restoreapp role must have the required permissions; your restore account must be able to create a database and assume that role. Authenticate through your configured client credentials, not a password pasted into the command.
First verify the trusted database.dump checksum and inspect its contents. A dump can execute SQL supplied by its source; listing it is not a security review. The new name restore_demo_check must not exist. This example does not reproduce source ownership or grants: apply the intended target grants separately.
(
set -euo pipefail
test -s ./database.dump
pg_restore --list ./database.dump
createdb --template=template0 --owner=restoreapp restore_demo_check
pg_restore --exit-on-error --single-transaction \
--no-owner --no-acl --role=restoreapp \
--dbname=restore_demo_check ./database.dump
)
If database creation fails, this block stops before import. If import fails, keep the target isolated, inspect the error and do not continue to application startup. The empty target database may remain; do not blindly rerun against it or add --clean. Review the failed target separately. Single-transaction import is appropriate for this small example, not a universal large-database strategy.
Do not use the file exercise above to copy a running PostgreSQL/MySQL data directory. Physical backups, full-disk images and point-in-time recovery require their own engine/provider procedures. For OS choice, use the Ubuntu compatibility guide; for a basic web server, use the deployment walkthrough.
Check the service before returning traffic
Test on the isolated replacement with the intended application release and restored data. Keep the old source and the known-good backup intact. Record each result; a process marked active or a generic HTTP 200 is not proof that the application works.
- Configuration and startup: validate the target Nginx configuration, required services, runtime and database connections. Read startup errors rather than repeatedly restarting.
- Data and permissions: compare the expected recovery point, representative records, row counts/checksums where available, uploads and required file ownership. Verify login and that unauthorized operations are refused.
- Application behavior: read existing data, create and retrieve a harmless test record, and exercise an upload if the application uses uploads. Check workers with a controlled test job; block outbound mail, payments and real webhooks while testing.
- Persistence: after a planned restart or reboot of the isolated target, verify service startup and the test record again. Do not reboot the running production server to copy this check.
- Hostname and HTTPS: verify the intended domain, certificate and actual response body against the replacement before changing DNS. Check IPv4/IPv6 records and proxy/CDN routing separately.
- Cutover: agree on a final consistent copy and a write-freeze/synchronization plan. Switch only after the checks pass; monitor real requests and jobs. Enable scheduled work deliberately on one intended environment.
Check the replacement’s HTTPS endpoint before changing DNS
From a machine that can reach the replacement, substitute your real hostname and replacement IP below. example.com and 203.0.113.10 are documentation placeholders. The hostname stays in the HTTPS request and certificate check; --resolve selects the destination IP for this request. Use a harmless known route and inspect its body. Do not add -k to hide a certificate failure.
curl --noproxy '*' --fail --show-error --silent \
--connect-timeout 10 --max-time 20 \
--resolve example.com:443:203.0.113.10 \
https://example.com/Without --location, a redirect is not followed automatically. Inspect any redirect and test its target deliberately. This command checks a direct endpoint, not every CDN or client path; test the normal public path after cutover too.
When a recovery check fails
| Symptom | Check first | Next action |
|---|---|---|
| Checksum mismatch / incomplete archive | File size, transfer status, trusted expected checksum and selected restore point. | Stop extraction/import; retrieve the known-good copy again. Do not replace the expected checksum with the damaged file’s hash. |
| No space left / cannot create files | Target filesystem free bytes and inodes, mount state, quota, room for extracted data and import temporary files. | Provide sufficient target capacity; keep the last usable backup. Do not delete unexplained data to make the error disappear. |
| Unsupported dump / missing role or extension | Engine and client major versions, archive type, target roles, extensions and import log. | Use the correct compatible tools and recreate required dependencies. Repeat on a reviewed isolated target, not by suppressing errors. |
| Credentials cannot be decrypted | Application version and the original application encryption key from its protected recovery location. | Retrieve the required key. Creating a new key does not decrypt old secrets; keep traffic and integrations disabled. |
| Permission denied / 502 / service exits | Target service user, working directory, path ownership, database grants, upstream socket/port and logs. | Correct the specific mismatch, validate the target configuration and retry. Do not use chmod 777 or disable authentication. |
| HTTP 200 but missing or old data | Response body, application database endpoint, selected recovery point, cache and upload storage. | Confirm the app reads the intended restored database and matching uploads. Do not mark recovery passed from status code alone. |
| Duplicate mail, jobs or webhooks | Schedulers/workers running on both old and new environments; restored pending jobs. | Pause unintended consumers, inspect job state and apply the application’s replay/idempotency procedure before enabling them. |
| Wrong site / certificate after switching | A/AAAA, proxy/CDN origin, Host/SNI, vhost configuration, certificate names and cache. | Correct the intended route and certificate; check both endpoint and public path. Never bypass TLS verification as a permanent fix. |
Keep a verified, recent recovery point
RPO is your tolerated time window of data loss. RTO is your target time to recover the service. Choose them for the application; a daily schedule and a fast extraction do not establish either outcome.
- Capture consistently. Record the data point and application release; coordinate database and uploads.
- Verify the captured set. Check completion, expected inventory and integrity. Treat a failed or incomplete job as failed, even if a file was created.
- Keep an independent copy. Confirm it is retrievable without the source machine. Consider separate access/account dependencies; another folder on the same VPS is not independent storage.
- Confirm the destination. Check that the intended copy arrived, can be read and can be decrypted. Keep the recovery key available through a separately protected channel.
- Mark success and retain history. Only a completed, verified chain produces a successful recovery point. Run retention under a reviewed policy, preserving usable copies; a failed new run must not trigger blind removal of the last good one.
- Monitor and rehearse. Alert on failed jobs, stale verified points and storage problems. Periodically restore representative data and exercise the service.
What a restic integrity check does and does not establish
A normal restic check checks repository structure and metadata; it does not read every stored data pack. --read-data requests a full data read, while a subset check samples only part of it. Full reads take time and can incur storage/transfer charges. Select and record the intended snapshot ID rather than assuming “latest” is the correct recovery point.
Successful repository verification still does not show that the database was captured consistently, that a key is independently retrievable or that the application starts. Restore to a new target and perform the application checks too.
Scheduling, retention and encryption choices
Chain dependent stages on successful completion instead of assuming that an earlier cron job finished before a later one starts. Prevent overlapping runs. With shell pipelines, preserve the actual backup command’s exit status; a successful tee is not backup success.
Choose version history for your recovery needs and capacity. A mirror that propagates deletions is not a history of recovery points. Keep more than one suitable point so a discovered corruption or unwanted change does not leave only the damaged state.
Encrypt sensitive backups and protect the repository credentials. Keep the decryption key recoverable when the original server or account is unavailable; encryption cannot help recovery when the only key is lost with the server. A provider snapshot, a remote repository and a copy in a separately controlled account have different failure dependencies.
For the full recovery interval, record access recovery, target provisioning, environment setup, transfer, import, validation and any DNS/TLS work. Compare that defined interval with your RTO; do not substitute only the archive extraction time. Check hosting backup and recovery conditions separately when choosing storage or a provider.
What we actually restored
These are saved results from specific recovery drills, not a provider speed ranking or a promise about your application.
Ubuntu application + PostgreSQL: clean replacement VM
On 9 October 2026, we recovered a synthetic Python application and PostgreSQL database into a separate clean Ubuntu 24.04.5 LTS VM. The source VM was powered off before recovery; the two guests had different machine IDs. The target used Python 3.12.3, PostgreSQL 16.15 and Nginx 1.24.
- Data: 1,001 database rows and 3 uploaded files recovered; the data, application and upload hashes and expected file permissions matched.
- Negative checks: a deliberately corrupted archive was rejected; a source write made after the backup was absent, as expected for that recovery point.
- Working application: 11 HTTP checks included reads, uploaded files, refusal of an unauthorized write and a successful new transaction.
- After reboot: the new transaction (ID 1002) remained and the services started. The application secret was supplied separately, not included in the archive.
Read the Ubuntu + PostgreSQL verification record (JSON) →
Environment, timings and boundaries of this test
The local QEMU 11.1 TCG guest ran through software emulation on Windows with 2 vCPU, 1536 MiB RAM and an 8 GiB virtual disk. It was not a VDSina server or a production database.
481.01 seconds covered package installation, backup transfer, restore and initial checks on an already booted clean target. 42.67 seconds covered the restore script alone. Neither includes a full real-world outage, provisioning or DNS/TLS switching; neither predicts your VPS speed or RTO.
This test rebuilt an environment from a clean OS. It did not restore a provider disk snapshot, exercise real external integrations or measure load capacity. A passed synthetic example is evidence for this procedure and fixture, not every application.
Additional n8n check: SQLite and PostgreSQL
A saved CI run from our separate n8n project on 8 October 2026 tested n8n 2.42.5 on an Ubuntu 24.04.5 runner with a two-node Manual Trigger → No Operation workflow.
- SQLite: stopped writers, backed up the .n8n directory, restored a fresh volume, checked HTTP health, compared the exported workflow structure and successfully executed it through the CLI.
- PostgreSQL: restored a custom-format dump into a fresh database plus a fresh .n8n volume, supplied the test encryption key, compared the exported structure and successfully executed the workflow through the CLI. This branch did not test a persistent n8n HTTP service.
This does not validate real credentials, user permissions, webhooks, queues/Redis, external services or production capacity. The successful workflow does not exercise Python tasks. It is a historical smoke test with the recorded versions, not a recommendation to choose those versions today.
Static-file recovery: a separate, smaller claim
On 9 October 2026, we restored the 1 October static release of this website into an empty local directory. All 1,085 file hashes matched; an intentionally changed file failed verification, and six local HTTP responses matched their expected hashes.
The test used Windows and a loopback Python HTTP server. It did not recreate Ubuntu, Nginx, accounts, certificates or a database. Its file-operation times are not server recovery times.
Save your recovery plan and results
Record one copy, one replacement target and the checks you actually performed. The tool does not connect to your VPS. It saves only when you choose Save; exports and imports are processed in your browser. Keep passwords, tokens, private keys and confidential logs out of every field.
Changing the copy, scope, target or inventory resets previous check marks. Saved/imported marks describe earlier observations; verify they still apply. The application and machine-image checklists include a database-backed application; use the selected-file scope for a file-only drill.
Questions and answers
Can I restore the application at a different VPS provider?
Often you can rebuild a compatible environment and recover portable application data, provided you have the release, consistent database backup, uploads, configuration and necessary keys. Check architecture and version compatibility. A provider snapshot may only restore within that provider; its existence does not prove exportability.
Can a new encryption key unlock an old backup or application?
No. Retrieve the key required by that encrypted backup or application data through your protected recovery process. Generating a replacement key does not decrypt data encrypted with the lost key.
Can I remove the original website to prove the backup works?
You do not need to delete it. Restoring into a separate empty directory and checking the manifest tests the file copy without creating an outage. Test the complete service on a separate replacement environment.
Does a successful checksum mean the whole website is backed up?
It confirms the files listed in this manifest. It cannot discover an omitted asset, database, secret or DNS setting. Maintain an inventory and exercise the website's actual functions after service restoration.
Sources and verification
Reference material: Ubuntu Server backup documentation; GNU tar and Coreutils manuals; OpenSSH scp manual; PostgreSQL 16 pg_dump and pg_restore documentation; SQLite Online Backup API; MySQL 8.4 mysqldump and MariaDB mariadb-dump manuals; restic repository-check and restore documentation; curl connection-resolution documentation; n8n CLI documentation.
The database and connection-check references were reviewed on 13 October 2026. The published command blocks were checked for Bash syntax; they were not executed against a visitor’s VPS. The earlier fixture exercise used GNU tools in Git Bash on Windows. The separate historical Ubuntu/PostgreSQL, n8n and static-file drills are described in our verification records, with their exact scope and limitations.
Recovery steps must fit the actual engine, application and provider. This guide does not promise recovery of an incomplete copy or guarantee an outage duration.