From aa9a7c9a7bd77c3e3f820365e0ed7255b2582d5b Mon Sep 17 00:00:00 2001 From: Etienne Prothon Date: Tue, 11 Aug 2026 00:08:59 +0200 Subject: [PATCH 1/2] fix: drop the /etc/hosts requirement by setting S3_PROXY_URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Presigned image URLs were built from S3_ENDPOINT_URL (http://minio:9000). That host only resolves inside the compose network, but the URLs are opened by the browser on the host — hence the `127.0.0.1 minio` prerequisite. pyro-api already supports S3_PROXY_URL, which rewrites the host of presigned URLs; this repo just never passed it. Set it to http://localhost:9000 (the port MinIO already publishes) and drop the hosts entry. tests/test_media.py runs on the host and had the same dependency, so it now prefers the public endpoint too. Its bucket assertions were stale — they expected an `admin` bucket, from before the SERVER_NAME prefix — and only surfaced once the test could connect at all; rewritten against the actual {SERVER_NAME}-alert-api-{org_id} naming. Verified end to end with no hosts entry: upload a detection, GET its /url, and load the returned URL from the host (200, matching bytes). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VkLLGbSBX8pMthZzfmTdsj --- .env.test | 4 ++++ README.md | 10 ++++++---- docker-compose.yml | 2 ++ tests/test_media.py | 13 ++++++++----- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.env.test b/.env.test index d15ee6f..cca44ff 100644 --- a/.env.test +++ b/.env.test @@ -18,6 +18,10 @@ S3_ENDPOINT_URL=http://minio:9000 S3_ACCESS_KEY=JANE_DOE S3_SECRET_KEY=JANE_DOE S3_REGION=us-east-1 +# Public S3 endpoint: `minio` above only resolves inside the compose network, but +# presigned URLs are opened by the browser. Works because boto3 presigns with SigV2, +# which ignores the Host -- a SigV4 backend would need a Host-rewriting proxy. +S3_PROXY_URL=http://localhost:9000 # Prefix used by pyro-api to build per-org S3 bucket names. Must be a valid # bucket-name prefix (lowercase letters, digits, hyphens). Empty => InvalidBucketName. diff --git a/README.md b/README.md index 1aca242..abfa4c4 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,9 @@ This repository provides a Docker Compose configuration to run a full Pyronear d ### Prerequisites * Docker and Docker Compose -* Add this line to your hosts file (`/etc/hosts` on Linux/macOS, `C:\Windows\System32\drivers\etc\hosts` on Windows): - ``` - 127.0.0.1 minio - ``` + +> **Upgrading?** The `127.0.0.1 minio` hosts entry is no longer needed (`S3_PROXY_URL` +> replaces it) — harmless if left in place, but you can remove it. --- @@ -104,6 +103,9 @@ docker logs engine * **MinIO console (S3 GUI)**: [http://localhost:9001](http://localhost:9001) * Manage buckets, upload/delete files +* **MinIO S3 API**: `minio:9000` from the compose network (`S3_ENDPOINT_URL`), + [http://localhost:9000](http://localhost:9000) from the host (`S3_PROXY_URL`, used + for presigned image URLs) --- diff --git a/docker-compose.yml b/docker-compose.yml index b73bec8..8ad2d71 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,8 @@ services: - S3_ACCESS_KEY=${S3_ACCESS_KEY} - S3_SECRET_KEY=${S3_SECRET_KEY} - S3_REGION=${S3_REGION} + # Host used in presigned URLs, which the browser opens (`minio` is compose-only). + - S3_PROXY_URL=${S3_PROXY_URL:-} - SERVER_NAME=${SERVER_NAME} - PLATFORM_URL=${PLATFORM_URL:-http://localhost:8080} - TEMPORAL_API_URL=${TEMPORAL_API_URL:-} diff --git a/tests/test_media.py b/tests/test_media.py index 0bffcdf..1ea047b 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -6,8 +6,8 @@ # Load environment variables from .env file load_dotenv() -# Get S3 endpoint URL and credentials from environment variables -s3_endpoint_url = os.getenv("S3_ENDPOINT_URL") + "/" +# Tests run on the host: prefer the public endpoint, `minio` is compose-only. +s3_endpoint_url = (os.getenv("S3_PROXY_URL") or os.getenv("S3_ENDPOINT_URL")) + "/" s3_access_key = os.getenv("S3_ACCESS_KEY") s3_secret_key = os.getenv("S3_SECRET_KEY") s3_region = os.getenv("S3_REGION") @@ -28,10 +28,13 @@ def s3_client(): def test_s3_bucket(s3_client): response = s3_client.list_buckets() - assert response["Buckets"][0]["Name"] == "admin" - assert response["Buckets"][1]["Name"].endswith("-alert-api-1") + bucket_names = [bucket["Name"] for bucket in response["Buckets"]] - bucket_contents = s3_client.list_objects_v2(Bucket=response["Buckets"][1]["Name"]) + # pyro-api creates one bucket per organization, named {SERVER_NAME}-alert-api-{org_id} + alert_buckets = [name for name in bucket_names if name.endswith("-alert-api-1")] + assert alert_buckets, bucket_names + + bucket_contents = s3_client.list_objects_v2(Bucket=alert_buckets[0]) print(bucket_contents) [item["Key"] for item in bucket_contents.get("Contents", [])] # assert keys != [] From 12ef881d406240b2bf169c2199bc34ba4558c10e Mon Sep 17 00:00:00 2001 From: Etienne Prothon Date: Tue, 11 Aug 2026 22:41:18 +0200 Subject: [PATCH 2/2] style: shorten comment to satisfy ruff E501 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VkLLGbSBX8pMthZzfmTdsj --- tests/test_media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_media.py b/tests/test_media.py index 1ea047b..adbf5bd 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -30,7 +30,7 @@ def test_s3_bucket(s3_client): response = s3_client.list_buckets() bucket_names = [bucket["Name"] for bucket in response["Buckets"]] - # pyro-api creates one bucket per organization, named {SERVER_NAME}-alert-api-{org_id} + # One bucket per organization: {SERVER_NAME}-alert-api-{org_id} alert_buckets = [name for name in bucket_names if name.endswith("-alert-api-1")] assert alert_buckets, bucket_names