s3-presigned-url-proxyv2026.07.27.0833

A URL in.
A signed S3 link out.

Your app builds a plain URL with the object key; the proxy signs a time-limited S3 link and 302-redirects to it, so the browser downloads straight from S3. One container, no AWS SDK wiring in every service. For local development.

s3-presigned-url-proxy
$ cp .env.example .env       # AWS_BUCKET + credentials
$ docker-compose up -d
$ curl http://localhost:8087/health
{"status":"ok"}

One request, one redirect.

The key comes in as a query parameter; a pre-signed GET URL goes out in the Location header, valid for four hours by default.

curl: health, a 302 to a pre-signed S3 URL, and a 400 for a missing objectkey The proxy's log: its settings at start, then each request

Responses

302 when it can. A reason when it can’t.

Methods GET, POST, PUT, PATCH and DELETE, on / or any path; the path is ignored. A request with only some of the required parameters still gets a 400, so a typo shows up instead of a broken link.

302Redirect to the pre-signed GET URL
307Passthrough: the same URL on another port, method and body kept
400A required parameter is missing, or the host isn’t allowed to pass through
500Signing failed, or the passthrough port is the one it came in on
/health200 {“status”:”ok”}, never passed through
S3 says403 or 404 if the key doesn’t exist: signing doesn’t check

Configuration

Every setting is an environment variable.

AWS_BUCKETThe bucket (required)
AWS_REGIONDefault us-east-1
AWS credentialsOptional; empty means boto3’s own chain
PRESIGNED_URL_EXPIRATIONSeconds a link lives; 14400, four hours
OBJECT_KEY_PARAMThe query parameter with the key; objectkey
REQUIRED_PARAMSComma list that must be present; objectkey
PASSTHROUGH_PORTRequests with none of the parameters go here, 307
PASSTHROUGH_HOSTSHosts allowed to pass through, with subdomains

The container

Read-only, capped, checked.

The compose file runs it on a read-only filesystem with no new privileges, a quarter of a CPU and 256 MB, and a health check every 30 seconds. Images for amd64 and arm64 are on Docker Hub, released on every push.

docker-compose.yml
read_only: true
tmpfs: [/tmp, /run]
security_opt: [no-new-privileges:true]
cpus: 0.25
mem_limit: 256m
healthcheck: every 30s, 3 retries
ports: ["8087:8088"]   # host 8087, container 8088

Get it

One container. Or one file.

yml

Docker Compose

From a clone

GitHub
$ docker-compose up -d
docker

Docker

The published image

Docker Hub
$ docker run -d -p 8087:8088 --env-file .env ugurcandede/s3-presigned-url-proxy:latest
py

From source

Python 3.11

Quick start
$ python app.py

Credentials need s3:GetObject on the bucket. Don’t expose it to the internet: there is no authentication.

RequiresDocker and Docker Compose, or Python 3.11
AWSA bucket, and credentials with s3:GetObject
Imageslinux/amd64, linux/arm64
ReleasesCalVer, on every push to master
Written inPython, Flask, boto3
Testspytest, AWS stubbed
LicenseMIT