> ## Documentation Index
> Fetch the complete documentation index at: https://kaneo.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Object storage and uploads

> Configure S3-compatible storage for private images and attachments in task descriptions and comments.

## Overview

Kaneo supports private uploads in task descriptions and task comments.

Uploads use an S3-compatible object storage backend:

* the Kaneo API creates presigned upload URLs
* the browser uploads files directly to the storage backend
* Kaneo finalizes uploads into private asset records
* Kaneo serves uploaded files back through its own API

This means Kaneo can work with multiple backends as long as they expose a compatible S3-style API.

Current behavior:

* images render inline
* other files such as CSV, PDF, or ZIP are inserted as attachment cards/links
* uploaded files are private by default and are not meant to be served from public bucket URLs

If you want backend-specific setup examples, see the [storage backends guide](/docs/core/installation/storage-backends).

## Recommended local setup: MinIO

For local or self-hosted deployments, MinIO is the recommended storage backend.

It gives you:

* a stable S3-compatible API
* a web console for bucket management
* a simple Docker setup
* better compatibility for direct browser uploads

Example `.env` values:

```env theme={"theme":{"light":"min-light","dark":"min-dark"}}
MINIO_ROOT_USER=kaneo-local
MINIO_ROOT_PASSWORD=<choose-a-local-development-password>

S3_ENDPOINT=http://minio:9000
S3_BUCKET=kaneo-uploads
S3_ACCESS_KEY_ID=kaneo-local
S3_SECRET_ACCESS_KEY=<choose-a-local-development-password>
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true
```

Create the bucket before using uploads.

You do not need to make the bucket public.

## Important: internal Docker URLs are not enough in production

When Kaneo generates a presigned upload URL, the browser uploads directly to your storage backend.

That means `S3_ENDPOINT` must be reachable by the browser, not just by Docker containers.

For example:

* `http://minio:9000` works only inside Docker
* `https://files.example.com` works from the browser

If your Kaneo deployment is public, do not leave `S3_ENDPOINT` set to `http://minio:9000`.

Use a public MinIO hostname instead, for example:

```env theme={"theme":{"light":"min-light","dark":"min-dark"}}
S3_ENDPOINT=https://files.cloud.kaneo.app
S3_BUCKET=kaneo-uploads
S3_ACCESS_KEY_ID=kaneo
S3_SECRET_ACCESS_KEY=<generate-a-strong-random-secret>
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true
```

<Warning>
  Never expose MinIO with its default `minioadmin` root credentials. Create a
  dedicated, bucket-scoped user and generate unique credentials for every
  deployment.
</Warning>

## Recommended public setup

The simplest production setup is:

* `cloud.kaneo.app` for Kaneo
* `files.cloud.kaneo.app` for MinIO

Expose MinIO on its own public hostname through your reverse proxy.

Why this is needed:

* Kaneo signs uploads against `S3_ENDPOINT`
* the browser uses that signed URL directly
* Docker-internal names like `minio` are not resolvable from a user's browser

Using a dedicated subdomain is recommended over proxying MinIO under a path.

Reads do not need a public bucket URL, because Kaneo serves uploaded assets back through `/api/asset/:id`.

## Other S3-compatible backends

Kaneo is not tied to MinIO. Any deployment can point Kaneo at another S3-compatible backend by changing the `S3_*` environment variables.

Examples:

* AWS S3
* Cloudflare R2
* MinIO
* [`fs`](https://github.com/ferdzo/fs)

## Using `fs`

`fs` is another S3-compatible option if you want a lightweight local object store. It is built by friends of Kaneo.

When using `fs`, configure Kaneo like this:

```env theme={"theme":{"light":"min-light","dark":"min-dark"}}
S3_ENDPOINT=http://fs:2600
S3_BUCKET=kaneo-uploads
S3_ACCESS_KEY_ID=<your-access-key>
S3_SECRET_ACCESS_KEY=<your-secret-key>
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true
```

Important notes for `fs`:

* buckets are created through the S3 API or CLI, not through a web dashboard
* Kaneo still expects the bucket to exist before uploads start
* direct browser uploads may need reverse-proxy CORS headers, because `fs` does not currently implement S3 CORS APIs

## Required Kaneo storage variables

Set these in your Kaneo deployment:

| Name                   | Description                                                                                  |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| `S3_ENDPOINT`          | S3-compatible API endpoint used by Kaneo                                                     |
| `S3_BUCKET`            | Bucket used for uploaded files                                                               |
| `S3_ACCESS_KEY_ID`     | Access key used to create presigned upload URLs. Optional when using an IAM role (see below) |
| `S3_SECRET_ACCESS_KEY` | Secret key used to create presigned upload URLs. Optional when using an IAM role (see below) |
| `S3_REGION`            | Region used for signing                                                                      |
| `S3_FORCE_PATH_STYLE`  | Usually `true` for MinIO and `fs`                                                            |

Optional:

| Name                        | Description                                                                                     |
| --------------------------- | ----------------------------------------------------------------------------------------------- |
| `S3_PUBLIC_BASE_URL`        | Optional public asset base URL. Kaneo does not require this for the current private asset flow. |
| `S3_MAX_IMAGE_UPLOAD_BYTES` | Maximum allowed upload size in bytes for images and other uploaded files                        |
| `S3_PRESIGN_TTL_SECONDS`    | Presigned upload URL lifetime                                                                   |

## Using an IAM role (AWS S3) instead of static keys

When Kaneo runs on AWS (EC2, ECS, or EKS), you can let it authenticate with an
IAM role instead of long-lived access keys.

Leave both `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` unset. Kaneo then falls
back to the AWS SDK's default credential provider chain, which automatically
picks up credentials from:

* an EC2 instance profile
* an ECS task role
* an EKS service account / IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`)
* standard `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` environment variables
* a shared `~/.aws/credentials` profile

Example for AWS S3 with an attached IAM role:

```env theme={"theme":{"light":"min-light","dark":"min-dark"}}
S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
S3_BUCKET=kaneo-uploads
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=false
# S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY intentionally omitted
```

The IAM role must allow `s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject`
on the bucket.

<Warning>
  Set both keys or neither. Configuring only one of `S3_ACCESS_KEY_ID` /
  `S3_SECRET_ACCESS_KEY` is treated as a misconfiguration and Kaneo will refuse
  to start the storage client.
</Warning>

## Testing uploads

Once storage is configured:

1. Open a task.
2. Paste, drag, or select a file inside the description or a comment.
3. Confirm images render inline and other files render as attachment cards/links.

If uploads fail:

* confirm the bucket exists
* confirm the credentials can write to the bucket
* confirm the storage endpoint is reachable from the browser
* confirm `S3_ENDPOINT` is a public URL, not an internal Docker hostname
* if using direct browser uploads, confirm CORS is configured correctly for your storage endpoint
