oleander-whirlwind
10/29/2025, 10:00 AMMarvin
10/29/2025, 10:00 AMMarvin
10/29/2025, 10:06 AM->, not ->)
- Prefer a presigned URL for the artifact image (especially if your MinIO is private or HTTP-only); the Prefect UI renders the image by fetching that URL from your browser
- Install s3fs since RemoteFileSystem uses fsspec S3 under the hood: pip install s3fs
- Save the block once (outside normal runs), then load it in tasks/flows
Here’s a cleaned-up version that uploads the file and creates an image artifact from a presigned URL:
import os
from datetime import timedelta
from prefect import flow, task
from prefect.filesystems import RemoteFileSystem
from prefect.artifacts import create_image_artifact
from minio import Minio
BLOCK_NAME = "prefect-results-minio-s3"
def ensure_block():
# Run once to create/update the block, then comment out
rfs = RemoteFileSystem(
basepath="<s3://prefect-test/results/>",
settings={
# s3fs / fsspec options
"key": os.getenv("MINIO_ACCESS_KEY", "um_minio"),
"secret": os.getenv("MINIO_SECRET_KEY", "um_minio"),
"use_ssl": False,
"client_kwargs": {"endpoint_url": "<http://host.docker.internal:9900>"},
# If your MinIO requires path-style addressing, uncomment:
# "config_kwargs": {"s3": {"addressing_style": "path"}},
},
)
rfs.save(BLOCK_NAME, overwrite=True)
@task(log_prints=True)
def upload_to_minio(local_path: str) -> tuple[str, str]:
rfs = RemoteFileSystem.load(BLOCK_NAME)
bucket = "prefect-test"
key = f"results/{os.path.basename(local_path)}"
remote_path = f"s3://{bucket}/{key}"
with open(local_path, "rb") as f:
rfs.write_path(remote_path, f.read())
print(f"Uploaded to {remote_path}")
return bucket, key
@task(log_prints=True)
def presign_minio_url(
endpoint: str,
access_key: str,
secret_key: str,
bucket: str,
key: str,
secure: bool = False,
expires: timedelta = timedelta(hours=1),
) -> str:
client = Minio(
endpoint.replace("http://", "").replace("https://", ""),
access_key=access_key,
secret_key=secret_key,
secure=secure,
)
url = client.presigned_get_object(bucket, key, expires=expires)
print(f"Presigned URL: {url}")
return url
@task(log_prints=True)
def make_artifact(image_url: str):
create_image_artifact(
image_url=image_url,
description="My plot from MinIO",
key="plot",
)
print(f"Artifact created for {image_url}")
return image_url
@flow
def my_flow(local_path: str = "results_and_artifacts/local_file_store/image.png"):
# ensure_block() # run once, or manage this outside the flow
bucket, key = upload_to_minio(local_path)
url = presign_minio_url(
endpoint="<http://host.docker.internal:9900>",
access_key=os.getenv("MINIO_ACCESS_KEY", "um_minio"),
secret_key=os.getenv("MINIO_SECRET_KEY", "um_minio"),
bucket=bucket,
key=key,
secure=False,
)
return make_artifact(url)
if __name__ == "__main__":
# ensure_block() # uncomment to create/update the block
my_flow()
Notes and gotchas:
- The Prefect UI displays the image by loading it from your browser. If your URL is HTTP while the UI is HTTPS (e.g., Prefect Cloud), browsers may block mixed content; prefer HTTPS for the presigned URL.
- If your MinIO is not reachable from the machine where you view the UI, the image won’t render. Presigned + publicly reachable HTTPS endpoint is the safest route.
- Your original public_url = f"{endpoint}/{bucket}/{key}" will only work if that endpoint is directly accessible from your browser and the bucket is publicly readable.
Docs for reference:Marvin
10/29/2025, 10:06 AMoleander-whirlwind
10/29/2025, 10:23 AMMarvin
10/29/2025, 10:23 AMMarvin
10/29/2025, 10:24 AM<http://host.docker.internal:9900>.
You can handle this a few ways:
Option A: Pick the endpoint dynamically at runtime
import os
from pathlib import Path
def minio_endpoint(default_port="9900") -> str:
# Allow explicit override
if ep := os.getenv("MINIO_ENDPOINT"):
return ep
# Heuristic: inside a container -> use host.docker.internal
in_container = Path("/.dockerenv").exists() or os.getenv("RUNNING_IN_CONTAINER") == "1"
host = "host.docker.internal" if in_container else "localhost"
return f"http://{host}:{default_port}"
Use it everywhere you construct clients or file systems:
endpoint = minio_endpoint()
# s3fs via RemoteFileSystem
rfs = RemoteFileSystem(
basepath="<s3://prefect-test/results/>",
settings={
"key": os.getenv("MINIO_ACCESS_KEY", "um_minio"),
"secret": os.getenv("MINIO_SECRET_KEY", "um_minio"),
"use_ssl": False,
"client_kwargs": {"endpoint_url": endpoint},
# "config_kwargs": {"s3": {"addressing_style": "path"}}, # if needed
},
)
# MinIO SDK for presigning
from minio import Minio
client = Minio(
endpoint.replace("http://", "").replace("https://", ""),
access_key=os.getenv("MINIO_ACCESS_KEY", "um_minio"),
secret_key=os.getenv("MINIO_SECRET_KEY", "um_minio"),
secure=False,
)
Option B: Keep two blocks and choose one
- Create two blocks:
- rfs-minio-host with endpoint_url="<http://localhost:9900>"
- rfs-minio-devcontainer with endpoint_url="<http://host.docker.internal:9900>"
- Load based on environment:
block_name = "rfs-minio-devcontainer" if Path("/.dockerenv").exists() else "rfs-minio-host"
rfs = RemoteFileSystem.load(block_name)
Important notes for your artifact image URL
- Whatever URL you pass to create_image_artifact must be reachable from the machine where you view the Prefect UI. If you view locally, <http://localhost:9900/bucket/key> can work. If you view Prefect Cloud UI from another machine, localhost or host.docker.internal won’t work — use a resolvable host/IP and preferably HTTPS.
- Browsers may block mixed content (HTTPS UI trying to load HTTP image). If you’re using Prefect Cloud (HTTPS), serve the image over HTTPS or expect it to be blocked.
References
- RemoteFileSystem API: prefect.filesystems.RemoteFileSystem
- Artifacts how-to: Artifacts how-to
- create_image_artifact API: prefect.artifactsoleander-whirlwind
10/29/2025, 10:32 AMMarvin
10/29/2025, 10:32 AMMarvin
10/29/2025, 10:33 AMhost.docker.internal:9900 but your browser fetches via localhost:9900 (or vice versa), MinIO will return SignatureDoesNotMatch.
- Other causes: path vs virtual-host style mismatch, system clock skew, wrong keys, HTTP vs HTTPS changes via a proxy.
What to do
1) Presign with the exact public endpoint your browser will use
Since you’re viewing from the host, presign with `http://localhost:9900`:
@task
def presign_for_browser(bucket, key) -> str:
endpoint = "<http://localhost:9900>" # this must match how your browser reaches MinIO
client = Minio(
endpoint.replace("http://", "").replace("https://", ""),
access_key=os.getenv("MINIO_ACCESS_KEY", "um_minio"),
secret_key=os.getenv("MINIO_SECRET_KEY", "um_minio"),
secure=False,
)
return client.presigned_get_object(bucket, key, expires=timedelta(hours=1))
2) Keep your writer endpoint independent
It’s fine if your RemoteFileSystem inside a container uses host.docker.internal, but keep the presign endpoint set to the browser-facing host (likely localhost).
3) Force path-style addressing for s3fs (recommended for MinIO)
RemoteFileSystem(
basepath="<s3://prefect-test/results/>",
settings={
"key": "...",
"secret": "...",
"use_ssl": False,
"client_kwargs": {"endpoint_url": "http://<host-here>:9900"},
"config_kwargs": {"s3": {"addressing_style": "path"}},
},
)
4) Quick checks
- Time sync: ensure your machine clock is accurate.
- Keys: confirm the exact access/secret used to write and to presign are the same.
- Test with curl to confirm host consistency:
curl -v "<the exact presigned URL you generated>"
If you’d like, paste:
- The exact hostname/port used to generate the presigned URL
- The hostname/port you open in your browser
- Whether MinIO is behind any proxy
I can tailor the snippet to your setup.
References:
- RemoteFileSystem API: prefect.filesystems.RemoteFileSystem
- Artifacts how-to: Artifacts how-to
- create_image_artifact API: prefect.artifactsoleander-whirlwind
10/29/2025, 10:39 AMMarvin
10/29/2025, 10:39 AMMarvin
10/29/2025, 10:40 AMMINIO_INTERNAL_ENDPOINT=<http://host.docker.internal:9900>
MINIO_PUBLIC_ENDPOINT=<http://localhost:9900>
MINIO_ACCESS_KEY=um_minio
MINIO_SECRET_KEY=um_minio
Code changes (note the different endpoints):
import os
from datetime import timedelta
from prefect import flow, task
from prefect.filesystems import RemoteFileSystem
from prefect.artifacts import create_image_artifact
from minio import Minio
BLOCK_NAME = "prefect-results-minio-s3"
def ensure_block():
rfs = RemoteFileSystem(
basepath="<s3://prefect-test/results/>",
settings={
"key": os.getenv("MINIO_ACCESS_KEY"),
"secret": os.getenv("MINIO_SECRET_KEY"),
"use_ssl": False,
"client_kwargs": {"endpoint_url": os.getenv("MINIO_INTERNAL_ENDPOINT")},
# Path-style is safer with MinIO:
"config_kwargs": {"s3": {"addressing_style": "path"}},
},
)
rfs.save(BLOCK_NAME, overwrite=True)
@task
def upload_to_minio(local_path: str) -> tuple[str, str]:
rfs = RemoteFileSystem.load(BLOCK_NAME)
bucket = "prefect-test"
key = f"results/{os.path.basename(local_path)}"
with open(local_path, "rb") as f:
rfs.write_path(f"s3://{bucket}/{key}", f.read())
return bucket, key
@task
def presign_for_browser(bucket: str, key: str) -> str:
public_endpoint = os.getenv("MINIO_PUBLIC_ENDPOINT") # e.g. <http://localhost:9900>
client = Minio(
public_endpoint.replace("http://", "").replace("https://", ""),
access_key=os.getenv("MINIO_ACCESS_KEY"),
secret_key=os.getenv("MINIO_SECRET_KEY"),
secure=False,
)
return client.presigned_get_object(bucket, key, expires=timedelta(hours=1))
@task
def make_artifact(url: str):
create_image_artifact(image_url=url, key="plot", description="My plot")
return url
@flow
def my_flow(local_path: str = "results_and_artifacts/local_file_store/image.png"):
# ensure_block() # run once to (re)create the block
bucket, key = upload_to_minio(local_path)
url = presign_for_browser(bucket, key)
return make_artifact(url)
Option 2 (recommended): Use one canonical hostname everywhere
Pick a name (e.g., minio.local) and make it resolve to the host from both the host and the container.
- On the host: add to /etc/hosts
- 127.0.0.1 minio.local
- In your dev container (docker compose):
- extra_hosts:
- "minio.local:host-gateway"
- Then use http://minio.local:9900 for both the RemoteFileSystem endpoint_url and for presigning. Signatures will always match, and both environments can connect.
Quick verification
- From host: curl http://localhost:9900/minio/health/ready
- From container: curl http://host.docker.internal:9900/minio/health/ready
- If using canonical host: curl http://minio.local:9900/minio/health/ready from both host and container
Also recommended for MinIO with s3fs
- Use path-style addressing in RemoteFileSystem settings:
- "config_kwargs": {"s3": {"addressing_style": "path"}}
If you share your current compose (or how you run the dev container), I can give you an exact extra_hosts snippet.
ReferencesMarvin
10/29/2025, 10:40 AMoleander-whirlwind
10/29/2025, 10:43 AMMarvin
10/29/2025, 10:43 AMMarvin
10/29/2025, 10:44 AMcreate_image_artifact.
Using the MinIO Client (mc):
# Point mc at your MinIO (from the host)
mc alias set miniolocal <http://localhost:9900> um_minio um_minio
# Make the whole bucket publicly downloadable
mc anonymous set download miniolocal/prefect-test
# (Optional) Restrict to just a prefix instead of the whole bucket
mc anonymous set download miniolocal/prefect-test/results
Then your artifact URL is:
<http://localhost:9900/prefect-test/results/image.png>
and you can create the artifact without presigning:
create_image_artifact(
image_url="<http://localhost:9900/prefect-test/results/image.png>",
key="plot",
description="My plot from MinIO",
)
Notes:
- This uses path-style URLs (`http://host:port/bucket/key`), which is the default pattern for MinIO.
- If you’re viewing the Prefect UI over HTTPS (e.g., Prefect Cloud), browsers may block loading an HTTP image (mixed content). For a quick local test with Prefect Server (HTTP), this is fine. For Cloud, you’ll want HTTPS on your MinIO endpoint or stick with presigned HTTPS links.
- To revert after testing:
mc anonymous set none miniolocal/prefect-test
# or for just the prefix:
mc anonymous set none miniolocal/prefect-test/results
If you prefer to limit exposure further, you can keep the bucket private and only make a specific results/ prefix public as shown above.oleander-whirlwind
10/29/2025, 10:44 AMMarvin
10/29/2025, 10:44 AMMarvin
10/29/2025, 10:45 AMimport os, json
from minio import Minio
# Use the INTERNAL endpoint that your code can reach (container -> host.docker.internal, host -> localhost)
INTERNAL_ENDPOINT = os.getenv("MINIO_INTERNAL_ENDPOINT", "<http://localhost:9900>")
ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "um_minio")
SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "um_minio")
bucket = "prefect-test"
prefix = "results/"
client = Minio(
INTERNAL_ENDPOINT.replace("http://", "").replace("https://", ""),
access_key=ACCESS_KEY,
secret_key=SECRET_KEY,
secure=False,
)
# Ensure bucket exists (optional)
if not client.bucket_exists(bucket):
client.make_bucket(bucket)
# Public-read policy for just this prefix
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAnonReadPrefix",
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": ["s3:GetObject"],
"Resource": [f"arn:aws:s3:::{bucket}/{prefix}*"],
}
],
}
# Some MinIO SDK versions expose set_bucket_policy; if not, use boto3's put_bucket_policy
client.set_bucket_policy(bucket, json.dumps(policy))
print("Policy set for prefix:", prefix)
Then the artifact URL is just the PUBLIC endpoint your browser will load:
PUBLIC_ENDPOINT = os.getenv("MINIO_PUBLIC_ENDPOINT", "<http://localhost:9900>")
key = "results/image.png"
public_url = f"{PUBLIC_ENDPOINT}/{bucket}/{key}"
# Prefect artifact
from prefect.artifacts import create_image_artifact
create_image_artifact(image_url=public_url, key="plot", description="My plot")
Notes:
- Use your INTERNAL endpoint only for API calls (SDK, uploads). Use the PUBLIC endpoint in the URL you show to the browser/UI. They can be different (e.g., host.docker.internal vs localhost).
- If your MinIO SDK version does not have client.set_bucket_policy, a safe fallback is boto3:
- endpoint_url=INTERNAL_ENDPOINT, aws_access_key_id/secret_access_key, then s3.put_bucket_policy(Bucket=bucket, Policy=json.dumps(policy)).
- If you’re viewing Prefect Cloud (HTTPS), loading an HTTP image may be blocked by the browser (mixed content). For local Prefect Server (HTTP), it’s fine. For Cloud, serve MinIO over HTTPS or stick to presigned HTTPS URLs.oleander-whirlwind
10/29/2025, 10:47 AMMarvin
10/29/2025, 10:47 AMMarvin
10/29/2025, 10:48 AMfrom prefect.filesystems import RemoteFileSystem
rfs = RemoteFileSystem(
basepath="<s3://prefect-test/results/>",
settings={
"key": os.getenv("MINIO_ACCESS_KEY", "um_minio"),
"secret": os.getenv("MINIO_SECRET_KEY", "um_minio"),
"use_ssl": False,
"client_kwargs": {"endpoint_url": "<http://host.docker.internal:9900>"},
"config_kwargs": {"s3": {"addressing_style": "path"}},
},
)
rfs.save("prefect-results-minio-s3", overwrite=True)
2) One-time: make the prefix public with the MinIO Python SDK (or mc)
import json, os
from minio import Minio
client = Minio(
"host.docker.internal:9900", # INTERNAL endpoint your code can reach
access_key=os.getenv("MINIO_ACCESS_KEY", "um_minio"),
secret_key=os.getenv("MINIO_SECRET_KEY", "um_minio"),
secure=False,
)
bucket = "prefect-test"
prefix = "results/"
policy = {
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowAnonReadPrefix",
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": ["s3:GetObject"],
"Resource": [f"arn:aws:s3:::{bucket}/{prefix}*"],
}],
}
client.set_bucket_policy(bucket, json.dumps(policy))
3) After upload, construct a plain public URL for the artifact
- Use the PUBLIC endpoint your browser will use (likely localhost when you’re on the host):
PUBLIC_ENDPOINT = "<http://localhost:9900>"
bucket = "prefect-test"
key = "results/image.png"
public_url = f"{PUBLIC_ENDPOINT}/{bucket}/{key}"
from prefect.artifacts import create_image_artifact
create_image_artifact(image_url=public_url, key="plot", description="My plot")
Notes:
- RemoteFileSystem cannot set public-read or create unsigned links by itself; it only handles I/O.
- Keep INTERNAL and PUBLIC endpoints separate as needed (container vs host). Upload with host.docker.internal, display with localhost.
- If you’re viewing Prefect Cloud (HTTPS), browsers may block HTTP images (mixed content). Use HTTPS on MinIO or presigned HTTPS URLs.
References:
- RemoteFileSystem API: prefect.filesystems.RemoteFileSystem
- Artifacts how-to: Artifacts how-to
- create_image_artifact API: prefect.artifacts