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 sudo privileges.
  • 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 include ams3, fra1, sfo3 and sgp1. Choose the one closest to your server.

Step 1 - Creating a Space and an access key

In the DigitalOcean control panel:

  1. 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.
  2. 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.
  3. 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_url sends every request to Spaces instead of AWS. The datacenter is selected by the endpoint, not by region, 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.

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.