DigitalOcean Spaces is an S3-compatible object storage service, so any tool that speaks the S3 API can use it by pointing at a Spaces endpoint. That makes it a practical off-server location for backups and static files from a VPS at any provider. In this tutorial you will connect an Ubuntu 24.04 server to a Space with the AWS CLI and rclone, schedule daily backups with a systemd timer, expire old backups automatically with a lifecycle rule, and serve public files through the Spaces CDN.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user with
sudoprivileges. - A DigitalOcean account with Spaces Object Storage enabled on the account.
- The name of a datacenter region for your Space. This guide uses
nyc3; other regions includeams3,fra1,sfo3andsgp1. Choose the one closest to your server.
Step 1 - Creating a Space and an access key
In the DigitalOcean control panel:
- Open Spaces Object Storage and create a Space (bucket). Pick the region, give it a unique name such as
your_space_name, and keep file listing restricted so the contents are private. - In the Access Keys section of Spaces Object Storage, create a new key. You can give it full access or limit it to this Space; a key limited to one Space with read/write permissions is the safer choice for a backup client.
- Copy the Access Key ID and the Secret Key. The secret is shown only once.
Your Space is reachable through the regional endpoint https://nyc3.digitaloceanspaces.com. Every tool below needs that endpoint plus the key pair.
Step 2 - Installing the AWS CLI
Ubuntu 24.04 no longer ships the AWS CLI in its archive, so install version 2 with AWS's official installer. First install unzip:
sudo apt update
sudo apt install unzip curl
Download and install the CLI. On an ARM server, replace x86_64 with aarch64 in the URL:
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
unzip -q awscliv2.zip
sudo ./aws/install
Check the version:
aws --version
aws-cli/2.x.x Python/3.x.x Linux/6.8.0-... exe/x86_64.ubuntu.24
You can remove awscliv2.zip and the aws/ directory afterwards.
Step 3 - Configuring a Spaces profile
Keep Spaces settings in a named profile so they never mix with real AWS credentials. Create the credentials file:
mkdir -p ~/.aws
nano ~/.aws/credentials
[spaces]
aws_access_key_id = your_spaces_access_key
aws_secret_access_key = your_spaces_secret_key
Restrict its permissions:
chmod 600 ~/.aws/credentials
Then create the config file:
nano ~/.aws/config
[profile spaces]
region = us-east-1
endpoint_url = https://nyc3.digitaloceanspaces.com
request_checksum_calculation = when_required
response_checksum_validation = when_required
What these settings do:
endpoint_urlsends every request to Spaces instead of AWS. The datacenter is selected by the endpoint, not byregion, which only has to be a value the CLI accepts for request signing.- The two checksum settings stop recent AWS CLI versions from adding newer integrity checksums by default. Some S3-compatible services reject or mishandle them, so sending them only when required avoids upload errors.
List your Spaces to confirm the credentials work:
aws s3 ls --profile spaces
2026-09-25 10:12:03 your_space_name
Step 4 - Uploading and downloading files
Use the standard aws s3 commands with --profile spaces. Upload a file:
echo "hello from $(hostname)" > test.txt
aws s3 cp test.txt s3://your_space_name/test/test.txt --profile spaces
List it:
aws s3 ls s3://your_space_name/test/ --profile spaces
2026-09-25 10:15:41 22 test.txt
Download it again and compare:
aws s3 cp s3://your_space_name/test/test.txt /tmp/test-copy.txt --profile spaces
diff test.txt /tmp/test-copy.txt && echo identical
To mirror a directory, use sync. It only uploads new or changed files:
aws s3 sync ~/project-assets s3://your_space_name/assets/ --profile spaces
Remove the test object when you are done:
aws s3 rm s3://your_space_name/test/test.txt --profile spaces
Step 5 - Configuring rclone for scheduled jobs
The AWS CLI is convenient interactively, while rclone is a good fit for unattended jobs: it retries failed transfers, can limit bandwidth and has a single config file. Install it from the Ubuntu archive:
sudo apt install rclone
The backup job in the next step runs as root, so create the rclone config for root:
sudo mkdir -p /root/.config/rclone
sudo nano /root/.config/rclone/rclone.conf
[spaces]
type = s3
provider = DigitalOcean
access_key_id = your_spaces_access_key
secret_access_key = your_spaces_secret_key
endpoint = nyc3.digitaloceanspaces.com
acl = private
no_check_bucket = true
no_check_bucket = true stops rclone from trying to create the bucket before each upload, which a key limited to one Space is not allowed to do. Protect the file:
sudo chmod 600 /root/.config/rclone/rclone.conf
Check that rclone can see the Space:
sudo rclone lsd spaces:
-1 2026-09-25 10:12:03 -1 your_space_name
Step 6 - Automating daily backups
The script below creates compressed archives of /etc and /var/www in a temporary directory and copies them to a dated prefix in the Space. Old backups are not deleted by the script; the lifecycle rule in Step 7 takes care of that.
Create the script:
sudo nano /usr/local/bin/spaces-backup
#!/usr/bin/env bash
set -euo pipefail
SPACE="your_space_name"
HOST="$(hostname -s)"
STAMP="$(date +%F)"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
tar -czf "$WORKDIR/etc.tar.gz" -C / etc
tar -czf "$WORKDIR/www.tar.gz" -C / var/www
rclone copy "$WORKDIR" "spaces:${SPACE}/backups/${HOST}/${STAMP}/" --retries 5
Adjust the tar lines to the directories you want to protect. If the server runs MySQL or MariaDB, add a dump before the upload, for example mysqldump --all-databases --single-transaction | gzip > "$WORKDIR/mysql.sql.gz"; with pipefail set, a failed dump stops the script.
Make it executable and run it once by hand:
sudo chmod 750 /usr/local/bin/spaces-backup
sudo /usr/local/bin/spaces-backup
Confirm the files arrived:
sudo rclone ls spaces:your_space_name/backups/
1843210 web01/2026-09-25/etc.tar.gz
20411877 web01/2026-09-25/www.tar.gz
Now schedule it with a systemd timer. Create the service unit:
sudo nano /etc/systemd/system/spaces-backup.service
[Unit]
Description=Back up server files to DigitalOcean Spaces
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/spaces-backup
Create the timer:
sudo nano /etc/systemd/system/spaces-backup.timer
[Unit]
Description=Daily backup to DigitalOcean Spaces
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true runs a missed backup at the next boot if the server was off at 03:00. Enable the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now spaces-backup.timer
Check when it will run next:
systemctl list-timers spaces-backup.timer
After a run, sudo journalctl -u spaces-backup.service shows the output and any errors.
Step 7 - Expiring old backups with a lifecycle rule
Spaces supports S3 lifecycle rules that delete objects after a number of days. Create a rule that removes everything under backups/ after 30 days and cleans up interrupted multipart uploads:
nano lifecycle.json
{
"Rules": [
{
"ID": "expire-backups",
"Prefix": "backups/",
"Status": "Enabled",
"Expiration": { "Days": 30 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 2 }
}
]
}
Apply it to the Space:
aws s3api put-bucket-lifecycle-configuration \
--bucket your_space_name \
--lifecycle-configuration file://lifecycle.json \
--profile spaces
Read it back to verify:
aws s3api get-bucket-lifecycle-configuration --bucket your_space_name --profile spaces
The command prints the rule you just applied. Objects older than 30 days are removed by a background process, so deletion is not instant on day 31.
Importanta lifecycle rule deletes backups whether or not newer ones exist. If the backup job silently stops, 30 days later you have nothing. Alert on backup age, for example by checking the newest date prefix from your monitoring system.
Step 8 - Serving public files through the Spaces CDN
Spaces includes a CDN that caches objects at edge locations. In the control panel, open your Space, go to Settings and enable the CDN. You can optionally attach a custom subdomain with a certificate managed by DigitalOcean.
Objects are private by default. Upload a file that should be public with a public-read ACL:
aws s3 cp logo.png s3://your_space_name/public/logo.png --acl public-read --profile spaces
Request it through the CDN hostname:
curl -I https://your_space_name.nyc3.cdn.digitaloceanspaces.com/public/logo.png
HTTP/2 200
content-type: image/png
...
Keep public assets in a separate Space from backups, so a mistaken ACL can never expose backup archives.
Troubleshooting
SignatureDoesNotMatch or InvalidAccessKeyId. The key or secret is wrong, or the profile points to the wrong endpoint. Check ~/.aws/credentials, confirm endpoint_url matches the Space's region, and regenerate the key if in doubt.
AccessDenied from rclone when uploading. A key limited to one Space cannot create or check buckets. Make sure no_check_bucket = true is set and that the key has write permission on that Space.
Uploads fail with checksum or header errors after a CLI update. Confirm that the two *_checksum_* settings from Step 3 are in the [profile spaces] section.
The CDN returns 403. The object is private. Upload it again with --acl public-read, or change its permissions in the control panel.
Conclusion
Your server now talks to DigitalOcean Spaces through a dedicated AWS CLI profile and an rclone remote, backs up key directories every night with a systemd timer, and relies on a lifecycle rule to keep storage from growing forever.
As next steps, switch the backup to an encrypted, deduplicated tool such as restic with Spaces as its S3 backend, test a full restore on a scratch server, and add an alert that fires when the newest backup is more than a day old.
