VPS → Amazon S3 Backups: Step‑by‑Step Print

  • 0

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) 🟧

  1. Go to S3 → Create bucket.

  2. Bucket name: mycompany-backups-prod (must be globally unique).

  3. AWS Region: Asia Pacific (Mumbai) – ap-south-1.

  4. 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 region ap-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?

  1. Create a new access key for the VPS IAM user.

  2. Add it to ~/.aws/credentials as the same profile.

  3. Test a command.

  4. Disable the old key.

  5. 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.


Was this answer helpful?

« Back