A production‑ready guide to back up data from a Linux VPS to Amazon S3. Every action clearly states where to run it:
-
🟧 AWS Console — Browser (root/owner privileges)
-
🟦 Admin machine + AWS CLI — Your laptop/management host with admin credentials
-
🟩 VPS (SSH) — The server you’re backing up (no SSH command shown)
Best practice: Do all AWS account administration (buckets, IAM users/policies) via AWS Console or Admin machine, not from the VPS. On the VPS, use a least‑privileged IAM user restricted to one bucket/prefix.
0) Prerequisites
-
AWS account with permissions to create S3 buckets and IAM users.
-
Linux VPS with sudo privileges.
-
Estimated retention & storage class plan (e.g., keep 30 daily copies, move to Glacier Instant Retrieval after 30 days).
1) Create an S3 bucket (Mumbai) — Do not run on the VPS
Option A — AWS Console (Recommended) 🟧
-
Go to S3 → Create bucket.
-
Bucket name:
mycompany-backups-prod
(must be globally unique). -
AWS Region: Asia Pacific (Mumbai) – ap-south-1.
-
Uncheck public access (or leave default Block Public Access = ON), then Create bucket.
Option B — AWS CLI (admin credentials) 🟦
Run on your Admin machine, not the VPS. The backup user on the VPS should not have
s3:CreateBucket
permissions.
# Where to run: 🟦 Admin machine + AWS CLI
aws s3api create-bucket \
--bucket mycompany-backups-prod \
--region ap-south-1 \
--create-bucket-configuration LocationConstraint=ap-south-1
2) Secure the bucket (versioning, encryption, public access) — Admin side
Enable Versioning 🟦
# Where to run: 🟦 Admin machine + AWS CLI
aws s3api put-bucket-versioning \
--bucket mycompany-backups-prod \
--versioning-configuration Status=Enabled
Enforce Default Encryption (SSE-S3) 🟦
# Where to run: 🟦 Admin machine + AWS CLI
aws s3api put-bucket-encryption \
--bucket mycompany-backups-prod \
--server-side-encryption-configuration '{
"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]
}'
Block Public Access 🟦
# Where to run: 🟦 Admin machine + AWS CLI
aws s3api put-public-access-block \
--bucket mycompany-backups-prod \
--public-access-block-configuration '{
"BlockPublicAcls": true,
"IgnorePublicAcls": true,
"BlockPublicPolicy": true,
"RestrictPublicBuckets": true
}'
Lifecycle Policy (cost control + hygiene) 🟦
// Save as lifecycle.json on your Admin machine
{
"Rules": [
{
"ID": "transition-and-expire",
"Status": "Enabled",
"Filter": { "Prefix": "" },
"Transitions": [
{ "Days": 30, "StorageClass": "GLACIER_IR" }
],
"NoncurrentVersionExpiration": { "NoncurrentDays": 180 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}
]
}
# Where to run: 🟦 Admin machine + AWS CLI
aws s3api put-bucket-lifecycle-configuration \
--bucket mycompany-backups-prod \
--lifecycle-configuration file://lifecycle.json
3) Create a least‑privilege IAM user for the VPS — Admin side
Policy: Restrict to a single prefix (example path servers/prod01/*
) 🟦/🟧
// Save as backup-policy.json (Admin machine) or paste in Console → IAM → Policies
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucketWithinPrefix",
"Effect": "Allow",
"Action": [ "s3:ListBucket", "s3:GetBucketLocation" ],
"Resource": "arn:aws:s3:::mycompany-backups-prod",
"Condition": { "StringLike": { "s3:prefix": [ "servers/prod01/*" ] } }
},
{
"Sid": "WriteObjectsWithinPrefix",
"Effect": "Allow",
"Action": [
"s3:PutObject", "s3:GetObject", "s3:DeleteObject",
"s3:ListBucketMultipartUploads", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"
],
"Resource": "arn:aws:s3:::mycompany-backups-prod/servers/prod01/*",
"Condition": { "StringEquals": { "s3:x-amz-server-side-encryption": "AES256" } }
}
]
}
-
Create IAM User (programmatic access).
-
Attach the backup-policy.
-
Generate Access Key ID and Secret Access Key → store securely.
Optional hardening: Add a Bucket Policy that allows access only from your server’s IP using
aws:SourceIp
(skip if your IP changes).
4) Install AWS CLI v2 on the VPS — Run on VPS 🟩
Ubuntu/Debian
# Where to run: 🟩 VPS (SSH)
sudo apt update
sudo apt install -y unzip
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
unzip -q awscliv2.zip
sudo ./aws/install
aws --version
AlmaLinux/Rocky/CentOS
# Where to run: 🟩 VPS (SSH)
sudo dnf -y install unzip
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
unzip -q awscliv2.zip
sudo ./aws/install
aws --version
5) Configure a named AWS profile on the VPS — Run on VPS 🟩
# Where to run: 🟩 VPS (SSH)
aws configure --profile backup
# AWS Access Key ID: <from IAM user>
# AWS Secret Access Key: <from IAM user>
# Default region name: ap-south-1
# Default output format: json
chmod 600 ~/.aws/credentials ~/.aws/config
aws sts get-caller-identity --profile backup
6) Sanity test upload — Run on VPS 🟩
# Where to run: 🟩 VPS (SSH)
echo "hello from $(hostname) at $(date -Iseconds)" > /tmp/test.txt
aws s3 cp /tmp/test.txt \
s3://mycompany-backups-prod/servers/prod01/test.txt \
--profile backup
aws s3 ls s3://mycompany-backups-prod/servers/prod01/ --profile backup
7) Choose your backup method(s) — Run on VPS 🟩
A) Incremental directory sync (web roots, home dirs)
# Where to run: 🟩 VPS (SSH)
aws s3 sync /var/www \
s3://mycompany-backups-prod/servers/prod01/www/ \
--profile backup \
--delete \
--exact-timestamps \
--no-follow-symlinks \
--exclude "*/cache/*" --exclude "*/node_modules/*"
--delete
makes S3 mirror your source (use with care; Versioning enables rollback).
B) Compressed archive (point-in-time)
# Where to run: 🟩 VPS (SSH)
# Archive /etc, /var/www, and any custom paths, then stream to S3
sudo tar --warning=no-file-changed -czf - /etc /var/www \
| aws s3 cp - \
s3://mycompany-backups-prod/servers/prod01/archives/backup-$(date +%F).tar.gz \
--profile backup --no-progress
C) MySQL/MariaDB dump
# Where to run: 🟩 VPS (SSH)
mysqldump --single-transaction --quick --lock-tables=false --all-databases \
| gzip \
| aws s3 cp - \
s3://mycompany-backups-prod/servers/prod01/db/mysql-$(date +%F).sql.gz \
--profile backup --no-progress
D) PostgreSQL dump
# Where to run: 🟩 VPS (SSH)
sudo -u postgres pg_dumpall \
| gzip \
| aws s3 cp - \
s3://mycompany-backups-prod/servers/prod01/db/postgres-$(date +%F).sql.gz \
--profile backup --no-progress
8) Automate with cron — Run on VPS 🟩
Edit the crontab for the user that owns the backups:
# Where to run: 🟩 VPS (SSH) — crontab -e
AWS_PROFILE=backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 01:30 daily — Archive configs + web roots
30 1 * * * tar -czf - /etc /var/www \
| aws s3 cp - s3://mycompany-backups-prod/servers/prod01/archives/backup-$(date +\%F).tar.gz \
--no-progress
# 02:10 daily — MySQL all databases
10 2 * * * mysqldump --single-transaction --quick --lock-tables=false --all-databases \
| gzip \
| aws s3 cp - s3://mycompany-backups-prod/servers/prod01/db/mysql-$(date +\%F).sql.gz \
--no-progress
# 03:00 daily — Incremental sync of /var/www
0 3 * * * aws s3 sync /var/www s3://mycompany-backups-prod/servers/prod01/www/ \
--delete --exact-timestamps --no-follow-symlinks \
--exclude "*/cache/*" --exclude "*/node_modules/*"
Tip: Redirect cron output to a log file or email, or ping a monitoring URL upon success.
9) Verify and restore — Where to run varies
List latest backups 🟩
# Where to run: 🟩 VPS (SSH)
aws s3 ls s3://mycompany-backups-prod/servers/prod01/archives/ --profile backup
aws s3 ls s3://mycompany-backups-prod/servers/prod01/db/ --recursive --human-readable --summarize --profile backup
Restore files to a temp folder 🟩
# Where to run: 🟩 VPS (SSH)
mkdir -p /tmp/restore
aws s3 sync s3://mycompany-backups-prod/servers/prod01/www/ /tmp/restore/www/ --profile backup
Extract a tar archive 🟩
# Where to run: 🟩 VPS (SSH)
aws s3 cp s3://mycompany-backups-prod/servers/prod01/archives/backup-2025-09-07.tar.gz - --profile backup \
| sudo tar -xz -C /tmp/restore
Restore MySQL 🟩
# Where to run: 🟩 VPS (SSH)
aws s3 cp s3://mycompany-backups-prod/servers/prod01/db/mysql-2025-09-07.sql.gz - --profile backup \
| gunzip \
| mysql
Restore PostgreSQL 🟩
# Where to run: 🟩 VPS (SSH)
aws s3 cp s3://mycompany-backups-prod/servers/prod01/db/postgres-2025-09-07.sql.gz - --profile backup \
| gunzip \
| psql -U postgres
Need an older copy? Use S3 Versioning to fetch a previous object version.
10) Security & cost checklist
-
Use a dedicated IAM user per VPS; scope to
servers/<hostname>/*
prefix. -
Rotate access keys regularly;
chmod 600
AWS creds on the VPS. -
Enable Default Encryption, Block Public Access, Versioning, Lifecycle.
-
Prefer ap-south-1 (Mumbai) for low latency and India data‑residency needs.
-
Exclude caches/builds to reduce size:
--exclude "*/cache/*" --exclude "*/node_modules/*"
. -
Periodically test restores (table import, tar extract) — backups aren’t real until you restore.
11) Full backup script — Run on VPS 🟩
Save as /usr/local/bin/s3-backup.sh
, then chmod +x /usr/local/bin/s3-backup.sh
.
#!/usr/bin/env bash
set -Eeuo pipefail
PROFILE="backup"
BUCKET="mycompany-backups-prod"
PREFIX="servers/prod01"
DATE="$(date +%F)"
log() { printf '[%s] %s\n' "$(date -Iseconds)" "$*"; }
log "Starting backup"
# MySQL dump (if present)
if command -v mysqldump >/dev/null 2>&1; then
log "Dumping MySQL"
mysqldump --single-transaction --quick --lock-tables=false --all-databases \
| gzip \
| aws s3 cp - "s3://${BUCKET}/${PREFIX}/db/mysql-${DATE}.sql.gz" --profile "$PROFILE" --no-progress
fi
# PostgreSQL dump (if present)
if command -v pg_dumpall >/dev/null 2>&1; then
log "Dumping PostgreSQL"
sudo -u postgres pg_dumpall \
| gzip \
| aws s3 cp - "s3://${BUCKET}/${PREFIX}/db/postgres-${DATE}.sql.gz" --profile "$PROFILE" --no-progress
fi
# Archive configs + web roots
log "Archiving /etc and /var/www"
sudo tar --warning=no-file-changed -czf - /etc /var/www \
| aws s3 cp - "s3://${BUCKET}/${PREFIX}/archives/backup-${DATE}.tar.gz" --profile "$PROFILE" --no-progress
# Incremental sync of web roots
log "Syncing /var/www → S3"
aws s3 sync /var/www "s3://${BUCKET}/${PREFIX}/www/" \
--profile "$PROFILE" \
--delete --exact-timestamps --no-follow-symlinks \
--exclude "*/cache/*" --exclude "*/node_modules/*" \
--no-progress
log "Backup complete"
Cron example (daily 02:00) 🟩
0 2 * * * /usr/local/bin/s3-backup.sh >> /var/log/s3-backup.log 2>&1
12) Optional advanced tools — Run on VPS 🟩
rclone (fast multi‑threaded sync, fine‑grained control)
# Where to run: 🟩 VPS (SSH)
rclone config # remote name: s3backup → Storage: s3 → Provider: AWS → Region: ap-south-1
rclone sync /var/www s3backup:mycompany-backups-prod/servers/prod01/www \
-P --s3-storage-class STANDARD_IA --transfers 8 --checkers 16 \
--exclude "*/cache/*" --exclude "*/node_modules/*"
restic (encrypted, deduplicated snapshots)
# Where to run: 🟩 VPS (SSH)
export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...
export RESTIC_REPOSITORY="s3:https://s3.ap-south-1.amazonaws.com/mycompany-backups-prod/servers/prod01/restic"
export RESTIC_PASSWORD="use-a-strong-password"
restic init
restic backup /etc /var/www --exclude='**/cache/**' --exclude='**/node_modules/**'
restic snapshots
restic restore latest --target /tmp/restore
With restic, manage retention using:
restic forget --keep-daily 7 --keep-weekly 4 --prune
.
13) Troubleshooting — Quick map
-
AccessDenied: The VPS profile likely lacks permissions or wrong prefix; confirm policy matches
servers/prod01/*
and regionap-south-1
. -
Bucket already exists: Choose a globally unique name.
-
Clock skew errors: Ensure VPS time sync (chrony/systemd-timesyncd).
-
Slow uploads: Exclude caches, consider rclone with parallelism, schedule off‑peak.
-
Large dumps: MySQL
--single-transaction
(InnoDB), ensure adequate temp space. -
Multipart leftovers: Lifecycle rule cleans after 7 days.
That’s it
You now have a clearly separated flow: Admin creates and secures AWS resources; the VPS only uploads backups using a locked‑down IAM user. If you want, we can tailor excludes/paths for cPanel, DirectAdmin, Webuzo, or custom app stacks.
FAQ — VPS → Amazon S3 Backups
1) Where do I run each command?
-
AWS bucket/IAM setup: Admin workstation (AWS Console or admin host with AWS CLI).
-
Copy/backup commands: On the VPS that holds the data (over SSH).
-
Never keep bucket-creation rights on a production VPS after setup.
2) What’s the fastest way to copy a folder to S3 right now?
aws s3 cp /var/www \
s3://<bucket>/servers/<host>/www/ \
--recursive --profile backup --no-progress \
--exclude "*/cache/*" --exclude "*/node_modules/*"
For ongoing mirroring use sync
:
aws s3 sync /var/www s3://<bucket>/servers/<host>/www/ \
--delete --exact-timestamps --no-follow-symlinks \
--exclude "*/cache/*" --exclude "*/node_modules/*" \
--profile backup --no-progress
3) What’s the difference between cp --recursive
and sync
?
-
cp --recursive
: Uploads everything; doesn’t delete on S3. -
sync
: Makes S3 match the source; removes objects on S3 that no longer exist locally when--delete
is used. Enable bucket Versioning to rollback.
4) Which files should I exclude on web servers?
Typical excludes:
--exclude "*/cache/*" --exclude "*/tmp/*" --exclude "*/.git/*" \
--exclude "*/node_modules/*" --exclude "*/vendor/*" (if you can rebuild)
Panels:
-
cPanel: also exclude
*/.cpcache/*
, logs you don’t need. -
DirectAdmin/Webuzo: exclude per-app caches, build artifacts, and session files.
5) How do I back up databases directly to S3 (no temp files)?
MySQL/MariaDB (all DBs):
mysqldump --single-transaction --quick --lock-tables=false --all-databases \
| gzip \
| aws s3 cp - \
s3://<bucket>/servers/<host>/db/mysql-$(date +%F).sql.gz \
--profile backup --no-progress
PostgreSQL (cluster):
sudo -u postgres pg_dumpall \
| gzip \
| aws s3 cp - \
s3://<bucket>/servers/<host>/db/postgres-$(date +%F).sql.gz \
--profile backup --no-progress
6) How do I make a point-in-time snapshot of configs + sites?
sudo tar --warning=no-file-changed -czf - /etc /var/www \
| aws s3 cp - s3://<bucket>/servers/<host>/archives/backup-$(date +%F).tar.gz \
--profile backup --no-progress
7) How do I schedule daily backups?
Edit crontab:
AWS_PROFILE=backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 01:30 — Archive configs + sites
30 1 * * * tar -czf - /etc /var/www \
| aws s3 cp - s3://<bucket>/servers/<host>/archives/backup-$(date +\%F).tar.gz --no-progress
# 02:10 — MySQL
10 2 * * * mysqldump --single-transaction --quick --lock-tables=false --all-databases \
| gzip | aws s3 cp - s3://<bucket>/servers/<host>/db/mysql-$(date +\%F).sql.gz --no-progress
# 03:00 — Incremental sync
0 3 * * * aws s3 sync /var/www s3://<bucket>/servers/<host>/www/ \
--delete --exact-timestamps --no-follow-symlinks \
--exclude "*/cache/*" --exclude "*/node_modules/*" \
--no-progress
8) Do I need --sse AES256
if the bucket has default encryption?
-
If your IAM policy requires the SSE header, add
--sse AES256
on uploads. -
If not required, bucket-level default encryption (SSE-S3) is sufficient, and you can omit it.
9) Should I use SSE-KMS instead of SSE-S3?
Use SSE-KMS when you need customer-managed keys, auditability, and granular key policies:
aws s3 cp file.tar.gz s3://<bucket>/path/ \
--sse aws:kms --sse-kms-key-id alias/backup-key --profile backup
SSE-KMS adds minor cost and requires KMS permissions.
10) How do I verify backups completed and are restorable?
-
List with totals:
aws s3 ls s3://<bucket>/servers/<host>/ --recursive --human-readable --summarize --profile backup
-
Spot-restore to
/tmp/restore
:
mkdir -p /tmp/restore && \
aws s3 sync s3://<bucket>/servers/<host>/www/ /tmp/restore/www/ --profile backup
-
Test DB import on a staging DB, or at least
gunzip -t
on dump files.
11) How can I throttle bandwidth?
aws s3
has no native throttle. Pipe through pv
:
tar -czf - /var/www | pv -L 10m \
| aws s3 cp - s3://<bucket>/servers/<host>/archives/site.tgz --profile backup
For powerful throttling/parallelism, use rclone:
rclone sync /var/www s3:<bucket>/servers/<host>/www \
-P --transfers 8 --checkers 16 --bwlimit 10M \
--exclude "*/cache/*" --exclude "*/node_modules/*"
12) How do I resume or retry large uploads?
-
The AWS CLI automatically uses multipart uploads for large files and retries.
-
If a job was interrupted, re-run the same command; it will only send missing parts.
-
A lifecycle rule should abort incomplete multipart uploads after a few days.
13) Why am I getting AccessDenied
?
-
The VPS IAM user likely lacks permission for the bucket/prefix or KMS key.
-
Check caller:
aws sts get-caller-identity --profile backup
-
Review bucket policy, IAM policy, region, and required SSE headers.
14) Why do I see InvalidLocationConstraint
on bucket creation?
You passed the wrong region or omitted the create-bucket configuration. For ap-south-1:
--region ap-south-1 \
--create-bucket-configuration LocationConstraint=ap-south-1
(Only us-east-1
doesn’t need the LocationConstraint.)
15) How do I rotate access keys without downtime?
-
Create a new access key for the VPS IAM user.
-
Add it to
~/.aws/credentials
as the same profile. -
Test a command.
-
Disable the old key.
-
Remove the old key after a cooling period.
16) Can I back up straight to Glacier?
You upload to S3 and transition via Lifecycle to GLACIER/DEEP_ARCHIVE, or with rclone
/S3 API you can set storage class on upload:
aws s3 cp file.tgz s3://<bucket>/archive/ --storage-class GLACIER_IR --profile backup
(Choose GLACIER_IR/DEEP_ARCHIVE based on retrieval time/cost.)
17) What storage class should I use?
-
STANDARD: Hot data, frequent access.
-
STANDARD_IA / ONEZONE_IA: Infrequent, lower cost.
-
INTELLIGENT_TIERING: Auto-optimize for changing patterns.
-
GLACIER_IR / GLACIER / DEEP_ARCHIVE: Long-term, cheapest storage with slower retrieval.
18) How do I keep costs predictable?
-
Enable Versioning + Lifecycle (transition older to IA/Glacier, expire old versions).
-
Exclude caches/builds; compress before upload; prefer
sync
over repeated full uploads. -
Keep objects >128KB so they benefit from tiering rules.
19) How do I do per-file integrity checks?
-
Generate and upload sidecar checksums:
sha256sum /var/www/file.jpg > /var/www/file.jpg.sha256
aws s3 cp /var/www/file.jpg.sha256 s3://<bucket>/path/ --profile backup
-
For end-to-end verification and deduplication, consider restic (built-in verification).
20) Can I use KMS with bucket default encryption?
Yes. Set the bucket default to SSE-KMS (via Console or CLI), and ensure the VPS IAM user has kms:Encrypt
(and related) permissions for that key. Uploads then encrypt automatically without --sse
flags.
21) How do I restore a single file/version I deleted?
-
If Versioning is enabled: list versions, then copy the required
VersionId
back.
aws s3api list-object-versions --bucket <bucket> --prefix servers/<host>/www/path/file.jpg
aws s3api get-object --bucket <bucket> --key servers/<host>/www/path/file.jpg \
--version-id <VersionId> /tmp/restore/file.jpg
22) What about compliance/data residency for India?
-
Use ap-south-1 (Mumbai) for data locality.
-
Document retention (DPDP, contractual policies).
-
Enable Block Public Access, enforce encryption, and maintain audit trails for key/bucket policy changes.
23) Should I use cron or systemd timers?
-
Cron is simple and widely used.
-
systemd timers add robust logging and dependency management. Either is fine; standardize across your fleet.
24) Can I use temporary credentials instead of long-lived keys?
-
On EC2, prefer Instance Profiles (IAM Roles) with IMDSv2—no static keys on disk.
-
On non-EC2 VPS (Hetzner, etc.), you can broker STS short-lived creds via a secure process, but it’s more complex. Most use scoped IAM users with rotation.
25) How do I run the whole backup in one script?
Use the consolidated script from the main article (e.g., /usr/local/bin/s3-backup.sh
) and schedule it with cron. Keep logs in /var/log/s3-backup.log
and alert on failures.
26) How do I migrate or duplicate backups to another bucket/account?
-
Server-side copy (no re-upload from VPS):
aws s3 sync s3://<src-bucket>/servers/<host>/ s3://<dst-bucket>/servers/<host>/ \
--source-region ap-south-1 --region ap-south-1
-
For cross-account, set a bucket policy/role trust and use
--profile
.
27) How do I get alerted if backups fail?
-
Send cron output to mail/webhook.
-
Ping a health check URL (e.g., at job end).
-
Optionally, emit CloudWatch metrics/logs if running in AWS; or use Prometheus node exporter + alert rules.
28) What if the VPS clock is off and uploads fail with signature errors?
Enable time sync (chrony or systemd-timesyncd). Clock skew breaks AWS request signing.
29) How big can a single object be?
Up to 5 TB. For very large files, AWS CLI uses multipart uploads automatically.
30) How often should I test restores?
At least monthly. Practice a full flow: fetch an archive, extract to a staging path, and verify application boots/DB imports.