This guide adds off-server storage for the bundled single-server backup flow.
The local backup remains the source step:
october-backup.timer -> scripts/backup.sh -> /var/backups/october -> optional S3 upload
S3 upload is disabled by default. Enable it per server through runtime secrets, not through the project .env and not through git.
When enabled, scripts/backup.sh uploads the files created during the current backup run:
postgres-<tag>.dumpstorage-app-<tag>.tar.gzmetadata-<tag>.txtenv-<tag>only whenBACKUP_INCLUDE_SECRETS=1auth-<tag>.jsononly whenBACKUP_INCLUDE_SECRETS=1andauth.jsonexists
The script does not manually split files into 100M parts. aws s3 cp uses S3 multipart upload for large files while keeping a single object in the bucket, which keeps restore simple.
For a Debian server:
sudo apt-get update
sudo apt-get install -y awscli
aws --versionThe PHP and Nginx images do not need awscli. Backup upload runs on the host through systemd.
Create a bucket and prefix, for example:
s3://company-october-backups/projects/example-site/production
Use a dedicated access key for backups. Minimum IAM permissions for upload and restore checks:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::company-october-backups",
"Condition": {
"StringLike": {
"s3:prefix": ["projects/example-site/production/*"]
}
}
},
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject"],
"Resource": "arn:aws:s3:::company-october-backups/projects/example-site/production/*"
}
]
}Prefer bucket lifecycle rules for remote retention, for example keep daily backups for 30-90 days depending on the client contract.
scripts/backup.sh reads regular environment variables. The source can be a simple systemd EnvironmentFile, a Vault-rendered file, platform secrets or another runtime secret mechanism.
Recommended order:
- Simple VPS:
/etc/october-backup.envwith0600 root:root. - Stronger VPS: Vault Agent or another secret manager renders
/run/october-backup/envon tmpfs, then systemd reads that file. - Orchestrator: Kubernetes, BeCloud-like platforms, Docker Swarm or another scheduler injects the values as runtime secrets into a backup CronJob.
- systemd credentials: use
LoadCredential=with a small wrapper or a future script extension that reads$CREDENTIALS_DIRECTORY.
The backup script should not need to know where the values came from. It only expects environment variables.
Create a root-readable environment file:
sudo install -m 600 -o root -g root /dev/null /etc/october-backup.env
sudoedit /etc/october-backup.envExample:
BACKUP_NOTIFY_ENABLED=1
TELEGRAM_BOT_TOKEN=123456:example
TELEGRAM_CHAT_ID=-1001234567890
TELEGRAM_THREAD_ID=
BACKUP_S3_ENABLED=1
BACKUP_S3_URI=s3://company-october-backups/projects/example-site/production
BACKUP_S3_REGION=eu-central-1
BACKUP_S3_STORAGE_CLASS=STANDARD_IA
BACKUP_S3_ENDPOINT=
BACKUP_S3_DELETE_LOCAL_AFTER_UPLOAD=0
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=eu-central-1Do not put this file in the repository. Do not include it in regular project backups unless it is encrypted and access-controlled.
For a server where S3 is not ready yet, keep S3 explicitly disabled:
BACKUP_NOTIFY_ENABLED=0
BACKUP_S3_ENABLED=0
BACKUP_S3_DELETE_LOCAL_AFTER_UPLOAD=0When Telegram credentials are available, enable notifications without enabling S3:
BACKUP_NOTIFY_ENABLED=1
TELEGRAM_BOT_TOKEN=123456:example
TELEGRAM_CHAT_ID=-1001234567890
TELEGRAM_THREAD_ID=
BACKUP_S3_ENABLED=0
BACKUP_S3_DELETE_LOCAL_AFTER_UPLOAD=0Backup control:
BACKUP_DIR=/var/backups/october
BACKUP_NOTIFY_ENABLED=0
BACKUP_NOTIFY_SCRIPT=/opt/october/app/scripts/telegram-notify.sh
BACKUP_S3_ENABLED=0
BACKUP_S3_DELETE_LOCAL_AFTER_UPLOAD=0
BACKUP_INCLUDE_SECRETS=0
BACKUP_RETENTION_COUNT=14Telegram notifications:
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
TELEGRAM_THREAD_ID=S3 upload:
BACKUP_S3_URI=s3://company-october-backups/projects/example-site/production
BACKUP_S3_REGION=eu-central-1
BACKUP_S3_STORAGE_CLASS=STANDARD_IA
BACKUP_S3_ENDPOINT=
BACKUP_S3_AWS_BIN=awsAWS credentials used by aws s3 cp:
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=eu-central-1
AWS_SESSION_TOKEN=Use AWS_SESSION_TOKEN only for temporary credentials. For permanent IAM user keys, leave it unset.
For stronger deployments, render the same variables to a runtime file outside the project directory:
/run/october-backup/env
Example systemd override:
[Service]
EnvironmentFile=
EnvironmentFile=-/run/october-backup/envThe empty EnvironmentFile= line clears the value generated by the installer. The next line points systemd at the Vault-rendered runtime file.
The secret manager should render values such as:
BACKUP_NOTIFY_ENABLED=1
TELEGRAM_BOT_TOKEN=...
TELEGRAM_CHAT_ID=...
BACKUP_S3_ENABLED=1
BACKUP_S3_URI=s3://company-october-backups/projects/example-site/production
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=eu-central-1After changing the override:
sudo systemctl daemon-reload
sudo systemctl restart october-backup.timersystemd credentials keep secret values out of normal environment files. This is a good hardening path for dedicated VPS deployments, but it needs a small wrapper or a script extension that maps credential files into environment variables before calling scripts/backup.sh.
The systemd unit would use credentials like:
[Service]
LoadCredential=TELEGRAM_BOT_TOKEN:/etc/secure/october/telegram_bot_token
LoadCredential=TELEGRAM_CHAT_ID:/etc/secure/october/telegram_chat_id
LoadCredential=AWS_ACCESS_KEY_ID:/etc/secure/october/aws_access_key_id
LoadCredential=AWS_SECRET_ACCESS_KEY:/etc/secure/october/aws_secret_access_keyAt runtime, systemd exposes them under $CREDENTIALS_DIRECTORY. A wrapper can export:
export TELEGRAM_BOT_TOKEN="$(cat "$CREDENTIALS_DIRECTORY/TELEGRAM_BOT_TOKEN")"
export TELEGRAM_CHAT_ID="$(cat "$CREDENTIALS_DIRECTORY/TELEGRAM_CHAT_ID")"
export AWS_ACCESS_KEY_ID="$(cat "$CREDENTIALS_DIRECTORY/AWS_ACCESS_KEY_ID")"
export AWS_SECRET_ACCESS_KEY="$(cat "$CREDENTIALS_DIRECTORY/AWS_SECRET_ACCESS_KEY")"
exec /opt/october/app/scripts/backup.shFor now, the bundled installer uses EnvironmentFile because it is portable and simple for a baseline VPS setup.
Install or refresh the timer:
cd /opt/october/app
BACKUP_ENV_FILE=/etc/october-backup.env \
BACKUP_DIR=/var/backups/october \
./scripts/install-backup-timer.shThe generated service contains:
EnvironmentFile=-/etc/october-backup.envThe leading - means the file is optional. If the file is missing, local backups still run and S3 remains disabled unless variables are provided another way. If you switch to Vault or another runtime secret source, set BACKUP_ENV_FILE to that generated file path or add a systemd override.
When BACKUP_NOTIFY_ENABLED=1, scripts/backup.sh calls scripts/telegram-notify.sh.
It sends:
[backup:start]with project, host, tag, git commit, image tag and S3 state[backup:success]with local directory, S3 URI, file list and total size[backup:failure]with exit code, partial file list and journal command
Notification failures do not fail the backup itself. The backup job writes a warning to the journal instead.
Run one backup through systemd:
sudo systemctl start october-backup.service
sudo journalctl -u october-backup.service -n 120 --no-pagerCheck local files:
ls -lh /var/backups/octoberCheck S3:
aws s3 ls s3://company-october-backups/projects/example-site/production/Download and test restore on a disposable machine or temporary container before trusting the setup.
For MinIO, Selectel, Cloudflare R2, Yandex Object Storage or another S3-compatible provider, set an endpoint:
BACKUP_S3_ENDPOINT=https://s3.example-provider.com
BACKUP_S3_REGION=ru-1
AWS_DEFAULT_REGION=ru-1If the provider needs path-style access, configure it in the AWS CLI profile or use provider-specific AWS environment variables. For complex S3-compatible setups, rclone can be added later as a second provider, but the bundled script currently targets aws s3 cp.
Download the files you need:
mkdir -p /var/backups/october
aws s3 cp \
s3://company-october-backups/projects/example-site/production/postgres-20260601T031500Z.dump \
/var/backups/october/
aws s3 cp \
s3://company-october-backups/projects/example-site/production/storage-app-20260601T031500Z.tar.gz \
/var/backups/october/Then follow Backup And Restore for PostgreSQL and storage-app restore.
For multi-node production, do not treat a local Docker volume as the source of truth for media/uploads. Use S3 or MinIO as the application media storage, or use a shared RWX storage system and back it up with the platform's backup tooling.
In Kubernetes or BeCloud-like platforms, replace the host systemd timer with a platform CronJob and inject the same variables through Secrets.