From 1c75a948f985c3f200d0cba73bb9bce9a74d2366 Mon Sep 17 00:00:00 2001 From: Henry <30578846+hjball@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:52:27 +0100 Subject: [PATCH] feat: setup postgres with docker compose (#85) * feat: add postgres to docker compose * feat: run migrations via entrypoint * chore: tidy .env.example * fix: allow empty strings for optional envs * fix: remove external from kan-network * chore: update readme --- .env.example | 33 ++++++++++++------- README.md | 71 ++++++++++++++++++++++++++++----------- apps/web/Dockerfile | 3 +- apps/web/entrypoint.sh | 16 +++++++++ apps/web/next.config.js | 1 + apps/web/src/env.ts | 17 +++++++--- docker-compose.yml | 73 +++++++++++++++++++++++++++++++---------- 7 files changed, 160 insertions(+), 54 deletions(-) create mode 100644 apps/web/entrypoint.sh diff --git a/.env.example b/.env.example index 48222264..e09caf70 100644 --- a/.env.example +++ b/.env.example @@ -1,28 +1,39 @@ -POSTGRES_URL= +# https://github.com/kanbn/kan?tab=readme-ov-file#environment-variables-) -EMAIL_FROM= +# Required environment variables +NEXT_PUBLIC_BASE_URL= # e.g. https://kan.bn +BETTER_AUTH_SECRET= # Random 32+ char string (can gen with: openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32) +POSTGRES_URL= # e.g. postgresql://kan:your_password@your_host:5432/kan + +# Only required if deploying from compose +POSTGRES_PASSWORD= + +# SMTP (optional) SMTP_HOST= -SMTP_PORT= +SMTP_PORT=465 # Port 587 will not work SMTP_USER= SMTP_PASSWORD= +EMAIL_FROM= # e.g. "Kan " -NEXT_PUBLIC_BASE_URL= -NEXT_PUBLIC_STORAGE_URL= -NEXT_PUBLIC_AVATAR_BUCKET_NAME= -NEXT_PUBLIC_ALLOW_CREDENTIALS= -NEXT_PUBLIC_DISABLE_SIGN_UP= - +# S3 storage (optional) S3_REGION= S3_ENDPOINT= S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= +NEXT_PUBLIC_STORAGE_URL= +NEXT_PUBLIC_AVATAR_BUCKET_NAME= +NEXT_PUBLIC_STORAGE_DOMAIN= -BETTER_AUTH_SECRET= -BETTER_AUTH_TRUSTED_ORIGINS= +# Auth config (optional) +NEXT_PUBLIC_ALLOW_CREDENTIALS= +NEXT_PUBLIC_DISABLE_SIGN_UP= +# Integration providers (optional) TRELLO_APP_API_KEY= TRELLO_APP_SECRET= +# OAuth providers (optional) +BETTER_AUTH_TRUSTED_ORIGINS= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= DISCORD_CLIENT_ID= diff --git a/README.md b/README.md index 35e7f197..a1a7ae37 100644 --- a/README.md +++ b/README.md @@ -47,34 +47,67 @@ See our [roadmap](https://kan.bn/kan/roadmap) for upcoming features. ## Self Hosting 🐳 -### PostgreSQL Database Setup +The easiest way to self-host Kan is with Docker Compose. This will set up everything for you including your postgres database. -Kan requires a PostgreSQL database. You can run one using the official PostgreSQL Docker image: +1. Create a new file called `docker-compose.yml` and paste the following configuration: -```bash -# Run PostgreSQL in a container using the official postgres image -docker run -d \ - --name kan-db \ - -e POSTGRES_DB=kan \ - -e POSTGRES_USER=kan_user \ - -e POSTGRES_PASSWORD=your_secure_password \ - -p 5432:5432 \ - -v kan_postgres_data:/var/lib/postgresql/data \ - postgres:15 +```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 -# Your POSTGRES_URL should be: -# postgres://kan_user:your_secure_password@your_host:5432/kan + 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: + postgres_data: ``` -### Kan Application Deployment - -Deploy Kan with Docker using our pre-built image: +2. Start the containers in detached mode: ```bash -docker pull ghcr.io/kanbn/kan:latest && docker run -it -p 3000:3000 --env-file .env ghcr.io/kanbn/kan:latest +docker compose up -d ``` -Make sure to create a `.env` file with the required environment variables (see the Environment Variables section below). +3. Access Kan at http://localhost:3000 + +The application will be running in the background. You can manage the containers using these commands: + +- To stop the containers: `docker compose down` +- To view logs: `docker compose logs -f` +- To restart the containers: `docker compose restart` + +For the complete Docker Compose configuration, see [docker-compose.yml](./docker-compose.yml) in the repository. + +> **Note**: The Docker Compose configuration shown above is a minimal example. For a complete setup with all features (email, OAuth, file uploads, etc.), you'll need to create a `.env` file with the required environment variables. See the Environment Variables section below for the full list of available options. ## Local Development 🧑‍💻 diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index ecdfe2c7..d898e44b 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -75,6 +75,5 @@ ARG PORT=3000 ENV PORT=${PORT} EXPOSE ${PORT} -# Run migration on start for now (until we have a better way to handle this) -CMD ["sh", "-c", "cd /app && pnpm db:migrate && cd /app/apps/web && pnpm start"] +CMD ["./entrypoint.sh"] diff --git a/apps/web/entrypoint.sh b/apps/web/entrypoint.sh new file mode 100644 index 00000000..85557067 --- /dev/null +++ b/apps/web/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# Run migrations from the Turborepo root context +echo "Running database migrations..." +# Navigate to the Turborepo root to run the db:migrate command +pnpm --prefix /app db:migrate + +# Check if migration was successful +if [ $? -ne 0 ]; then + echo "\nDatabase migration failed! Exiting." + exit 1 +fi + +echo "\nStarting Next.js application..." +# Start the Next.js application from the web app's directory +exec node .next/standalone/apps/web/server.js \ No newline at end of file diff --git a/apps/web/next.config.js b/apps/web/next.config.js index e909fa3c..08f0f5d2 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -10,6 +10,7 @@ configureRuntimeEnv(); /** @type {import("next").NextConfig} */ const config = { + output: "standalone", reactStrictMode: true, /** Enables hot reloading for local packages without a build step */ diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index ad42bbf0..dee7f7ca 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -17,8 +17,11 @@ export const env = createEnv({ BETTER_AUTH_SECRET: z.string(), BETTER_AUTH_TRUSTED_ORIGINS: z .string() - .refine((s) => - s.split(",").every((l) => z.string().url().safeParse(l).success), + .transform((s) => (s === "" ? undefined : s)) + .refine( + (s) => + !s || + s.split(",").every((l) => z.string().url().safeParse(l).success), ) .optional(), POSTGRES_URL: z.string().url(), @@ -82,11 +85,17 @@ export const env = createEnv({ NEXT_PUBLIC_STORAGE_DOMAIN: z.string().optional(), NEXT_PUBLIC_ALLOW_CREDENTIALS: z .string() - .refine((s) => s.toLowerCase() === "true" || s.toLowerCase() === "false") + .transform((s) => (s === "" ? undefined : s)) + .refine( + (s) => !s || s.toLowerCase() === "true" || s.toLowerCase() === "false", + ) .optional(), NEXT_PUBLIC_DISABLE_SIGN_UP: z .string() - .refine((s) => s.toLowerCase() === "true" || s.toLowerCase() === "false") + .transform((s) => (s === "" ? undefined : s)) + .refine( + (s) => !s || s.toLowerCase() === "true" || s.toLowerCase() === "false", + ) .optional(), }, /** diff --git a/docker-compose.yml b/docker-compose.yml index 7c1c8405..76dd7233 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,49 @@ services: web: image: ghcr.io/kanbn/kan:latest + container_name: kan-web ports: - - "${WEB_PORT:-3001}:3000" + - "${WEB_PORT:-3000}:3000" networks: + - kan-network - dokploy-network build: context: . dockerfile: ./apps/web/Dockerfile env_file: - .env - command: ["pnpm", "start"] environment: - - EMAIL_FROM=${EMAIL_FROM} + # Required environment variables + - NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - POSTGRES_URL=${POSTGRES_URL} + + # SMTP (optional) - SMTP_HOST=${SMTP_HOST} - SMTP_PORT=${SMTP_PORT} - SMTP_USER=${SMTP_USER} - SMTP_PASSWORD=${SMTP_PASSWORD} - - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - - BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} - - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} - - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - - POSTGRES_URL=${POSTGRES_URL} + - EMAIL_FROM=${EMAIL_FROM} + + # S3 storage (optional) + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + - S3_REGION=${S3_REGION} + - S3_ENDPOINT=${S3_ENDPOINT} + - NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL} + - NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME} + - NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN} + + # Auth config (optional) + - NEXT_PUBLIC_ALLOW_CREDENTIALS=${NEXT_PUBLIC_ALLOW_CREDENTIALS} + - NEXT_PUBLIC_DISABLE_SIGN_UP=${NEXT_PUBLIC_DISABLE_SIGN_UP} + + # Integration providers (optional) - TRELLO_APP_API_KEY=${TRELLO_APP_API_KEY} - TRELLO_APP_SECRET=${TRELLO_APP_SECRET} + + # OAuth providers (optional) + - BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} - DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID} @@ -61,18 +81,35 @@ services: - APPLE_CLIENT_ID=${APPLE_CLIENT_ID} - APPLE_CLIENT_SECRET=${APPLE_CLIENT_SECRET} - APPLE_APP_BUNDLE_IDENTIFIER=${APPLE_APP_BUNDLE_IDENTIFIER} - - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - - S3_REGION=${S3_REGION} - - S3_ENDPOINT=${S3_ENDPOINT} + + # Cloud (not required) - NEXT_PUBLIC_KAN_ENV=${NEXT_PUBLIC_KAN_ENV} - - NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL} - - NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL} - - NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME} - - NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN} - NEXT_PUBLIC_UMAMI_ID=${NEXT_PUBLIC_UMAMI_ID} - - NEXT_PUBLIC_ALLOW_CREDENTIALS=${NEXT_PUBLIC_ALLOW_CREDENTIALS} - - NEXT_PUBLIC_DISABLE_SIGN_UP=${NEXT_PUBLIC_DISABLE_SIGN_UP} + - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} + depends_on: + - postgres + restart: unless-stopped + + postgres: + image: postgres:15 + container_name: kan-db + environment: + - POSTGRES_DB=kan_db + - POSTGRES_USER=kan + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + ports: + - 5432:5432 + volumes: + - kan_postgres_data:/var/lib/postgresql/data + restart: unless-stopped + networks: + - kan-network + networks: + kan-network: dokploy-network: external: true + +volumes: + kan_postgres_data: