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 S3 backup strategy for your servers: automated scripts for files and databases, scheduling, verification, lifecycle policies for cost control, and how to deploy and manage your backup scripts using [DeployHQ](https://www.deployhq.com).

* * *

## 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 tools like [DeployHQ](https://www.deployhq.com), S3 integrates naturally into your existing workflow — you can manage backup scripts alongside your application code and deploy them automatically. The same [DeployHQ](https://www.deployhq.com) S3 connector that ships your backup scripts can also [deploy a static site or built assets directly from Git to an S3 bucket](https://www.deployhq.com/blog/setting-up-deployments-from-git-to-amazon-s3).

* * *

## S3 Backup Strategy Overview

A solid backup strategy answers four questions:

1. **What** gets backed up? (files, databases, configuration)
2. **How often?** (frequency based on data change rate)
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
```

### 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/*"
      ]
    }
  ]
}
```

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
```

### 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}.sql.gz"
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 --format=custom "$DB_NAME" | gzip > "${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
```

* * *

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

* * *

## Deploying Backup Scripts with DeployHQ

If you already use [DeployHQ](https://www.deployhq.com) 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** — push to Git and [DeployHQ](https://www.deployhq.com) deploys updated scripts to your server
- **Consistency** — every server gets the same backup configuration
- **Zero-downtime** — [DeployHQ's zero-downtime deployments](https://deployhq.com/features/zero-downtime-deployments) ensure your backups aren't interrupted during a deploy

### 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](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://deployhq.com/support/projects/configuration-files) lets you inject different S3 bucket names or AWS credentials per environment. Create a config file in [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com) 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

```
aws s3 cp s3://your-backup-bucket/backups/database/pg_myapp_20260215_060000.sql.gz /tmp/
gunzip /tmp/pg_myapp_20260215_060000.sql.gz
pg_restore --clean --dbname=your_database /tmp/pg_myapp_20260215_060000.sql
```

* * *

## 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.
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.
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. **Version your backup scripts** — keep them in Git alongside your application code and deploy them with [DeployHQ](https://www.deployhq.com) so every server stays in sync.

* * *

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

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

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

* * *

## The Bottom Line

Automated backups to S3 are straightforward to set up and inexpensive to run. 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](https://www.deployhq.com), managing your backup scripts through the same pipeline keeps everything version-controlled and automatically deployed. [Start your free trial](https://www.deployhq.com/signup) to see how [DeployHQ](https://www.deployhq.com) simplifies deployment for your entire infrastructure — application code and operational scripts alike.

