> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Automated encrypted backups to Amazon S3 | Upsun

> Learn how to configure Duplicity, GPG keys, and cron on Upsun to send incremental, encrypted backups of your application to AWS S3 for reliable restores


export const PostMeta = ({data = {}}) => {
  const {author, date, image} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (!image && authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
      {image && <img src={image} alt="" className="post-meta-image" aria-hidden="true" />}
    </div>;
};

<PostMeta data={{ author: ["dereliahmet1"], date: "2026-01-12T12:01:00.000Z", image: "/images/posts/how-tos/automated-encrypt-backups-amazon-s3/UpsunxSkvare-800x300.webp" }} />

A secure, automated backup solution for applications hosted on Upsun can be implemented using Duplicity with GPG encryption to back up data to Amazon S3. This approach ensures data integrity, confidentiality, and recoverability. The following guide outlines how to set up such a system.

## Why this backup strategy?

Traditional backup methods often fall short in cloud-native environments: they may lack encryption, require manual intervention, or fail to integrate cleanly with immutable infrastructure. An S3-based backup solution using Duplicity and GPG encryption addresses these gaps through the following design choices:

### End-to-end encryption:

All backups are encrypted at rest using GPG before leaving the application container. Even if S3 credentials are compromised, data remains protected by a strong passphrase and private key. Private keys and passphrases should never be stored in plaintext on the platform.

### Efficient incremental backups

Duplicity uses incremental syncing, meaning only changed files are uploaded after the initial full backup. This reduces bandwidth, storage costs, and backup windows. Critical for large Drupal sites with frequent file uploads.

### Cloud-native and portable

By using AWS S3 (a durable, versionable, and globally available object store) and boto3 (the official AWS SDK), avoid vendor lock-in while leveraging battle-tested infrastructure. The same backup can be restored anywhere, like Upsun, local dev, or another cloud.

### Declarative and reproducible

Thanks to Upsun’s composable Nix stack, every dependency (Duplicity, GnuPG, Python packages) is versioned and immutable. There’s no “works on my machine” problem. Your backup environment is identical across all deployments.

### Fully automated

With cron-driven execution and deploy-time key setup, backups run without human intervention. Combined with environment-based toggles (S3\_BACKUP=On), it’s easy to enable/disable per environment (on for prod, off for dev).

## How to set up:

### 1. Configure the composable stack

[Upsun’s composable images](https://docs.upsun.com/create-apps/app-reference/composable-image.html) (based on Nix) let you declaratively define your runtime environment. To support encrypted S3 backups, add the required packages to your

```yaml {filename=".upsun/config.yaml"} theme={null}
applications:
  drupal:
    type: 'composable:25.05'
    stack:
      - gnupg
      - duplicity
      - python3
      - python3Packages.numpy
      - python3Packages.boto3
      # Other required packages...
```

#### Why these packages?

* Duplicity: Handles incremental, encrypted backups.
* gnupg: Manages GPG key operations.
* boto3: Enables Duplicity to communicate with AWS S3.
* python3 & numpy: Runtime dependencies for Duplicity and boto3.

### 2. Generate GPG keys locally

Backups are encrypted using GPG. Generate a dedicated key pair on your local machine:

```yaml {filename="Terminal"} theme={null}
gpg --full-generate-key
```

* Choose RSA and RSA (default)
* Set key size to 4096 bits.
* Set a strong passphrase (you’ll store this securely)
* Use a descriptive name/email ([devops+upsun-backup@example.com](mailto:devops+upsun-backup@example.com))

**After creation, note the Key ID (ABC123DEF456):**

#### Export the keys in ASCII-armored format:

```bash {filename="Terminal"} theme={null}
# Public key
gpg --armor --export ABC123DEF456 > public.key

# Private key
gpg --armor --export-secret-keys ABC123DEF456 > private.key
```

**Tip**: Store the passphrase, Key ID, and both key files in your secret storage or password manager.

### 3. Set environment variables

Upsun has a 4096-byte limit on environment variable values. Since GPG private keys often exceed this, split the private key into two parts:

```bash theme={null}
GPG_PASSPHRASE=your_strong_passphrase
GPG_KEY_ID=ABC123DEF456
GPG_PUBLIC_KEY=-----BEGIN PGP PUBLIC KEY BLOCK-----...
GPG_KEY_PART1=-----BEGIN PGP PRIVATE KEY BLOCK-----... (first half)
GPG_KEY_PART2=... (second half)-----END PGP PRIVATE KEY BLOCK-----
AWS_ACCESS_KEY_ID=your_aws_key
AWS_SECRET_ACCESS_KEY=your_aws_secret
AWS_REGION=us-east-2
AWS_DEFAULT_REGION=us-east-2
S3_BACKUP=On
```

**Tip**: When adding environment variables or keys in Upsun, especially long ones such as SSH keys or encrypted tokens, it’s generally easier to use the Upsun Console UI rather than the CLI.\
The Settings > Variables section in UI allows you to paste or edit long multi-line values directly without worrying about formatting issues or line breaks.

### 4. Create an S3 bucket

Create a dedicated S3 bucket in your preferred region (us-east-2):

```bash theme={null}
aws s3api create-bucket \
    --bucket your-unique-backup-bucket-name \
    --region us-east-2 \
```

Security note: The bucket does not need to be publicly accessible. Ensure your AWS credentials have s3:PutObject, s3:GetObject, and s3:ListBucket permissions only.

Set the bucket name via the **AWS\_S3\_BUCKET** environment variable.

### 5. Automate backups with a cron job

Add a cron task to .upsun/config.yaml to run incremental backups every 30 minutes:\
You can adjust the schedule to match your preference.

```yaml {filename=".upsun/config.yaml"} theme={null}
crons:
  backup:
    spec: '*/30 * * * *'
    commands:
      start: |
        if [ "$S3_BACKUP" = "On" ]; then
          PASSPHRASE="$GPG_PASSPHRASE" GNUPGHOME=/tmp/gnupg \
          duplicity incremental \
            --encrypt-key "$GPG_KEY_ID" \
            /app \
            boto3+s3://$AWS_S3_BUCKET
        fi
```

| Cron Expression           | Meaning                  |
| ------------------------- | ------------------------ |
| \*\*\*/30 \* \* \* \*\*\* | Every 30 minutes         |
| \*\*0 2 \* \* \*\*\*      | Daily at 2 AM (for full) |
| **0 1 \* \* 0**           | Weekly at 1 AM on Sunday |

#### What’s backed up?

By default, the entire /app directory (containing the application codebase and files) is backed up incrementally. Additional directories can be included as needed.

### 6. Import GPG keys on deploy

During deployment, import your GPG keys into a temporary keyring:

```yaml {filename=".upsun/config.yaml"} theme={null}
hooks:
  deploy: |
    set -e
    if [ "$S3_BACKUP" = "On" ]; then
      export GNUPGHOME=/tmp/gnupg
      mkdir -p /tmp/gnupg
      chmod 700 /tmp/gnupg

      # Reconduct the keys
      echo "$GPG_PUBLIC_KEY" > /tmp/gnupg/public.key
      echo "$GPG_KEY_PART1$GPG_KEY_PART2" > /tmp/gnupg/private.key
      # Secure the keys
      chmod 600 /tmp/gnupg/public.key
      chmod 600 /tmp/gnupg/private.key
      # Import public key
      gpg --batch --import /tmp/gnupg/public.key
      # Import private key
      gpg --batch --import /tmp/gnupg/private.key
      # Trust the key fully
      echo -e "trust\n5\ny\n" | gpg --command-fd 0 --edit-key "$GPG_KEY_ID"
      # Delete the key files
      rm -f /tmp/gnupg/public.key
      rm -f /tmp/gnupg/private.key
    fi
```

This ensures Duplicity can encrypt (and later decrypt) backups.

### 7. Test Your first backup

After deployment, trigger a manual full backup to verify the setup:

```bash {filename="Terminal"} theme={null}
# SSH into your environment
upsun ssh

# Run full backup
PASSPHRASE="$GPG_PASSPHRASE" GNUPGHOME=/tmp/gnupg \
duplicity full --encrypt-key "$GPG_KEY_ID" /app boto3+s3://$AWS_S3_BUCKET
```

Then check your S3 bucket. You should see encrypted .gpg files and a signature file.

```bash theme={null}
Bucket: platform-s3-backup-demo
│
├── duplicity-full-signatures.20250405T100000Z.sigtar.gpg
├── duplicity-full.20250405T100000Z.manifest.gpg
├── duplicity-full.20250405T100000Z.vol1.difftar.gpg
├── duplicity-inc.20250405T103000Z.to.20250405T110000Z.manifest.gpg
├── duplicity-inc.20250405T103000Z.to.20250405T110000Z.vol1.difftar.gpg
└── ...
```

### 8. Restoring from backup

Need to restore? Since Upsun is a read-only environment, use a local or a Cloud Server to restore backups.

* Spin up a recovery environment on a cloud server or on a local server.
* Re-import GPG keys
* Use duplicity restore with your S3 URL and passphrase
* Sync files back to Upsun

## Conclusion

This setup provides secure, automated, and incremental backups of Upsun-hosted applications directly to AWS S3 with end-to-end GPG encryption. By combining Upsun’s composable Nix stack with Duplicity’s robust backup logic, it ensures data resilience while maintaining operational simplicity.
