> ## 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.

# Storage backends

> Configure private uploads with MinIO, AWS S3, or Cloudflare R2.

## Overview

Kaneo uses S3-compatible object storage for uploads in:

* task descriptions
* task comments

The browser uploads directly to the configured storage backend using presigned URLs.

Kaneo then serves uploaded assets back through its own API.

Current behavior:

* images render inline
* non-image files such as CSV, PDF, and ZIP render as attachment cards/links
* assets are private by default

That means one rule matters for every backend:

* `S3_ENDPOINT` must be reachable by the browser

Do not use a Docker-internal hostname such as `http://minio:9000` for a public deployment unless the browser can actually reach it.

## Required Kaneo variables

```env theme={"theme":{"light":"min-light","dark":"min-dark"}}
S3_ENDPOINT=
S3_BUCKET=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=false
```

Notes:

* `S3_FORCE_PATH_STYLE=true` is usually needed for MinIO.
* `S3_FORCE_PATH_STYLE=false` is usually correct for AWS S3 and R2.
* `S3_PUBLIC_BASE_URL` is optional and not required for the current private asset flow.

## MinIO

MinIO is the recommended self-hosted option.

### Local Docker setup

For local development, this is fine:

```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
```

This works when Kaneo and MinIO are on the same Docker network and your browser reaches MinIO through `localhost`.

### Public deployment

For a public deployment, expose MinIO on its own hostname through your reverse proxy.

Example:

* Kaneo: `https://cloud.kaneo.app`
* MinIO: `https://files.cloud.kaneo.app`

This can be done with Caddy, Nginx, Traefik, or any other reverse proxy.

Then use:

```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
```

You also need:

* a created bucket
* a dedicated, non-root MinIO user with access limited to that bucket
* MinIO CORS allowing your Kaneo origin
* no anonymous bucket read policy is required

<Warning>
  Never expose MinIO with its default `minioadmin` root credentials. Generate
  unique credentials for every deployment and keep the MinIO Console private.
</Warning>

## AWS S3

AWS S3 is the simplest managed option.

Use a bucket and an IAM user with access to that bucket.

Example:

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

For another AWS region, adjust:

* `S3_ENDPOINT`

### Using an IAM role instead of an access key

If Kaneo runs on AWS (EC2, ECS, or EKS), you can omit `S3_ACCESS_KEY_ID` and
`S3_SECRET_ACCESS_KEY` entirely and let Kaneo authenticate with the instance's
IAM role. Leave both unset and the AWS SDK resolves credentials from its default
provider chain (instance profile, ECS task role, or EKS IRSA).

```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 omitted; the IAM role is used
```

Set both keys or neither: configuring only one is rejected. The role must allow
`s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject` on the bucket.

Recommended S3 CORS policy:

```json theme={"theme":{"light":"min-light","dark":"min-dark"}}
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "HEAD"],
    "AllowedOrigins": ["https://cloud.kaneo.app"],
    "ExposeHeaders": ["ETag"]
  }
]
```

## Cloudflare R2

R2 works well because it exposes an S3-compatible API.

Use your account endpoint, bucket, and R2 access keys.

Example:

```env theme={"theme":{"light":"min-light","dark":"min-dark"}}
S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_BUCKET=kaneo-uploads
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_REGION=auto
S3_FORCE_PATH_STYLE=false
```

Notes:

* `S3_REGION=auto` is typical for R2
* a public bucket is not required for Kaneo's current private asset flow

## One copy-paste self-hosted example

This example gives you Kaneo + MinIO in one Compose file.

```yml theme={"theme":{"light":"min-light","dark":"min-dark"}}
services:
  postgres:
    image: postgres:16-alpine
    env_file:
      - .env
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U kaneo -d kaneo"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    image: ghcr.io/usekaneo/api:latest
    env_file:
      - .env
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped

  web:
    image: ghcr.io/usekaneo/web:latest
    env_file:
      - .env
    depends_on:
      - api
    restart: unless-stopped

  minio:
    image: minio/minio:latest
    command: server /data --console-address ":9001"
    env_file:
      - .env
    volumes:
      - minio_data:/data
    restart: unless-stopped

volumes:
  postgres_data:
  minio_data:
```

Matching `.env`:

```env theme={"theme":{"light":"min-light","dark":"min-dark"}}
KANEO_CLIENT_URL=https://cloud.kaneo.app
KANEO_API_URL=https://cloud.kaneo.app/api

MINIO_ROOT_USER=<generate-a-unique-admin-user>
MINIO_ROOT_PASSWORD=<generate-a-strong-random-secret>

S3_ENDPOINT=https://files.cloud.kaneo.app
S3_BUCKET=kaneo-uploads
S3_ACCESS_KEY_ID=<generate-a-unique-admin-user>
S3_SECRET_ACCESS_KEY=<generate-a-strong-random-secret>
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true
```

Before testing uploads:

1. Create the `kaneo-uploads` bucket.
2. Configure MinIO CORS for your Kaneo origin.
3. Make sure `files.cloud.kaneo.app` resolves publicly.

## Troubleshooting

If uploads fail:

* check that the bucket exists
* check that `S3_ENDPOINT` is public and browser-reachable
* check CORS on your storage backend
* check that the access key can `PutObject` and `GetObject`
