AWS S3 Backups: How to Automate Server Backups to Amazon S3

Open Source, Security, Tips & Tricks, and Tutorials

AWS S3 Backups: How to Automate Server Backups to Amazon S3

Losing server data to hardware failure, accidental deletion, or a ransomware attack can shut down a business overnight. A reliable backup strategy is the safety net every production system needs — and Amazon S3 is one of the best places to store those backups.

This guide walks through a complete AWS S3 backup strategy for your servers: automated scripts for files and databases, cron scheduling, verification, lifecycle policies for cost control, backing up data that already lives in S3, and how to deploy and manage your backup scripts using DeployHQ.


Why Amazon S3 for Server Backups?

Amazon S3 (Simple Storage Service) is purpose-built for durable, scalable storage:

  • 99.999999999% durability (11 nines) — your data is replicated across multiple facilities
  • Cost-effective — pay only for what you store, with tiered pricing for infrequent access and archival
  • Versioning — recover previous versions of any object
  • Encryption — server-side encryption (SSE-S3, SSE-KMS) protects data at rest
  • Lifecycle policies — automatically transition or delete objects based on age
  • Cross-region replication — replicate backups to a second AWS region for disaster recovery

For teams that already deploy web applications with an automated deployment tool, S3 integrates naturally into your existing workflow — you can manage backup scripts alongside your application code and deploy them automatically. The same DeployHQ S3 connector that ships your backup scripts can also deploy a static site or built assets directly from Git to an S3 bucket.


S3 Backup Strategy Overview

Before writing a single script, anchor your plan to two numbers that professional operators use to size every backup system:

  • RPO (Recovery Point Objective) — how much data you can afford to lose, measured in time. An RPO of 6 hours means your backups must run at least every 6 hours.
  • RTO (Recovery Time Objective) — how long you can afford to be down while restoring. A tight RTO pushes you toward keeping recent backups in fast-retrieval storage classes rather than deep archive.

Then apply the 3-2-1 rule, the backup industry's baseline: keep 3 copies of your data, on 2 different media/storage types, with 1 copy off-site. A production server's local disk is copy one; an S3 bucket in your primary region is copy two on a different medium; cross-region replication or a second S3-compatible provider gives you the off-site third copy. S3 makes the 2 and 1 almost free to satisfy.

With RPO/RTO and 3-2-1 defined, a solid backup strategy answers four questions:

  1. What gets backed up? (files, databases, configuration)
  2. How often? (frequency driven by your RPO)
  3. How long are backups retained? (retention policy)
  4. How do you verify backups work? (automated testing)

Here's a typical strategy for a web application server:

Component Frequency Retention Storage Class
Application files Daily 30 days S3 Standard → IA after 30d
Database Every 6 hours 30 days S3 Standard → IA after 30d
Configuration files Daily 90 days S3 Standard
Full system snapshot Weekly 90 days S3 Standard → Glacier after 30d

Prerequisites

Install AWS CLI v2

The modern way to install the AWS CLI on Ubuntu/Debian:

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

If you're comfortable at the shell, our roundup of essential Linux server commands covers the cron, tar, and permissions tooling this guide relies on.

Create a Dedicated IAM User

Create an IAM user with only the permissions needed for backups — never use root credentials:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::your-backup-bucket",
        "arn:aws:s3:::your-backup-bucket/*"
      ]
    }
  ]
}

Note: The "Version": "2012-10-17" field is the IAM policy language version — it is the current, correct value AWS still requires today, not a date you need to update.

Configure the CLI with the IAM user credentials:

aws configure
# Enter your Access Key ID, Secret Access Key, region, and output format

Create an Encrypted S3 Bucket

# Create the bucket
aws s3 mb s3://your-backup-bucket --region eu-west-1

# Enable default encryption
aws s3api put-bucket-encryption \
    --bucket your-backup-bucket \
    --server-side-encryption-configuration '{
        "Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
    }'

# Enable versioning
aws s3api put-bucket-versioning \
    --bucket your-backup-bucket \
    --versioning-configuration Status=Enabled

# Block public access
aws s3api put-public-access-block \
    --bucket your-backup-bucket \
    --public-access-block-configuration \
        BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Backup Scripts

File Backup Script

Create backup-files.sh:

#!/bin/bash
set -euo pipefail

# Configuration
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_DIR="/var/www"
BACKUP_NAME="files_${TIMESTAMP}.tar.gz"
S3_BUCKET="your-backup-bucket"
S3_PREFIX="backups/files"
TMP_DIR="/tmp/backups"

# Ensure temp directory exists
mkdir -p "$TMP_DIR"

echo "[$(date)] Starting file backup..."

# Create compressed archive
tar -czf "${TMP_DIR}/${BACKUP_NAME}" "$BACKUP_DIR" 2>/dev/null

# Upload to S3 with server-side encryption
aws s3 cp "${TMP_DIR}/${BACKUP_NAME}" "s3://${S3_BUCKET}/${S3_PREFIX}/${BACKUP_NAME}" \
    --sse AES256 \
    --storage-class STANDARD

# Clean up local file
rm -f "${TMP_DIR}/${BACKUP_NAME}"

echo "[$(date)] File backup uploaded: s3://${S3_BUCKET}/${S3_PREFIX}/${BACKUP_NAME}"

Database Backup Script

Create backup-db.sh for MySQL/MariaDB:

#!/bin/bash
set -euo pipefail

# Configuration
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
DB_NAME="your_database"
BACKUP_NAME="db_${DB_NAME}_${TIMESTAMP}.sql.gz"
S3_BUCKET="your-backup-bucket"
S3_PREFIX="backups/database"
TMP_DIR="/tmp/backups"

mkdir -p "$TMP_DIR"

echo "[$(date)] Starting database backup for ${DB_NAME}..."

# Dump and compress in one step
# Uses ~/.my.cnf for credentials (never hardcode passwords in scripts)
mysqldump --defaults-file=~/.my.cnf \
    --single-transaction \
    --routines \
    --triggers \
    "$DB_NAME" | gzip > "${TMP_DIR}/${BACKUP_NAME}"

# Upload to S3
aws s3 cp "${TMP_DIR}/${BACKUP_NAME}" "s3://${S3_BUCKET}/${S3_PREFIX}/${BACKUP_NAME}" \
    --sse AES256

# Clean up
rm -f "${TMP_DIR}/${BACKUP_NAME}"

echo "[$(date)] Database backup uploaded: s3://${S3_BUCKET}/${S3_PREFIX}/${BACKUP_NAME}"

Important: Store database credentials in ~/.my.cnf with restricted permissions, not in the script:

# ~/.my.cnf
[mysqldump]
user=backup_user
password=your_secure_password
chmod 600 ~/.my.cnf

The --single-transaction flag gives you a consistent snapshot of InnoDB tables without locking writes — critical for backing up a live database. If you're still deciding on a database engine for a new project, our comparison of choosing the right database walks through the operational tradeoffs, including backup behaviour.

PostgreSQL Backup Script

For PostgreSQL databases, create backup-pg.sh:

#!/bin/bash
set -euo pipefail

TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
DB_NAME="your_database"
BACKUP_NAME="pg_${DB_NAME}_${TIMESTAMP}.dump"
S3_BUCKET="your-backup-bucket"
S3_PREFIX="backups/database"
TMP_DIR="/tmp/backups"

mkdir -p "$TMP_DIR"

echo "[$(date)] Starting PostgreSQL backup for ${DB_NAME}..."

# Uses ~/.pgpass for credentials.
# pg_dump's custom format (-Fc) is already compressed, so no separate gzip step is needed.
pg_dump --format=custom "$DB_NAME" > "${TMP_DIR}/${BACKUP_NAME}"

aws s3 cp "${TMP_DIR}/${BACKUP_NAME}" "s3://${S3_BUCKET}/${S3_PREFIX}/${BACKUP_NAME}" \
    --sse AES256

rm -f "${TMP_DIR}/${BACKUP_NAME}"

echo "[$(date)] PostgreSQL backup uploaded: s3://${S3_BUCKET}/${S3_PREFIX}/${BACKUP_NAME}"

Scheduling Backups with Cron

Make the scripts executable and schedule them:

chmod +x backup-files.sh backup-db.sh

Edit your crontab (crontab -e):

# File backup — daily at 2:00 AM
0 2 * * * /opt/backups/backup-files.sh >> /var/log/backup-files.log 2>&1

# Database backup — every 6 hours
0 */6 * * * /opt/backups/backup-db.sh >> /var/log/backup-db.log 2>&1

# Backup verification — daily at 4:00 AM
0 4 * * * /opt/backups/verify-backup.sh >> /var/log/backup-verify.log 2>&1

Your cron frequency should map directly back to the RPO you set earlier: a 6-hour database schedule means your worst-case data loss is roughly six hours of writes.


Backup Verification

Backups you never test are backups you can't trust. Create verify-backup.sh:

#!/bin/bash
set -euo pipefail

S3_BUCKET="your-backup-bucket"
ALERT_EMAIL="admin@example.com"
MAX_AGE_HOURS=24

echo "[$(date)] Starting backup verification..."

# Check file backups
LATEST_FILE=$(aws s3 ls "s3://${S3_BUCKET}/backups/files/" --recursive | sort | tail -n 1)
if [[ -z "$LATEST_FILE" ]]; then
    echo "ALERT: No file backups found!" | mail -s "Backup Alert" "$ALERT_EMAIL"
    exit 1
fi

# Check the timestamp of the latest backup
BACKUP_DATE=$(echo "$LATEST_FILE" | awk '{print $1" "$2}')
BACKUP_EPOCH=$(date -d "$BACKUP_DATE" +%s)
CURRENT_EPOCH=$(date +%s)
AGE_HOURS=$(( (CURRENT_EPOCH - BACKUP_EPOCH) / 3600 ))

if [[ $AGE_HOURS -gt $MAX_AGE_HOURS ]]; then
    echo "ALERT: Latest file backup is ${AGE_HOURS}h old (max: ${MAX_AGE_HOURS}h)" | \
        mail -s "Backup Alert: Stale Backup" "$ALERT_EMAIL"
    exit 1
fi

# Check database backups
LATEST_DB=$(aws s3 ls "s3://${S3_BUCKET}/backups/database/" --recursive | sort | tail -n 1)
if [[ -z "$LATEST_DB" ]]; then
    echo "ALERT: No database backups found!" | mail -s "Backup Alert" "$ALERT_EMAIL"
    exit 1
fi

# Verify file integrity by downloading and testing
LATEST_KEY=$(echo "$LATEST_FILE" | awk '{print $4}')
aws s3 cp "s3://${S3_BUCKET}/${LATEST_KEY}" /tmp/verify_backup.tar.gz --quiet
tar -tzf /tmp/verify_backup.tar.gz > /dev/null 2>&1
RESULT=$?
rm -f /tmp/verify_backup.tar.gz

if [[ $RESULT -ne 0 ]]; then
    echo "ALERT: Latest backup is corrupted!" | mail -s "Backup Alert: Corrupt" "$ALERT_EMAIL"
    exit 1
fi

echo "[$(date)] All backup checks passed"

S3 Lifecycle Policy for Cost Control

Move older backups to cheaper storage classes automatically. Create lifecycle.json:

{
  "Rules": [
    {
      "ID": "BackupLifecycle",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "backups/"
      },
      "Transitions": [
        {
          "Days": 30,
          "StorageClass": "STANDARD_IA"
        },
        {
          "Days": 90,
          "StorageClass": "GLACIER"
        }
      ],
      "Expiration": {
        "Days": 365
      },
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 30
      }
    }
  ]
}

Apply it:

aws s3api put-bucket-lifecycle-configuration \
    --bucket your-backup-bucket \
    --lifecycle-configuration file://lifecycle.json

This gives you:

  • Days 1-30: S3 Standard (~$0.023/GB/month)
  • Days 31-90: S3 Infrequent Access (~$0.0125/GB/month)
  • Days 91-365: Glacier (~$0.004/GB/month)
  • After 365 days: Automatically deleted

For a typical web server with 50GB of daily backups, this lifecycle policy reduces annual storage costs by roughly 60% compared to keeping everything in S3 Standard. For archives you rarely touch, S3 Glacier Deep Archive is cheaper still — at the cost of longer retrieval times, which is exactly the RTO tradeoff to weigh before you send recent backups there. Always confirm current rates on the AWS S3 pricing page, as they vary by region and change over time.


Backing Up Data That Already Lives in S3

Not every S3 backup question is about pushing server files to S3. A large share of searchers want to protect data that already sits in an S3 bucket — application uploads, user media, or a data lake — against accidental deletion or a bad deploy. S3 gives you three native tools for this:

  1. Versioning (enabled earlier) keeps every prior version of an object, so an overwrite or delete is recoverable. Combine it with the NoncurrentVersionExpiration rule above so old versions don't accumulate cost forever.
  2. Cross-region replication (CRR) copies objects to a bucket in a second AWS region automatically. This is your off-site copy for the 3-2-1 rule and protects against a whole-region outage.
  3. Scheduled bucket-to-bucket sync for a cheap, explicit second copy:
# Mirror a source bucket to a backup bucket (add --delete to prune removed objects)
aws s3 sync s3://production-bucket s3://production-bucket-backup \
    --sse AES256

You can point that same aws s3 sync at an S3-compatible provider in a different cloud for true vendor diversity — for example, replicating to Cloudflare R2 or Backblaze B2. We break down the durability, egress, and pricing differences in how S3 compares to Cloudflare R2 for storage. The scripts in this guide work unchanged against any S3-compatible endpoint — you only change the --endpoint-url in your AWS CLI configuration.


Deploying Backup Scripts with DeployHQ

If you already use DeployHQ to deploy your web application, you can manage your backup scripts as part of your codebase — version-controlled, tested, and deployed automatically alongside your application code.

Why Deploy Backup Scripts Through DeployHQ?

  • Version control — backup script changes go through code review like any other code
  • Automatic deployment — with automatic deployments from Git, a push deploys updated scripts to your server
  • Consistency — every server gets the same backup configuration
  • Zero-downtimeDeployHQ's zero-downtime deployments ensure your backups aren't interrupted during a deploy

Managing operational scripts through the same pipeline as your app means your backup logic never drifts out of sync across servers. If your code lives on GitHub, you can deploy from GitHub to your server — backup scripts and application alike — in a few clicks.

Project Structure

Add your backup scripts to your repository:

project/
├── scripts/
│   └── backups/
│       ├── backup-files.sh
│       ├── backup-db.sh
│       ├── verify-backup.sh
│       └── lifecycle.json
├── src/
│   └── ...
└── ...

Configure DeployHQ SSH Commands

In your DeployHQ project, go to SSH Commands and add a post-deploy command to install or update the backup cron jobs:

# Copy backup scripts to the right location
cp /var/www/myapp/current/scripts/backups/*.sh /opt/backups/
chmod +x /opt/backups/*.sh

# Install cron jobs (idempotent — safe to run on every deploy)
crontab -l 2>/dev/null | grep -v '/opt/backups/' > /tmp/crontab.tmp || true
echo "0 2 * * * /opt/backups/backup-files.sh >> /var/log/backup-files.log 2>&1" >> /tmp/crontab.tmp
echo "0 */6 * * * /opt/backups/backup-db.sh >> /var/log/backup-db.log 2>&1" >> /tmp/crontab.tmp
echo "0 4 * * * /opt/backups/verify-backup.sh >> /var/log/backup-verify.log 2>&1" >> /tmp/crontab.tmp
crontab /tmp/crontab.tmp
rm /tmp/crontab.tmp

This way, whenever you update a backup script and push to Git, DeployHQ automatically deploys the updated script and refreshes the cron schedule on your server.

Using Config Files for Per-Environment Settings

DeployHQ's config files feature lets you inject different S3 bucket names or AWS credentials per environment. Create a config file in DeployHQ that maps to scripts/backups/.env:

S3_BUCKET=production-backup-bucket
AWS_DEFAULT_REGION=eu-west-1
DB_NAME=production_db
ALERT_EMAIL=ops@example.com

Then source this file in your backup scripts:

#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/.env"
# ... rest of script uses $S3_BUCKET, $DB_NAME, etc.

For staging, DeployHQ injects a different .env with the staging bucket name — no code changes needed.


Restoring from Backups

A backup strategy is only as good as your restore process. Here's how to restore from S3:

Restore Files

# List available backups
aws s3 ls s3://your-backup-bucket/backups/files/ | tail -10

# Download a specific backup
aws s3 cp s3://your-backup-bucket/backups/files/files_20260215_020000.tar.gz /tmp/

# Restore to the original location
cd /
tar -xzf /tmp/files_20260215_020000.tar.gz

Restore a MySQL Database

# Download the backup
aws s3 cp s3://your-backup-bucket/backups/database/db_myapp_20260215_060000.sql.gz /tmp/

# Decompress and restore
gunzip /tmp/db_myapp_20260215_060000.sql.gz
mysql --defaults-file=~/.my.cnf your_database < /tmp/db_myapp_20260215_060000.sql

Restore a PostgreSQL Database

# The custom-format dump is already compressed — pg_restore reads it directly, no gunzip needed
aws s3 cp s3://your-backup-bucket/backups/database/pg_myapp_20260215_060000.dump /tmp/
pg_restore --clean --dbname=your_database /tmp/pg_myapp_20260215_060000.dump

Best Practices

  1. Test restores regularly — schedule a monthly restore drill to a test server. A backup you've never restored is a backup you can't trust.
  2. Encrypt everything — enable S3 default encryption and use IAM policies to restrict access. Never store AWS credentials in your backup scripts, and harden the Linux server your backups run on so a compromised host can't leak them.
  3. Monitor and alert — set up CloudWatch alarms for failed S3 uploads, or use the verification script above with email alerts.
  4. Use cross-region replication — for critical data, replicate your backup bucket to a second AWS region to satisfy the off-site copy in the 3-2-1 rule.
  5. Separate backup IAM credentials — the IAM user that writes backups should have minimal permissions (PutObject, GetObject, ListBucket) and no ability to delete the bucket itself.
  6. Log everything — redirect script output to log files so you can diagnose failures quickly.
  7. Treat backups as a compliance control — auditable, versioned, off-site backups are table stakes for frameworks like SOC 2. See how backups fit into broader SOC 2 compliance deployment workflows.

Common Questions

How much does S3 backup storage cost?

For a typical web server generating ~2GB of compressed daily backups with a 30-day retention policy, expect roughly $1.50-3.00/month in S3 Standard storage. With lifecycle policies moving older backups to Glacier, annual costs drop significantly. Rates vary by region — confirm current numbers on the AWS S3 pricing page.

Can I back up to S3 from any server?

Yes. S3 is cloud-agnostic — you can back up from any Linux server with internet access, whether it's on AWS, DigitalOcean, Hetzner, or a bare-metal server in your office. All you need is the AWS CLI and valid IAM credentials.

What about S3-compatible alternatives?

Services like Cloudflare R2, Backblaze B2, and MinIO offer S3-compatible APIs. The scripts in this guide work with any S3-compatible storage — just change the --endpoint-url in your AWS CLI configuration. This is also an easy way to get the off-site, different-provider copy the 3-2-1 rule calls for.

How do I handle large databases?

For databases larger than 10GB, consider incremental backups (using tools like xtrabackup for MySQL or pg_basebackup for PostgreSQL) instead of full dumps. You can also pipe the dump directly to S3 using aws s3 cp - s3://bucket/key to avoid filling up local disk.

How do I back up an existing S3 bucket?

Enable versioning, turn on cross-region replication, or run a scheduled aws s3 sync to a second bucket — see Backing Up Data That Already Lives in S3 above.


The Bottom Line

Automated backups to S3 are straightforward to set up and inexpensive to run. Define your RPO and RTO, apply the 3-2-1 rule, and the combination of bash scripts, cron scheduling, lifecycle policies, and verification checks gives you a reliable safety net for your production data.

If you're already deploying your web application with DeployHQ, managing your backup scripts through the same pipeline keeps everything version-controlled and automatically deployed. Start your free trial to see how DeployHQ simplifies deployment for your entire infrastructure — application code and operational scripts alike.


Questions about deploying your backup scripts with DeployHQ? Email us at support@deployhq.com or reach out on X (@deployhq).