diff --git a/apps/docs/guides/self-hosting/introduction.mdx b/apps/docs/guides/self-hosting/introduction.mdx
new file mode 100644
index 00000000..5b17d36a
--- /dev/null
+++ b/apps/docs/guides/self-hosting/introduction.mdx
@@ -0,0 +1,140 @@
+---
+title: "Introduction"
+description: "Overview and quick start to run Kan on your own infrastructure using Docker Compose."
+mode: "wide"
+tag: "NEW"
+---
+
+This guide introduces how to self-host Kan. It starts with the minimal Docker Compose setup (web + PostgreSQL) and points you to optional features like email and S3-based file storage.
+
+## What you’ll set up
+
+
+
+ Next.js application served on port 3000.
+
+
+ Primary database for Kan data.
+
+
+
+
+ For file uploads (avatars), OAuth, and other advanced options, see the
+ Environment Variables section in the README and the dedicated [S3
+ guide](/guides/self-hosting/s3). The [full
+ compose](https://github.com/kanbn/kan/blob/main/docker-compose.yml) in the
+ repo includes a richer configuration via .env.
+
+
+## Prerequisites
+
+- Docker and Docker Compose
+- A long random string for BETTER_AUTH_SECRET (32+ chars)
+
+## Quick start
+
+
+
+ Paste the following minimal configuration into a new docker-compose.yml file:
+
+ ```yaml
+ services:
+ web:
+ image: ghcr.io/kanbn/kan:latest
+ container_name: kan-web
+ ports:
+ - "3000:3000"
+ networks:
+ - kan-network
+ environment:
+ NEXT_PUBLIC_BASE_URL: http://localhost:3000
+ BETTER_AUTH_SECRET: your_auth_secret
+ POSTGRES_URL: postgresql://kan:your_postgres_password@postgres:5432/kan_db
+ NEXT_PUBLIC_ALLOW_CREDENTIALS: true
+ depends_on:
+ - postgres
+ restart: unless-stopped
+
+ postgres:
+ image: postgres:15
+ container_name: kan-db
+ environment:
+ POSTGRES_DB: kan_db
+ POSTGRES_USER: kan
+ POSTGRES_PASSWORD: your_postgres_password
+ ports:
+ - 5432:5432
+ volumes:
+ - kan_postgres_data:/var/lib/postgresql/data
+ restart: unless-stopped
+ networks:
+ - kan-network
+
+ networks:
+ kan-network:
+
+ volumes:
+ kan_postgres_data:
+ ```
+
+
+ The example above is intentionally minimal. The repository provides a more feature-complete compose file at [docker-compose.yml](https://github.com/kanbn/kan/blob/main/docker-compose.yml) if you want environment-based configuration, OAuth, S3, and more.
+
+
+
+
+
+ Bring everything up in detached mode:
+
+ ```bash
+ docker compose up -d
+ ```
+
+ Once started, open [http://localhost:3000](http://localhost:3000).
+
+
+
+
+ Useful commands while developing or testing:
+
+ - Stop the containers: docker compose down
+ - View logs: docker compose logs -f
+ - Restart: docker compose restart
+
+
+
+
+ For a production-like setup and more features (email, OAuth, file uploads, etc.), create a .env file and set the relevant variables shown in the README’s Environment Variables section.
+
+
+ ```bash
+ # Required
+ NEXT_PUBLIC_BASE_URL=http://localhost:3000
+ BETTER_AUTH_SECRET=replace_with_long_random_string
+ POSTGRES_URL=postgresql://kan:your_postgres_password@postgres:5432/kan_db
+
+ # Optional: Email
+ EMAIL_FROM="Kan "
+ SMTP_HOST=smtp.resend.com
+ SMTP_PORT=465
+ SMTP_USER=resend
+ SMTP_PASSWORD=re_xxxx
+ SMTP_SECURE=true
+
+ # Optional: Auth toggles
+ NEXT_PUBLIC_ALLOW_CREDENTIALS=true
+ NEXT_PUBLIC_DISABLE_SIGN_UP=false
+ ```
+
+
+ If you plan to enable file uploads (avatars, etc.), you’ll also need S3 variables (S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, NEXT_PUBLIC_STORAGE_URL, NEXT_PUBLIC_STORAGE_DOMAIN, …). See the S3 guide linked at the top.
+
+
+
+
+
+
+## Reference
+
+- [GitHub README](https://github.com/kanbn/kan/blob/main/README.md#self-hosting-)
+- [GitHub docker-compose.yml](https://github.com/kanbn/kan/blob/main/docker-compose.yml)
diff --git a/apps/docs/guides/self-hosting/s3.mdx b/apps/docs/guides/self-hosting/s3.mdx
new file mode 100644
index 00000000..29d8f2f5
--- /dev/null
+++ b/apps/docs/guides/self-hosting/s3.mdx
@@ -0,0 +1,397 @@
+---
+title: "Kan + MinIO (S3)"
+mode: "wide"
+tag: "NEW"
+---
+
+Deploy Kan with PostgreSQL and MinIO (S3-compatible storage) using Docker Compose, with clear steps and production notes.
+
+## What you’ll set up
+
+
+
+ Kan web app (Next.js), on port 3000.
+
+
+ PostgreSQL database for Kan.
+
+
+
+ MinIO object storage (console port 9001, S3 API port 9000).
+
+
+## How it works
+
+- Kan stores data in PostgreSQL.
+- Kan uploads files (e.g., avatars) to MinIO over the S3 API.
+- The browser fetches public files directly from MinIO’s public URL.
+- The Next.js image optimizer in Kan must be explicitly allowed to fetch from your storage host.
+
+Key domain settings:
+
+- NEXT_PUBLIC_BASE_URL → the Kan site
+- NEXT_PUBLIC_STORAGE_URL → the public S3 base URL
+- NEXT_PUBLIC_STORAGE_DOMAIN → the exact S3 hostname (no scheme)
+
+## Prerequisites
+
+- Docker and Docker Compose
+- Open local ports: 3000 (Kan), 5432 (Postgres), 9000/9001 (MinIO)
+- A long random string for BETTER_AUTH_SECRET (32+ chars)
+
+
+ For production you’ll want a reverse proxy (Traefik/Nginx/Caddy), valid TLS
+ certificates, and DNS for your domains (e.g., kan.example.com,{" "}
+ s3.example.com).
+
+
+## Quick start
+
+
+ Why localtest.me? It resolves to 127.0.0.1{" "}
+ automatically, so you can test domain-based configs locally without editing
+ hosts.
+
+
+
+
+ Provide the minimum required configuration (local example):
+
+ ```bash
+ NEXT_PUBLIC_BASE_URL=http://kan.localtest.me:3000
+ BETTER_AUTH_SECRET=
+ POSTGRES_URL=postgresql://kan:@postgres:5432/kan_db
+
+ # MinIO/S3
+ S3_ENDPOINT=http://s3.localtest.me:9000
+ S3_ACCESS_KEY_ID=
+ S3_SECRET_ACCESS_KEY=
+ S3_REGION=none
+ S3_FORCE_PATH_STYLE=true
+
+ # Public storage access
+ NEXT_PUBLIC_STORAGE_URL=http://s3.localtest.me:9000
+ NEXT_PUBLIC_STORAGE_DOMAIN=s3.localtest.me
+ NEXT_PUBLIC_AVATAR_BUCKET_NAME=kan
+ ```
+
+
+ Issue #109 fix: make sure NEXT_PUBLIC_STORAGE_DOMAIN exactly
+ equals the hostname that serves your images (no scheme, no port).
+
+
+ Optional (see README for full list): Email (`EMAIL_FROM`, `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_SECURE`), OAuth/OIDC (`GOOGLE_*`, `GITHUB_*`, `OIDC_*`), auth toggles, Trello import, etc.
+
+
+
+
+ You can start from the minimal compose at the repository root (docker-compose.yml) and review the production-oriented settings in cloud/docker-compose.yml.
+
+ Start with the minimal setup (web + postgres + minio) and ensure environment variables are passed to the web service.
+
+
+
+ ```yaml
+ services:
+ postgres:
+ image: postgres:15
+ container_name: kan-db
+ ports:
+ - "5432:5432"
+ environment:
+ POSTGRES_USER: kan
+ POSTGRES_PASSWORD: changeme
+ POSTGRES_DB: kan_db
+ volumes:
+ - pg_data:/var/lib/postgresql/data
+ restart: unless-stopped
+
+ minio:
+ image: minio/minio:latest
+ container_name: kan-minio
+ command: server /data --console-address ":9001"
+ ports:
+ - "9000:9000" # S3 API
+ - "9001:9001" # Console
+ environment:
+ # Use the same credentials in your .env
+ # as S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY
+ MINIO_ROOT_USER: minio
+ MINIO_ROOT_PASSWORD: minio123456789
+ volumes:
+ - minio_data:/data
+ restart: unless-stopped
+
+ web:
+ image: ghcr.io/kanbn/kan:latest
+ container_name: kan-web
+ depends_on:
+ - postgres
+ - minio
+ ports:
+ - "3000:3000"
+ # Load variables from .env
+ # (see the "Set environment variables" step)
+ env_file:
+ - .env
+ restart: unless-stopped
+
+ volumes:
+ pg_data:
+ minio_data:
+ ```
+
+
+ Ensure your .env contains values that match this compose file.
+ For example:
+
+ -
+
POSTGRES_URL=postgresql://kan:changeme@postgres:5432/kan_db
+
+ -
+
S3_ENDPOINT=http://s3.localtest.me:9000
+
+ -
+
S3_ACCESS_KEY_ID=minio and{" "}
+ S3_SECRET_ACCESS_KEY=minio123456789
+
+ -
+
S3_FORCE_PATH_STYLE=true
+
+ -
+
NEXT_PUBLIC_STORAGE_URL=http://s3.localtest.me:9000
+
+ -
+
NEXT_PUBLIC_STORAGE_DOMAIN=s3.localtest.me
+
+ -
+
NEXT_PUBLIC_AVATAR_BUCKET_NAME=kan
+
+
+
+
+
+
+
+
+
+
+ ```bash
+ docker compose up -d
+ ```
+
+ Then open:
+
+
+
+
+
+
+ 1) Log into the MinIO Console (http://minio.localtest.me:9001).
+
+ 2. Create a bucket (e.g., kan).
+
+ 3. For simple public avatars, apply a read-only policy so GET requests are allowed for objects:
+
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Principal": { "AWS": ["*"] },
+ "Action": ["s3:GetBucketLocation", "s3:ListBucket"],
+ "Resource": ["arn:aws:s3:::kan"]
+ },
+ {
+ "Effect": "Allow",
+ "Principal": { "AWS": ["*"] },
+ "Action": ["s3:GetObject"],
+ "Resource": ["arn:aws:s3:::kan/*"]
+ }
+ ]
+ }
+ ```
+
+
+ Alternatively, keep the bucket private and use presigned URLs. In that case,
+ ensure your server and browser access paths are correctly configured.
+
+
+
+
+
+
+ - Sign in to Kan and upload an avatar (Settings).
+ - Confirm the object is created in your MinIO bucket.
+ - The avatar should render without errors.
+
+
+ If you see a 400 from /\_next/image with “url parameter is not allowed”:
+
+
+ -
+
NEXT_PUBLIC_STORAGE_DOMAIN must exactly match the S3 hostname
+ that serves images.
+
+ -
+
NEXT_PUBLIC_STORAGE_URL should use the same host (with
+ scheme/port).
+
+ - Ensure you’re using the latest Kan image.
+
+
+
+
+
+## Production setup
+
+
+
+
+
+ NEXT_PUBLIC_BASE_URL=http://kan.localtest.me:3000
+ S3_ENDPOINT=http://s3.localtest.me:9000
+ NEXT_PUBLIC_STORAGE_URL=http://s3.localtest.me:9000
+ NEXT_PUBLIC_STORAGE_DOMAIN=s3.localtest.me
+ - Keep
S3_FORCE_PATH_STYLE=true for MinIO.
+
+
+
+
+
+ NEXT_PUBLIC_BASE_URL=https://kan.example.com
+ S3_ENDPOINT=https://s3.example.com
+ NEXT_PUBLIC_STORAGE_URL=https://s3.example.com
+ NEXT_PUBLIC_STORAGE_DOMAIN=s3.example.com
+ - Keep
S3_FORCE_PATH_STYLE=true for MinIO.
+ - Put Kan and MinIO behind HTTPS with a reverse proxy (Traefik/Nginx) and valid TLS.
+
+
+
+
+## Troubleshooting
+
+
+
+
+ - If public: confirm GET is allowed on objects (bucket policy).
+ - If private: ensure presigned URLs are generated and valid.
+ - 403 AccessDenied indicates permissions, not CORS. CORS is not required for simple
<img> GETs.
+
+
+
+
+ Use your MinIO root credentials to allow anonymous reads:
+
+ ```bash
+ # Replace with your MINIO_ROOT_PASSWORD
+ MINIO_PASS=''
+
+ # Point mc at MinIO via the container network (no ports required)
+ docker run --rm --network container:kan-minio minio/mc \
+ mc alias set local http://127.0.0.1:9000 minio "$MINIO_PASS"
+
+ # Allow public downloads from the bucket
+ docker run --rm --network container:kan-minio minio/mc \
+ mc anonymous set download local/kan
+
+ # Optional: verify anonymous status
+ docker run --rm --network container:kan-minio minio/mc \
+ mc anonymous get local/kan
+ ```
+
+
+
+
+ If you prefer a bucket policy, apply a public-read policy for objects:
+
+ ```json policy.json
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "PublicReadGetObject",
+ "Effect": "Allow",
+ "Principal": "*",
+ "Action": ["s3:GetObject"],
+ "Resource": ["arn:aws:s3:::kan/*"]
+ }
+ ]
+ }
+ ```
+
+ ```bash
+ # Use your MinIO root credentials
+ MINIO_PASS=''
+
+ docker run --rm --network container:kan-minio \
+ -e AWS_ACCESS_KEY_ID=minio \
+ -e AWS_SECRET_ACCESS_KEY="$MINIO_PASS" \
+ -e AWS_DEFAULT_REGION=us-east-1 -e AWS_S3_FORCE_PATH_STYLE=true \
+ -v "$PWD:/work" amazon/aws-cli \
+ s3api put-bucket-policy --bucket kan --policy file:///work/policy.json \
+ --endpoint-url http://127.0.0.1:9000
+ ```
+
+
+
+
+
+ -
+ Exact match on
NEXT_PUBLIC_STORAGE_DOMAIN with your storage
+ host.
+
+ -
+ Same host in
NEXT_PUBLIC_STORAGE_URL.
+
+ - Update to the latest Kan image.
+
+
+
+
+
+ - This means Next.js accepted the URL, but the upstream returned a non-image (e.g., 403 HTML/XML).
+ - Fix: make the bucket/object publicly readable (see above), or use presigned URLs.
+ - Sanity test from the web network (replace with your image URL):
+
+
+ ```bash
+ IMG_URL="https://s3.example.com/kan/path/to/avatar.jpg"
+
+ # Headers/content-type as seen from the app network
+ docker run --rm --network container:kan-web curlimages/curl:8.9.1 \
+ -I -L --max-redirs 5 "$IMG_URL"
+
+ # Quick status + content-type summary
+ docker run --rm --network container:kan-web curlimages/curl:8.9.1 \
+ -s -o /dev/null -w "HTTP:%{http_code} CT:%{content_type} URL:%{url_effective}\n" -L "$IMG_URL"
+ ```
+
+
+
+
+
+ - The Kan container must reach
S3_ENDPOINT.
+ - Verify DNS/ports inside the container (e.g.,
docker exec -it <kan-container> sh).
+
+
+
+
+## References
+
+- [Kan README](https://github.com/kanbn/kan/blob/main/README.md)
+- [Cloud compose reference](https://github.com/kanbn/kan/blob/main/cloud/docker-compose.yml)
+- [Kan #109 Issue](https://github.com/kanbn/kan/issues/109)
diff --git a/apps/docs/mint.json b/apps/docs/mint.json
index 078057bd..0040c99a 100644
--- a/apps/docs/mint.json
+++ b/apps/docs/mint.json
@@ -44,6 +44,18 @@
"group": "Get Started",
"pages": ["introduction"]
},
+ {
+ "group": "Guides",
+ "pages": [
+ {
+ "group": "Self-Hosting",
+ "pages": [
+ "guides/self-hosting/introduction",
+ "guides/self-hosting/s3"
+ ]
+ }
+ ]
+ },
{
"group": "Import",
"pages": ["imports/trello"]