VPS Snaps

How to use UpCloud Object Storage for server backups

UpCloud Managed Object Storage is S3-compatible, billed in 250 GB steps, with no egress fees, so any S3 tool can store backups there and restores cost no bandwidth. Create an instance in a region away from your servers, add a bucket, give each server a user whose custom policy reaches only that bucket, and point the AWS CLI at the instance's endpoint, https://<id>.upcloudobjects.com, with its region, such as europe-1. It supports versioning and lifecycle rules but not Object Lock yet, so keep a second copy elsewhere.

10 min readUpdated Checked against official documentation

Pick a region

Each instance lives in one of five regions. Servers in the zones listed can also reach it over an SDN private network.

RegionPhysical locationPrivate network access from
APAC-1SG-SIN1 (Singapore)AU-SYD1, SG-SIN1
EUROPE-1FI-HEL2 (Helsinki)All European zones
EUROPE-2DE-FRA1 (Frankfurt)All European zones
EUROPE-3SE-STO1 (Stockholm)All European zones
US-1US-CHI1 (Chicago)US-CHI1, US-NYC1, US-SJO1

The European zones are DE-FRA1, DK-CPH1, ES-MAD1, FI-HEL1, FI-HEL2, NL-AMS1, NO-SVG1, PL-WAW1, SE-STO1 and UK-LON1. Config files and the API use the lowercase name, such as europe-1.

Keep backups in a different data centre from the servers they protect: a server in DE-FRA1 backing up to EUROPE-2 keeps both copies in one building. Servers outside UpCloud reach any region over its public endpoint.

Managed Object Storage replaced UpCloud's legacy Object Storage, scheduled for end of life on February 28, 2025. Tutorials written for the legacy service won't match these steps.

Create the instance and a bucket

  1. In the UpCloud Control Panel, open Object Storage and click Create new Object Storage.
  2. Choose the region. Enable Public access if servers outside UpCloud's private networks will connect, and attach an SDN private network if yours run in a zone listed for that region.
  3. Name the instance, click Create Object Storage, and wait for the status Ready.
  4. Open the instance's Buckets tab and click Create bucket. The name becomes a subdomain of the endpoint and must be unique only within this instance; use lowercase letters, numbers and hyphens.

Each instance has its own endpoint, shown in its details, such as https://abcd1.upcloudobjects.com; a private network adds https://abcd1-private.upcloudobjects.com, reachable only from that network. The examples use abcd1 and a bucket named acme-server-backups.

An instance can be deleted along with everything in it, so turn on termination protection, an API field. Create an API token in the Control Panel, put it in UPCLOUD_TOKEN, and list your instances (this needs jq):

Terminal
curl -sS https://api.upcloud.com/1.3/object-storage-2 -H "Authorization: Bearer $UPCLOUD_TOKEN" | jq -r '.[] | [.uuid, .name, .region, (.endpoints[] | select(.type == "public" and .mode == "api") | .domain_name)] | @tsv'

That prints each instance's UUID, name, region and public endpoint. Put the UUID in SERVICE, then:

Terminal
curl -sS -X PATCH "https://api.upcloud.com/1.3/object-storage-2/$SERVICE" -H "Authorization: Bearer $UPCLOUD_TOKEN" -H "Content-Type: application/json" -d '{"termination_protection": true}'

Termination protection guards the instance as a whole. Deleting a bucket through the UpCloud API, which the Terraform provider uses, removes every object in it, permanently; only the S3 API refuses to delete a bucket that isn't empty.

Create a key that reaches one bucket

Users, policies and keys belong to one instance. The built-in ECSS3FullAccess policy covers every bucket, and ECSS3ReadOnlyAccess reads every bucket. Give the first to an admin user for your workstation, never to a server:

  1. On the instance's Users tab, click Add user, name it, for example admin, and click Add.
  2. Add the ECSS3FullAccess policy.
  3. Click Access key and save both parts. The secret is shown once; UpCloud doesn't store it.

For a server, write a custom policy in the JSON format AWS uses:

backup-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBackupBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::acme-server-backups"
    },
    {
      "Sid": "ReadWriteBackups",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::acme-server-backups/*"
    }
  ]
}
  • s3:ListBucket takes the bucket ARN and lets aws s3 ls and aws s3 sync list the bucket. Object actions take the ARN ending in /*.
  • s3:PutObject uploads, s3:GetObject downloads for restores and checks, and s3:AbortMultipartUpload cleans up a failed upload.
  • s3:DeleteObject lets a backup tool prune old copies; without s3:DeleteObjectVersion, a delete in a versioned bucket only adds a delete marker. Drop it if lifecycle rules do the pruning.

The API takes the policy URL-encoded inside a JSON body, up to 6,144 characters. tojson compacts the file and @uri encodes it:

Terminal
jq '{name: "backups-acme-server-backups", description: "Read and write acme-server-backups only", document: (tojson | @uri)}' backup-policy.json > policy-request.json

Create the policy and the user, attach one to the other, and make the user's key:

Terminal
curl -sS -X POST "https://api.upcloud.com/1.3/object-storage-2/$SERVICE/policies" -H "Authorization: Bearer $UPCLOUD_TOKEN" -H "Content-Type: application/json" -d @policy-request.json
Terminal
curl -sS -X POST "https://api.upcloud.com/1.3/object-storage-2/$SERVICE/users" -H "Authorization: Bearer $UPCLOUD_TOKEN" -H "Content-Type: application/json" -d '{"username": "web-01-backup"}'
Terminal
curl -sS -X POST "https://api.upcloud.com/1.3/object-storage-2/$SERVICE/users/web-01-backup/policies" -H "Authorization: Bearer $UPCLOUD_TOKEN" -H "Content-Type: application/json" -d '{"name": "backups-acme-server-backups"}'
Terminal
curl -sS -X POST "https://api.upcloud.com/1.3/object-storage-2/$SERVICE/users/web-01-backup/access-keys" -H "Authorization: Bearer $UPCLOUD_TOKEN"

The last response holds access_key_id and secret_access_key. The secret is returned only this once.

A user can have two active access keys. To rotate without downtime, create a second key, move the server to it, then mark the old key Inactive and delete it.

Configure the AWS CLI

~/.aws/config
[profile upcloud]
region = europe-1
endpoint_url = https://abcd1.upcloudobjects.com
request_checksum_calculation = WHEN_REQUIRED
response_checksum_validation = WHEN_REQUIRED
retry_mode = adaptive
max_attempts = 5
s3 =
  multipart_threshold = 100MB
  multipart_chunksize = 64MB
  max_concurrent_requests = 20
~/.aws/credentials
[upcloud]
aws_access_key_id = <access_key_id>
aws_secret_access_key = <secret_access_key>
  • The checksum lines add integrity checksums only where an operation requires one; UpCloud says AWS CLI 2.23.0 and later need them to avoid Content-MD5 errors. UpCloud's guide writes response_checksum_calculation, but AWS reads response_checksum_validation.
  • retry_mode = adaptive retries with backoff, up to 5 attempts, and slows down when throttled.
  • Under s3, files over 100 MB upload in 64 MB parts, 20 at a time, as UpCloud suggests for large objects.

Run chmod 600 ~/.aws/config ~/.aws/credentials, since the secret is stored in plain text. Then list the bucket:

Terminal
aws s3 ls s3://acme-server-backups/ --profile upcloud --endpoint-url https://abcd1.upcloudobjects.com

--endpoint-url overrides the profile's endpoint_url, so the commands below leave it off. No output means an empty bucket and a working key. Skip the bare aws s3 ls: listing every bucket isn't in the policy. Listing another bucket should fail with AccessDenied.

Turn on versioning and expire old backups

Bucket settings need the admin key, so run these on your workstation with a second profile, upcloud-admin, set up the same way. Versioning keeps overwritten and deleted backups as older versions:

Terminal
aws s3api put-bucket-versioning --bucket acme-server-backups --versioning-configuration Status=Enabled --profile upcloud-admin
Terminal
aws s3api get-bucket-versioning --bucket acme-server-backups --profile upcloud-admin

The second should print "Status": "Enabled". Every version counts as stored data, so add a lifecycle rule. UpCloud warns that AWS CLI 2.23.0 and later can fail to set one with Missing required header for this request: Content-MD5, and suggests AWS CLI 2.22.32 or earlier, or s3cmd. For s3cmd, run s3cmd --configure -c ~/.s3cfg-upcloud and give the admin key, a blank region, abcd1.upcloudobjects.com as the endpoint, %(bucket)s.abcd1.upcloudobjects.com as the bucket template, and HTTPS. Then write the rule:

lifecycle.xml
<?xml version="1.0" encoding="UTF-8"?>
<LifecycleConfiguration>
  <Rule>
    <ID>expire-backups</ID>
    <Status>Enabled</Status>
    <Prefix></Prefix>
    <Expiration>
      <Days>30</Days>
    </Expiration>
    <NoncurrentVersionExpiration>
      <NoncurrentDays>7</NoncurrentDays>
    </NoncurrentVersionExpiration>
    <AbortIncompleteMultipartUpload>
      <DaysAfterInitiation>7</DaysAfterInitiation>
    </AbortIncompleteMultipartUpload>
  </Rule>
</LifecycleConfiguration>
  • An empty Prefix covers the whole bucket; web-01/ would cover one server.
  • Expiration turns each backup into an older version 30 days after upload, and NoncurrentVersionExpiration removes it for good 7 days later. A backup lives about 37 days, and an accidental delete can be undone for a week.
  • AbortIncompleteMultipartUpload clears parts of uploads that never finished, which still take up space.
Terminal
s3cmd -c ~/.s3cfg-upcloud setlifecycle lifecycle.xml s3://acme-server-backups
Terminal
s3cmd -c ~/.s3cfg-upcloud getlifecycle s3://acme-server-backups

Check that all three actions come back. A new configuration replaces the old one, as on S3, and applies to backups already in the bucket.

Upload backups on a schedule

Give each backup a dated name so nothing overwrites it, and store a checksum beside it. This script archives two directories with tar and uploads both files:

/usr/local/bin/upcloud-backup.sh
#!/bin/sh
set -eu
NAME="web-01-$(date +%F).tar.gz"
cd /var/backups
tar -czf "$NAME" /etc /var/www
sha256sum "$NAME" > "$NAME.sha256"
for f in "$NAME" "$NAME.sha256"; do
  aws s3 cp "$f" "s3://acme-server-backups/web-01/$f" --profile upcloud --only-show-errors
done
rm "$NAME" "$NAME.sha256"

set -eu stops at the first failure, so a failed upload never reaches the rm, and --only-show-errors keeps cron mail down to real problems. Make it executable with chmod 700, then schedule it:

/etc/cron.d/upcloud-backup
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
15 4 * * * root /usr/local/bin/upcloud-backup.sh

The PATH line matters: cron's default path often leaves out /usr/local/bin, where the AWS CLI v2 installer puts aws. See scheduling backups with cron.

UpCloud says load on its shared storage, and with it latency, rises around midnight to 2:00 AM in European time zones, when many backups run. Schedule outside that window or stagger servers.

Verify and restore a backup

Terminal
aws s3 ls s3://acme-server-backups/web-01/ --recursive --human-readable --summarize --profile upcloud

A listing proves the upload landed, not that it restores. Download one backup with its checksum file:

Terminal
mkdir -p /tmp/restore-test && cd /tmp/restore-test && aws s3 cp s3://acme-server-backups/web-01/ . --recursive --exclude "*" --include "web-01-2026-10-03.tar.gz*" --profile upcloud
Terminal
sha256sum -c web-01-2026-10-03.tar.gz.sha256 && tar -tzf web-01-2026-10-03.tar.gz > /dev/null && echo OK

Later filters win, so --exclude "*" then --include fetches just the two files. sha256sum -c prints web-01-2026-10-03.tar.gz: OK when the download matches the hash taken before upload, and tar -tzf reads the whole archive. Then extract into a scratch directory and check the files; see testing a restore.

To recover an overwritten or deleted backup, list its versions and fetch one by ID. The server key can't list versions, so use the admin profile:

Terminal
aws s3api list-object-versions --bucket acme-server-backups --prefix web-01/ --profile upcloud-admin
Terminal
aws s3api get-object --bucket acme-server-backups --key web-01/web-01-2026-10-03.tar.gz --version-id <version-id> web-01-2026-10-03.tar.gz --profile upcloud-admin

No Object Lock yet: protect backups another way

As of October 2026, UpCloud lists Object Lock as coming soon and the object retention and legal hold APIs as unsupported. Nothing on the storage side stops a user with full access, or anyone controlling your UpCloud account, from deleting backups. So:

  • Keep versioning on and give servers only a policy like the one above, so their keys can't delete versions or change bucket settings.
  • Keep the admin key and API tokens off servers, and turn on termination protection.
  • Keep a second copy with another provider, per the 3-2-1 rule. See protecting backups from ransomware.

Data is encrypted at rest with AES-256 under keys UpCloud manages. The bucket encryption API calls aren't supported, so for keys only you hold, encrypt backups before upload.

What it costs

Managed Object Storage is billed on stored data in 250 GB steps, each billed by the hour. A new instance starts at 250 GB and grows another 250 GB whenever usage crosses a step, with no upper limit. As of October 2026, UpCloud's cost calculator lists each 250 GB at €5.00 a month. Egress is zero-cost, and the billing page lists no request charges.

Stored (backups and old versions)Billed asPer month
100 GB250 GB€5.00
600 GB750 GB€15.00
2,000 GB2,000 GB€40.00

The minimum applies per instance, so use several buckets in one instance rather than an instance per server. For very heavy public traffic, UpCloud's Fair Transfer Policy applies: past the account's limit it sends a warning and may throttle to 100 Mbit/s. Private endpoint traffic doesn't count.

Limits and common errors

  • Objects up to 5 TB with multipart upload; parts up to 5 GiB.
  • Not supported: Object Lock, retention and legal hold, bucket replication, the Block Public Access and bucket encryption calls, and RestoreObject.
  • Missing required header for this request: Content-MD5: AWS CLI 2.23.0 or later. Add the two checksum lines; for lifecycle rules, use s3cmd or AWS CLI 2.22.32 or earlier.
  • Checksum mismatch errors from other tools on newer AWS SDKs: set AWS_REQUEST_CHECKSUM_CALCULATION=when_required and AWS_RESPONSE_CHECKSUM_VALIDATION=when_required, as UpCloud advises.
  • Could not connect to the endpoint URL: the endpoint is wrong, public access is off, or a -private endpoint is used from outside its network.
  • InvalidAccessKeyId: the key is wrong, or the endpoint belongs to a different instance from the one the user was created in.

Frequently asked questions

What is the S3 endpoint for UpCloud Object Storage?
Each instance has its own, https://<id>.upcloudobjects.com, shown in its details; a private network adds https://<id>-private.upcloudobjects.com.
What region do I use for UpCloud Object Storage in the AWS CLI?
The instance's region in lowercase, such as europe-1, us-1 or apac-1, together with the instance's endpoint.
Does UpCloud Object Storage support Object Lock?
Not as of October 2026. UpCloud lists it as coming soon, and the retention and legal hold APIs as unsupported. Versioning and lifecycle rules work.
How much does UpCloud Object Storage cost?
As of October 2026, UpCloud's cost calculator lists €5.00 a month per 250 GB, billed hourly, with a 250 GB minimum per instance and no egress fees.
Can I limit an UpCloud Object Storage key to one bucket?
Yes. Create a custom policy whose resources are that bucket's ARNs, through the UpCloud API or Terraform, and attach it to a user of its own.

How this was checked

Commands, limits and prices were checked against these official pages, on October 3, 2026: