* feat(docs): self-hosting s3 minio guide * feat(docs): self-hosting introduction guide * fix: build error * refactor: use kan over kan.bn --------- Co-authored-by: Henry <henry_ball@hotmail.co.uk>
398 lines
12 KiB
Plaintext
398 lines
12 KiB
Plaintext
---
|
||
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
|
||
|
||
<CardGroup cols={2}>
|
||
<Card title="Kan" icon="globe" color="#0284c7" horizontal>
|
||
Kan web app (Next.js), on port 3000.
|
||
</Card>
|
||
<Card title="PostgreSQL 15" icon="database" color="#65a30d" horizontal>
|
||
PostgreSQL database for Kan.
|
||
</Card>
|
||
</CardGroup>
|
||
<Card title="MinIO (S3-compatible)" icon="cloud" color="#ca8a04" horizontal>
|
||
MinIO object storage (console port 9001, S3 API port 9000).
|
||
</Card>
|
||
|
||
## 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:
|
||
|
||
- <code>NEXT_PUBLIC_BASE_URL</code> → the Kan site
|
||
- <code>NEXT_PUBLIC_STORAGE_URL</code> → the public S3 base URL
|
||
- <code>NEXT_PUBLIC_STORAGE_DOMAIN</code> → 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 <code>BETTER_AUTH_SECRET</code> (32+ chars)
|
||
|
||
<Note type="warning">
|
||
For production you’ll want a reverse proxy (Traefik/Nginx/Caddy), valid TLS
|
||
certificates, and DNS for your domains (e.g., <code>kan.example.com</code>,{" "}
|
||
<code>s3.example.com</code>).
|
||
</Note>
|
||
|
||
## Quick start
|
||
|
||
<Tip type="info">
|
||
Why <code>localtest.me</code>? It resolves to <code>127.0.0.1</code>{" "}
|
||
automatically, so you can test domain-based configs locally without editing
|
||
hosts.
|
||
</Tip>
|
||
|
||
<Steps>
|
||
<Step title="Set environment variables">
|
||
Provide the minimum required configuration (local example):
|
||
|
||
```bash
|
||
NEXT_PUBLIC_BASE_URL=http://kan.localtest.me:3000
|
||
BETTER_AUTH_SECRET=<long random string>
|
||
POSTGRES_URL=postgresql://kan:<password>@postgres:5432/kan_db
|
||
|
||
# MinIO/S3
|
||
S3_ENDPOINT=http://s3.localtest.me:9000
|
||
S3_ACCESS_KEY_ID=<minio-access-key>
|
||
S3_SECRET_ACCESS_KEY=<minio-secret-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
|
||
```
|
||
|
||
<Note type="info">
|
||
Issue #109 fix: make sure <code>NEXT_PUBLIC_STORAGE_DOMAIN</code> exactly
|
||
equals the hostname that serves your images (no scheme, no port).
|
||
</Note>
|
||
|
||
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.
|
||
|
||
</Step>
|
||
|
||
<Step title="Create or review Docker Compose files">
|
||
You can start from the minimal compose at the repository root (<code>docker-compose.yml</code>) and review the production-oriented settings in <code>cloud/docker-compose.yml</code>.
|
||
|
||
Start with the minimal setup (web + postgres + minio) and ensure environment variables are passed to the web service.
|
||
|
||
<Accordion title="Docker Compose example">
|
||
|
||
```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:
|
||
```
|
||
|
||
<Note type="info">
|
||
Ensure your <code>.env</code> contains values that match this compose file.
|
||
For example:
|
||
<ul>
|
||
<li>
|
||
<code>POSTGRES_URL=postgresql://kan:changeme@postgres:5432/kan_db</code>
|
||
</li>
|
||
<li>
|
||
<code>S3_ENDPOINT=http://s3.localtest.me:9000</code>
|
||
</li>
|
||
<li>
|
||
<code>S3_ACCESS_KEY_ID=minio</code> and{" "}
|
||
<code>S3_SECRET_ACCESS_KEY=minio123456789</code>
|
||
</li>
|
||
<li>
|
||
<code>S3_FORCE_PATH_STYLE=true</code>
|
||
</li>
|
||
<li>
|
||
<code>NEXT_PUBLIC_STORAGE_URL=http://s3.localtest.me:9000</code>
|
||
</li>
|
||
<li>
|
||
<code>NEXT_PUBLIC_STORAGE_DOMAIN=s3.localtest.me</code>
|
||
</li>
|
||
<li>
|
||
<code>NEXT_PUBLIC_AVATAR_BUCKET_NAME=kan</code>
|
||
</li>
|
||
</ul>
|
||
</Note>
|
||
|
||
</Accordion>
|
||
|
||
</Step>
|
||
|
||
<Step title="Start services">
|
||
|
||
```bash
|
||
docker compose up -d
|
||
```
|
||
|
||
Then open:
|
||
|
||
<ul>
|
||
<li>
|
||
Kan: <a href="http://kan.localtest.me:3000">http://kan.localtest.me:3000</a>
|
||
</li>
|
||
<li>
|
||
MinIO Console:{" "}
|
||
<a href="http://minio.localtest.me:9001">http://minio.localtest.me:9001</a>
|
||
</li>
|
||
<li>
|
||
MinIO S3 API:{" "}
|
||
<a href="http://s3.localtest.me:9000">http://s3.localtest.me:9000</a>
|
||
</li>
|
||
</ul>
|
||
|
||
</Step>
|
||
|
||
<Step title="Initialize MinIO">
|
||
1) Log into the MinIO Console (http://minio.localtest.me:9001).
|
||
|
||
2. Create a bucket (e.g., <code>kan</code>).
|
||
|
||
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/*"]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
<Note type="warning">
|
||
Alternatively, keep the bucket private and use presigned URLs. In that case,
|
||
ensure your server and browser access paths are correctly configured.
|
||
</Note>
|
||
|
||
</Step>
|
||
|
||
<Step title="Verify the setup">
|
||
<ul>
|
||
<li>Sign in to Kan and upload an avatar (Settings).</li>
|
||
<li>Confirm the object is created in your MinIO bucket.</li>
|
||
<li>The avatar should render without errors.</li>
|
||
</ul>
|
||
|
||
If you see a 400 from <code>/\_next/image</code> with “url parameter is not allowed”:
|
||
|
||
<ul>
|
||
<li>
|
||
<code>NEXT_PUBLIC_STORAGE_DOMAIN</code> must exactly match the S3 hostname
|
||
that serves images.
|
||
</li>
|
||
<li>
|
||
<code>NEXT_PUBLIC_STORAGE_URL</code> should use the same host (with
|
||
scheme/port).
|
||
</li>
|
||
<li>Ensure you’re using the latest Kan image.</li>
|
||
</ul>
|
||
|
||
</Step>
|
||
</Steps>
|
||
|
||
## Production setup
|
||
|
||
<Tabs>
|
||
<Tab title="Local">
|
||
|
||
<ul>
|
||
<li><code>NEXT_PUBLIC_BASE_URL=http://kan.localtest.me:3000</code></li>
|
||
<li><code>S3_ENDPOINT=http://s3.localtest.me:9000</code></li>
|
||
<li><code>NEXT_PUBLIC_STORAGE_URL=http://s3.localtest.me:9000</code></li>
|
||
<li><code>NEXT_PUBLIC_STORAGE_DOMAIN=s3.localtest.me</code></li>
|
||
<li>Keep <code>S3_FORCE_PATH_STYLE=true</code> for MinIO.</li>
|
||
</ul>
|
||
</Tab>
|
||
<Tab title="Production">
|
||
|
||
<ul>
|
||
<li><code>NEXT_PUBLIC_BASE_URL=https://kan.example.com</code></li>
|
||
<li><code>S3_ENDPOINT=https://s3.example.com</code></li>
|
||
<li><code>NEXT_PUBLIC_STORAGE_URL=https://s3.example.com</code></li>
|
||
<li><code>NEXT_PUBLIC_STORAGE_DOMAIN=s3.example.com</code></li>
|
||
<li>Keep <code>S3_FORCE_PATH_STYLE=true</code> for MinIO.</li>
|
||
<li>Put Kan and MinIO behind HTTPS with a reverse proxy (Traefik/Nginx) and valid TLS.</li>
|
||
</ul>
|
||
</Tab>
|
||
</Tabs>
|
||
|
||
## Troubleshooting
|
||
|
||
<AccordionGroup>
|
||
<Accordion title="Files upload but don’t display">
|
||
<ul>
|
||
<li>If public: confirm GET is allowed on objects (bucket policy).</li>
|
||
<li>If private: ensure presigned URLs are generated and valid.</li>
|
||
<li>403 AccessDenied indicates permissions, not CORS. CORS is not required for simple <code><img></code> GETs.</li>
|
||
</ul>
|
||
</Accordion>
|
||
|
||
<Accordion title="Make the bucket public (read-only) with mc" defaultOpen>
|
||
Use your MinIO root credentials to allow anonymous reads:
|
||
|
||
```bash
|
||
# Replace with your MINIO_ROOT_PASSWORD
|
||
MINIO_PASS='<your-minio-password>'
|
||
|
||
# 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
|
||
```
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Alternative: S3 bucket policy (AWS CLI)">
|
||
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='<your-minio-password>'
|
||
|
||
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
|
||
```
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Next.js optimizer 400 — url parameter is not allowed">
|
||
<ul>
|
||
<li>
|
||
Exact match on <code>NEXT_PUBLIC_STORAGE_DOMAIN</code> with your storage
|
||
host.
|
||
</li>
|
||
<li>
|
||
Same host in <code>NEXT_PUBLIC_STORAGE_URL</code>.
|
||
</li>
|
||
<li>Update to the latest Kan image.</li>
|
||
</ul>
|
||
</Accordion>
|
||
|
||
<Accordion title="Next/Image: “url parameter is valid but upstream response is invalid”">
|
||
<ul>
|
||
<li>This means Next.js accepted the URL, but the upstream returned a non-image (e.g., 403 HTML/XML).</li>
|
||
<li>Fix: make the bucket/object publicly readable (see above), or use presigned URLs.</li>
|
||
<li>Sanity test from the web network (replace with your image URL):</li>
|
||
</ul>
|
||
|
||
```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"
|
||
```
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Connectivity checks">
|
||
<ul>
|
||
<li>The Kan container must reach <code>S3_ENDPOINT</code>.</li>
|
||
<li>Verify DNS/ports inside the container (e.g., <code>docker exec -it <kan-container> sh</code>).</li>
|
||
</ul>
|
||
</Accordion>
|
||
</AccordionGroup>
|
||
|
||
## 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)
|