How to Back Up Postgres to AWS S3 With Encryption
A nightly Postgres backup should never touch local disk - a temp file forces you to size /tmp to your largest database and leaves a half-written object if the process dies. Stream it instead: pg_dump app | gzip | aws s3 cp - s3://db-backups/app-$(date +%s).sql.gz, and add --sse aws:kms so the dump is encrypted at rest the moment it lands. Here is the full pipeline.
Stream the dump - no temp file
The naive backup writes a dump to disk, then uploads it:
pg_dump -U postgres app | gzip > /tmp/app.sql.gz
aws s3 cp /tmp/app.sql.gz s3://db-backups/app.sql.gz
That forces you to size /tmp to your largest database, fails outright when
the disk fills, and leaves a half-written file if the process dies mid-run.
The fix is a single streaming pipeline - aws s3 cp - reads the upload body
from stdin, so the dump flows straight into S3 as pg_dump produces it:
#!/bin/bash
set -euo pipefail
TS=$(date +%s)
KEY="app-${TS}.sql.gz"
pg_dump -U postgres app | gzip | aws s3 cp - "s3://db-backups/${KEY}"
echo "s3://db-backups/${KEY}"
Three commands, one pass, zero intermediate files. Constant memory, no /tmp
pressure, and nothing to clean up if the process is interrupted. The object
key app-<unix-timestamp>.sql.gz (for example app-1779625707.sql.gz) keeps
every nightly dump distinct and sorts chronologically.
Turn on AWS S3 encryption
A database dump is your most sensitive object - it holds every row. Encrypt it
at rest. AWS gives you two server-side options, both applied at upload time by
a single flag on aws s3 cp:
# SSE-S3: AWS-managed keys, no key setup, no extra cost
pg_dump -U postgres app | gzip | \
aws s3 cp - "s3://db-backups/${KEY}" --sse AES256
# SSE-KMS: your own KMS key, per-key access control + an audit trail
pg_dump -U postgres app | gzip | \
aws s3 cp - "s3://db-backups/${KEY}" \
--sse aws:kms --sse-kms-key-id alias/db-backups
--sse AES256 is the zero-config default. Reach for --sse aws:kms when you
want to control who can decrypt (via the KMS key policy) and log every
decrypt call in CloudTrail - the right choice for backups that must satisfy
an audit.
Enforce encryption at the bucket
Setting the flag per-upload works, but one script that forgets it uploads a plaintext dump. Enforce encryption on the bucket so S3 encrypts every object regardless of what the client sends. Set default encryption:
aws s3api put-bucket-encryption \
--bucket db-backups \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "alias/db-backups"}
}]
}'
With default encryption on, any object that arrives without an SSE header is
still encrypted with the bucket's chosen key. To go further and reject
unencrypted uploads outright, add a bucket policy that denies
s3:PutObject when s3:x-amz-server-side-encryption is absent - the request
then fails with 403 AccessDenied instead of silently storing plaintext.
Verify the backup is real and encrypted
A backup you have not restored is a guess. Confirm the object exists, has real size, and reports its encryption:
aws s3api head-object --bucket db-backups --key "$KEY" \
--query '{size:ContentLength, enc:ServerSideEncryption}'
# -> {"size": 1043, "enc": "aws:kms"}
Then prove it round-trips - pull it back, gunzip, and grep for a known table:
aws s3 cp "s3://db-backups/${KEY}" - | gunzip | grep -c 'CREATE TABLE'
A non-zero count means the dump is complete and readable. Wire the whole script into a nightly CronJob and alert on a non-zero exit, and you have a self-managed Postgres backup that lands encrypted every night.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Streaming pg_dump through gzip into aws s3 cp - with no temp file
- Encrypting the upload at rest with --sse AES256 or --sse aws:kms
- Enforcing default bucket encryption and verifying an object round-trips
FAQ
How do I enable AWS S3 encryption on an upload?
Add --sse AES256 to aws s3 cp for AWS-managed SSE-S3, or --sse aws:kms --sse-kms-key-id <key> for SSE-KMS with your own key. S3 encrypts the object at rest at write time; no client-side work is needed.
What is the difference between SSE-S3 and SSE-KMS on S3?
SSE-S3 (--sse AES256) uses keys AWS manages entirely - zero setup, no extra cost. SSE-KMS (--sse aws:kms) uses a KMS key you control, so you can restrict decrypt access via the key policy and log every decrypt call in CloudTrail, at a small per-request cost.
How do I back up a Postgres database to S3 without a temp file?
Stream it in one pipeline: pg_dump -U postgres app | gzip | aws s3 cp - s3://db-backups/app-$(date +%s).sql.gz. The aws s3 cp - form reads the upload from stdin, so the dump flows straight to S3 with constant memory and no disk file to clean up.
How do I force every object in an S3 bucket to be encrypted?
Set default encryption with aws s3api put-bucket-encryption so S3 encrypts anything that arrives without an SSE header. To reject unencrypted uploads instead of silently fixing them, add a bucket policy that denies s3:PutObject when s3:x-amz-server-side-encryption is missing.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →