A provider can support the S3 operations your backup tool uses every night and still fail the operation you need during recovery. A successful upload is useful evidence. Retrieving an overwritten version, recovering through a delete marker and checking the recovered bytes is stronger evidence.
This tutorial supplies a small, downloadable compatibility and restore lab for evaluating European S3 endpoints. It deliberately produces operation-level results rather than a provider league table. You can run the same synthetic fixture against a dedicated test bucket, keep the raw result, and decide whether that specific endpoint meets your application's contract.
Validation boundary, 2 November 2026: the supplied fixture passed against a local MinIO instance. A second local run against LocalStack surfaced a version-copy failure, retained in the download. No DanubeData, Hetzner, Scaleway, OVHcloud or other hosted endpoint was tested. There are no European performance, availability or recovery-time measurements in this article.
Download the complete S3 lab, safety tests and raw local results. For endpoint setup and commercial details, use the DanubeData object storage page; this lab evaluates behavior after that setup.
Define the operations you actually depend on
The fixture uses five synthetic object names under a newly generated UUID namespace. It uploads a small object with an explicit SHA-256 checksum, downloads and hashes it, checks a byte range, completes a two-part object, aborts an independent multipart upload, and exercises signed PUT and GET requests. Finally, it creates two versions, adds a delete marker, retrieves the original version and copies that version into a new current object.
It also reads bucket CORS and lifecycle configuration. Those two inspections have a different meaning from a successful data operation: a readable configuration is not proof of browser behavior or scheduled expiry. The report records that distinction.
The multipart payload is intentionally small: one 5 MiB part and one 128 KiB final part. This is a bounded correctness fixture, not an attempt to saturate a network connection. Your real object-size distribution, concurrency, encryption mode and backup client remain separate tests.
Run the isolated local fixture first
Use Python 3.12 or 3.13 and a running Docker engine. Unpack the archive into its own directory, then install the pinned client dependency in a virtual environment:
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
python -m unittest -v
python local_integration.py --backend localstack > my-localstack-results.json
The runner creates one disposable container with a random name and a random loopback port. It creates a fresh version-enabled bucket, writes an outside-namespace sentinel, executes the lab and confirms that the sentinel survives cleanup. It mounts neither your working directory nor the Docker socket into the container, and stops its own container afterward.
The pinned LocalStack fixture exits nonzero at the previous-version CopyObject operation with InvalidRequest. That is an observed failure, not a passing restore. The earlier upload, range, multipart and signed-URL checks passed; direct retrieval of the original version and scoped cleanup also passed.
The archive also contains a complete passing MinIO result and a --backend minio runner. Its reviewed MinIO image was already cached locally. Fresh image pulls were unavailable during validation, so the README explains how to supply a reviewed local image and record its digest. This old local fixture is not a recommendation to install an archived server in production.
Prepare a dedicated bucket on the European endpoint
Create a disposable private bucket through the provider's supported setup process, and enable versioning before the lab. Do not point the fixture at a customer's application bucket. Use synthetic content and a dedicated credential; record the provider, region, endpoint, test date and relevant bucket settings beside the result.
Copy the actual service endpoint and signing region from your account's current documentation or console. Do not infer them from a provider name or reuse another provider's region string. Select path-style or virtual-hosted addressing according to that endpoint's contract. Provider documentation, such as Hetzner's bucket and object reference, helps define what should be supported; the test establishes only what happened in your account.
Load credentials through your normal secret-loading process into AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, with AWS_SESSION_TOKEN when applicable. Set AWS_DEFAULT_REGION, S3_ENDPOINT and S3_BUCKET. Optional S3_ADDRESSING_STYLE defaults to path. Keep TLS verification enabled; configure a trusted CA bundle when your environment requires one.
python lab.py > provider-results.json
The script refuses remote plaintext endpoints, embedded URL credentials and endpoints containing a bucket path or query string. It requires versioning to be enabled, then checks its new namespace for current objects, old versions, delete markers and incomplete uploads before writing. A failed preflight stops the run.
Keep the permission boundary narrow
The README lists the needed object, version, listing and inspection permissions. Use the provider's supported policy model rather than copying an AWS policy without checking it. Listing versions and deleting versions are distinct capabilities from ordinary object reads and writes. An application key that can upload a backup may intentionally lack recovery or cleanup permissions.
The lab never modifies bucket versioning, policies, CORS, lifecycle, retention or Object Lock. It has no delete-bucket operation. Every invocation generates an unpredictable fresh namespace; there is no option to choose a familiar existing prefix accidentally.
By default it retains the synthetic versions for inspection. To remove the objects made by the current invocation, choose cleanup explicitly:
python lab.py --cleanup > provider-results.json
Cleanup enumerates versions only beneath that invocation's namespace, refuses unexpected object names, and deletes only its five exact keys and their versions. It aborts only multipart upload IDs it recorded. It then checks that its namespace is empty. It cannot remove a previous run's retained data. A killed process or a lost upload-creation response still needs manual inspection of the exact reported namespace.
Check bytes independently of ETags
The downloaded-byte SHA-256 comparison is the core integrity check. The small PUT also supplies a server checksum, and the report separately records whether the server returned the expected checksum on inspection. A missing server checksum is marked unverified; it is not silently promoted to support.
An ETag is used as an opaque token when completing multipart upload. Do not treat a multipart ETag as a whole-object MD5. The AWS integrity guide explains why checksum types and multipart construction matter.
The client configuration makes its checksum behavior explicit: this fixture sends the requested SHA-256 and disables automatic optional SDK checksum features. Repeat relevant tests with your application's actual SDK version and defaults before declaring a migration safe.
Restore through an actual delete marker
The restore fixture first preserves an original version ID, then overwrites the object and performs a normal versioned delete. It confirms that the current key is unavailable, downloads the original version by ID and verifies its bytes. Only then does it copy that version to become current and verify an ordinary GET.
Copying a previous version is a documented recovery approach in AWS's version-restore guide. Here it is a concrete compatibility requirement: the passing MinIO fixture exercised it, while the pinned LocalStack fixture failed at that operation. Neither result predicts what a European hosted endpoint will do.
This recovers one synthetic object. It does not validate your backup catalogue, restore a database consistently, recover encryption keys or rebuild an application. Add those application-level checks to the recovery drill that follows.
Read the report without turning unknowns into passes
| Result | What it establishes | What remains separate |
|---|---|---|
| Upload/download hash passed | These downloaded fixture bytes match | Durability and large-scale throughput |
| Multipart complete/abort passed | These parts completed and the separate upload was aborted | Crash recovery across arbitrary clients |
| Signed PUT/GET passed | Those short-lived signed requests worked | Browser CORS and other signed headers |
| Version copy restore passed | A deleted fixture became readable with original bytes | Object Lock, retention and complete application recovery |
| CORS/lifecycle absent | The endpoint reported no configured rule | Feature support and policy enforcement |
| Inspection not_verified | The inspection could not establish the setting | Whether the cause is permission, configuration or implementation |
Signed URLs are exercised without being printed; treat them as temporary bearer credentials. Reports still include bucket names, namespace identifiers and inspected configurations, so review them before sharing. The downloadable results contain synthetic local data only.
Extend the lab around your application
For a browser upload workflow, use the exact application origin, methods and headers in a real preflight and upload test. Reading CORS rules does not perform that check; AWS's CORS reference describes the origin/method/header matching involved. Our Laravel private-file and signed-upload guide covers the application authorization side.
For lifecycle expiry, prepare separate synthetic versions and inspect them after the configured interval. For retention, build a separate authorized Object Lock drill. For costs, model your stored current and noncurrent versions, requests, incomplete uploads and transfer using current account rates. This lab supplies no bill comparison or fastest-provider claim.
Keep the raw result with the client version, endpoint and settings in your storage acceptance record. A narrow, reproducible passing contract is useful. An untested operation remains unverified even when a provider, or a local emulator, describes itself as S3 compatible.