<@ULVA73B9P> like this? from datetime import time...
# ask-marvin
o
@Marvin like this? from datetime import timedelta import os from prefect import flow, task from prefect.filesystems import RemoteFileSystem from prefect.artifacts import create_image_artifact # 1) Define and save a RemoteFileSystem block (run once, or do this in code) rfs = RemoteFileSystem( basepath="s3://prefect-test/results/", settings={ "key": "um_minio", "secret": "um_minio", "client_kwargs": { "endpoint_url": "http://host.docker.internal:9900" }, "use_ssl": False, # If your MinIO needs path-style addressing, uncomment: # "config_kwargs": {"s3": {"addressing_style": "path"}}, }, ) rfs.save("prefect-results-minio-s3", overwrite=True) from datetime import timedelta from minio import Minio @task(log_prints=True) def presign_minio_url(endpoint, access_key, secret_key, bucket, key, secure=False, expires=timedelta(hours=1)): client = Minio( endpoint.replace("http://", "").replace("https://", ""), access_key=access_key, secret_key=secret_key, secure=secure, ) return client.presigned_get_object(bucket, key, expires=expires) @task(log_prints=True) def upload_to_minio(local_path: str) -> str: # 2) Load the block and upload the file bytes rfs = RemoteFileSystem.load("prefect-results-minio-s3") bucket = "prefect-test" key = f"results/{os.path.basename(local_path)}" # e.g. results/myimage.png remote_path = f"s3://{bucket}/{key}" with open(local_path, "rb") as f: data = f.read() rfs.write_path(remote_path, data) # 3) Construct a browser URL for the uploaded object # For a public bucket or if MinIO serves objects over HTTP: # If your MinIO is using path-style addressing: endpoint = "http://host.docker.internal:9900" public_url = f"{endpoint}/{bucket}/{key}" return public_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(image_url) return image_url @flow def my_flow(local_path: str = "results_and_artifacts/local_file_store/image.png"): url = upload_to_minio(local_path) return make_artifact(url) if name == "__main__": my_flow()
m
thought for 353.4 seconds
Almost there! A few tweaks to make it work reliably with Prefect 3.x and MinIO: - Remove the angle brackets from your URLs/paths - Fix the HTML-escaped arrow in the type hint (
->
, 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:
Copy code
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:
- RemoteFileSystem API: prefect.filesystems.RemoteFileSystem - Artifacts how-to: Artifacts how-to - create_image_artifact API: prefect.artifacts
o
@Marvin through my dev container i acess the bucket via host.docker.internal but prefect runs on the host and then the endpoint must be named localhost
m
thought for 51.1 seconds
Yes — that’s exactly the nuance. If your flow/tasks run on the host, they need `http://localhost:9900`; if they run inside your dev container, they need
<http://host.docker.internal:9900>
. You can handle this a few ways: Option A: Pick the endpoint dynamically at runtime
Copy code
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:
Copy code
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:
Copy code
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.artifacts
o
@Marvin <Error> <Code>SignatureDoesNotMatch</Code> <Message> The request signature we calculated does not match the signature you provided. Check your key and signing method. </Message> <Key>results/image.png</Key> <BucketName>prefect-test</BucketName> <Resource>/prefect-test/results/image.png</Resource> <RequestId>1872EFA8A1B6E776</RequestId> <HostId> dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 </HostId> </Error>
m
thought for 64.8 seconds
That XML is a classic presigned URL mismatch. Most common culprit here: the hostname/port used to generate the signature doesn’t match the hostname/port your browser is using to fetch the URL. Why this happens - The signature includes the exact Host header and path. If you presign with
host.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`:
Copy code
@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)
Copy code
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:
Copy code
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.artifacts
o
@Marvin but when i use localhost for presign, minio will nor reach s3 ask run failed with exception: MaxRetryError("HTTPConnectionPool(host='localhost', port=9900): Max retries exceeded with url: /prefect-test?location= (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7fdbda983110>: Failed to establish a new connection: [Errno 111] Connection refused'))") Traceback (most recent call last): File "/home/labor/.local/lib/python3.12/site-packages/urllib3/connection.py", line 198, in _new_conn sock = connection.create_connection( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/labor/.local/lib/python3.12/site-packages/urllib3/util/connection.py", line 85, in create_connection raise err File "/home/labor/.local/lib/python3.12/site-packages/urllib3/util/connection.py", line 73, in create_connection sock.connect(sa) ConnectionRefusedError: [Errno 111] Connection refused The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/home/labor/.local/lib/python3.12/site-packages/urllib3/connectionpool.py", line 787, in urlopen response = self._make_request( ^^^^^^^^^^^^^^^^^^^ File "/home/labor/.local/lib/python3.12/site-packages/urllib3/connectionpool.py", line 493, in _make_request conn.request( File "/home/labor/.local/lib/python3.12/site-packages/urllib3/connection.py", line 494, in request self.endheaders() File "/usr/local/lib/python3.12/http/client.py", line 1333, in endheaders self._send_output(message_body, encode_chunked=encode_chunked) File "/usr/local/lib/python3.12/http/client.py", line 1093, in _send_output self.send(msg) File "/usr/local/lib/python3.12/http/client.py", line 1037, in send self.connect() File "/home/labor/.local/lib/python3.12/site-packages/urllib3/connection.py", line 325, in connect self.sock = self._new_conn() ^^^^^^^^^^^^^^^^ File "/home/labor/.local/lib/python3.12/site-packages/urllib3/connection.py", line 213, in _new_conn raise NewConnectionError( urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x7fdbda983110>: Failed to establish a new connection: [Errno 111] Connection refused The above exception was the direct cause of the following exception:
m
thought for 76.3 seconds
Right — you’ve got two different “views” of the same MinIO, and they must not share the same host name: - From inside your dev container: use host.docker.internal:9900 - From your browser (and anything on the host): use localhost:9900 If you use localhost for the writer inside the container, it will fail to connect (your error). If you use host.docker.internal for the presigned URL, your browser will fetch with a different host than what was signed and MinIO will return SignatureDoesNotMatch. Two solid ways to fix this: Option 1: Use two endpoints (internal vs public) - INTERNAL endpoint: used by code that talks to MinIO (s3fs/RemoteFileSystem, Minio SDK when running inside the container) - PUBLIC endpoint: used only for presigning so your browser can fetch the image Set env vars:
Copy code
MINIO_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):
Copy code
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. References
- RemoteFileSystem API: prefect.filesystems.RemoteFileSystem - Artifacts how-to: Artifacts how-to - create_image_artifact API: prefect.artifacts
o
@Marvin and as a quick workaround could w just create a public link with minio client without signing?
m
thought for 36.1 seconds
Yes — quickest workaround is to make the object (or bucket/prefix) publicly readable, then use a plain HTTP URL in
create_image_artifact
. Using the MinIO Client (mc):
Copy code
# 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:
Copy code
<http://localhost:9900/prefect-test/results/image.png>
and you can create the artifact without presigning:
Copy code
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:
Copy code
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.
o
@Marvin no i meant the minio python sdk
m
thought for 58.0 seconds
Yes. You can make a bucket/prefix publicly readable via the MinIO Python SDK by setting a bucket policy that allows anonymous GetObject, then use a plain URL in create_image_artifact. Example (set public-read on a prefix once; then use HTTP URL without signing):
Copy code
import 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:
Copy code
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.
o
@Marvin cant i do that with 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"}}, }, ) ?!?
m
thought for 42.2 seconds
Short answer: not with RemoteFileSystem alone. - RemoteFileSystem (fsspec/s3fs) can read/write objects, but it does not manage bucket policies/ACLs and it does not generate public links. - To use a plain, unsigned URL, the bucket/object must already be publicly readable. That policy change has to be done outside of RemoteFileSystem (via MinIO Python SDK, mc, or boto3). What you can do: 1) Keep using RemoteFileSystem to upload - Remove the angle brackets and prefer path-style addressing for MinIO.
Copy code
from 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)
Copy code
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):
Copy code
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