Compare commits

..

1 Commits

Author SHA1 Message Date
Henry
dda760f8f3 feat: reduce docker image size by using next standalone output 2025-06-18 22:13:57 +01:00
10 changed files with 145 additions and 80 deletions

View File

@@ -10,11 +10,10 @@ POSTGRES_PASSWORD=
# SMTP (optional) # SMTP (optional)
SMTP_HOST= SMTP_HOST=
SMTP_PORT=465 SMTP_PORT=465 # Port 587 will not work
SMTP_USER= SMTP_USER=
SMTP_PASSWORD= SMTP_PASSWORD=
EMAIL_FROM= # e.g. "Kan <hello@mail.kan.bn>" EMAIL_FROM= # e.g. "Kan <hello@mail.kan.bn>"
SMTP_SECURE= # set to "false" to use port 587
# S3 storage (optional) # S3 storage (optional)
S3_REGION= S3_REGION=

View File

@@ -138,35 +138,34 @@ pnpm dev
## Environment Variables 🔐 ## Environment Variables 🔐
| Variable | Description | Required | Example | | Variable | Description | Required | Example |
| -------------------------------- | -------------------------------------------------------- | ------------------ | --------------------------------------------- | | -------------------------------- | ----------------------------- | ------------------ | --------------------------------------------- |
| `POSTGRES_URL` | PostgreSQL connection URL | Yes | `postgres://user:pass@localhost:5432/db` | | `POSTGRES_URL` | PostgreSQL connection URL | Yes | `postgres://user:pass@localhost:5432/db` |
| `EMAIL_FROM` | Sender email address | For Email | `"Kan <hello@mail.kan.bn>"` | | `EMAIL_FROM` | Sender email address | For Email | `"Kan <hello@mail.kan.bn>"` |
| `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` | | `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` |
| `SMTP_PORT` | SMTP server port | For Email | `465` | | `SMTP_PORT` | SMTP server port | For Email | `465` |
| `SMTP_USER` | SMTP username/email | No | `resend` | | `SMTP_USER` | SMTP username/email | For Email | `resend` |
| `SMTP_PASSWORD` | SMTP password/token | No | `re_xxxx` | | `SMTP_PASSWORD` | SMTP password/token | For Email | `re_xxxx` |
| `SMTP_SECURE` | Use secure SMTP connection (defaults to true if not set) | For Email | `true` | | `NEXT_PUBLIC_BASE_URL` | Base URL of your installation | Yes | `http://localhost:3000` |
| `NEXT_PUBLIC_BASE_URL` | Base URL of your installation | Yes | `http://localhost:3000` | | `BETTER_AUTH_SECRET` | Auth encryption secret | Yes | Random 32+ char string |
| `BETTER_AUTH_SECRET` | Auth encryption secret | Yes | Random 32+ char string | | `BETTER_AUTH_TRUSTED_ORIGINS` | Allowed callback origins | No | `http://localhost:3000,http://localhost:3001` |
| `BETTER_AUTH_TRUSTED_ORIGINS` | Allowed callback origins | No | `http://localhost:3000,http://localhost:3001` | | `GOOGLE_CLIENT_ID` | Google OAuth client ID | For Google login | `xxx.apps.googleusercontent.com` |
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | For Google login | `xxx.apps.googleusercontent.com` | | `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | For Google login | `xxx` |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | For Google login | `xxx` | | `DISCORD_CLIENT_ID` | Discord OAuth client ID | For Discord login | `xxx` |
| `DISCORD_CLIENT_ID` | Discord OAuth client ID | For Discord login | `xxx` | | `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` |
| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` | | `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` |
| `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` | | `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` | | `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` |
| `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` | | `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `xxx` |
| `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `xxx` | | `S3_REGION` | S3 storage region | For file uploads | `WEUR` |
| `S3_REGION` | S3 storage region | For file uploads | `WEUR` | | `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` |
| `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` | | `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` |
| `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` | | `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads | `xxx` |
| `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads | `xxx` | | `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` |
| `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` | | `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` |
| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` | | `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` |
| `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` | | `NEXT_PUBLIC_ALLOW_CREDENTIALS` | Allow email & password login | For authentication | `true` |
| `NEXT_PUBLIC_ALLOW_CREDENTIALS` | Allow email & password login | For authentication | `true` | | `NEXT_PUBLIC_DISABLE_SIGN_UP` | Disable sign up | For authentication | `false` |
| `NEXT_PUBLIC_DISABLE_SIGN_UP` | Disable sign up | For authentication | `false` |
See `.env.example` for a complete list of supported environment variables. See `.env.example` for a complete list of supported environment variables.

84
apps/web/Dockerfile.new Normal file
View File

@@ -0,0 +1,84 @@
ARG NODE_VERSION=20
ARG APP_DIRNAME=web
ARG PROJECT=@kan/web
# 1. Alpine image
FROM node:${NODE_VERSION}-alpine AS alpine
RUN apk update
RUN apk add --no-cache libc6-compat python3 make g++
# Setup pnpm and turbo on the alpine base
FROM alpine AS base
RUN corepack enable
# Replace <your-major-version> with the major version installed in your repository. For example:
# RUN npm install turbo@2.1.3 --global
RUN npm install turbo@2.3.1 --global
RUN pnpm config set store-dir ~/.pnpm-store
# 2. Prune projects
FROM base AS pruner
# https://stackoverflow.com/questions/49681984/how-to-get-version-value-of-package-json-inside-of-dockerfile
# RUN export VERSION=$(npm run version)
ARG PROJECT
# Set working directory
WORKDIR /app
# It might be the path to <ROOT> turborepo
COPY . .
# Generate a partial monorepo with a pruned lockfile for a target workspace.
# Assuming "@acme/nextjs" is the name entered in the project's package.json: { name: "@acme/nextjs" }
RUN turbo prune --scope=${PROJECT} --scope=@kan/db --docker
# 3. Build the project
FROM base AS builder
ARG PROJECT
# Environment to skip .env validation on build
ENV CI=true
WORKDIR /app
# Copy lockfile and package.json's of isolated subworkspace
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=pruner /app/out/json/ .
# First install the dependencies (as they change less often)
RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile
# Copy source code of isolated subworkspace
COPY --from=pruner /app/out/full/ .
RUN pnpm build --filter=${PROJECT}
# Copy static files to standalone directory
RUN mkdir -p apps/web/.next/standalone/.next && \
mv apps/web/public apps/web/.next/standalone/ && \
mv apps/web/.next/static apps/web/.next/standalone/.next/
# 4. Final image - runner stage to run the application
FROM base AS runner
ARG APP_DIRNAME
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
WORKDIR /app
ENV NODE_ENV=production
COPY --chown=nextjs:nodejs --from=builder /app/ ./
WORKDIR /app/apps/${APP_DIRNAME}
ARG PORT=3000
ENV PORT=${PORT}
EXPOSE ${PORT}
CMD ["/bin/sh", "./entrypoint.sh"]

View File

@@ -12,12 +12,5 @@ if [ $? -ne 0 ]; then
fi fi
echo "\nStarting Next.js application..." echo "\nStarting Next.js application..."
# Start the Next.js application from the web app's directory
# Check if we should use standalone mode exec node .next/standalone/apps/web/server.js
if [ "$NEXT_PUBLIC_USE_STANDALONE_OUTPUT" = "true" ]; then
echo "Starting in standalone mode..."
exec node .next/standalone/apps/web/server.js
else
echo "Starting in standard mode..."
exec pnpm start
fi

View File

@@ -13,7 +13,7 @@ const config = {
output: output:
env("NEXT_PUBLIC_USE_STANDALONE_OUTPUT") === "true" env("NEXT_PUBLIC_USE_STANDALONE_OUTPUT") === "true"
? "standalone" ? "standalone"
: undefined, : "export",
reactStrictMode: true, reactStrictMode: true,
/** Enables hot reloading for local packages without a build step */ /** Enables hot reloading for local packages without a build step */

View File

@@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "pnpm with-env next build", "build": "pnpm next build",
"clean": "git clean -xdf .cache .next .turbo node_modules", "clean": "git clean -xdf .cache .next .turbo node_modules",
"dev": "pnpm with-env next dev | pino-pretty", "dev": "pnpm with-env next dev | pino-pretty",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",

View File

@@ -7,11 +7,10 @@ services:
networks: networks:
- dokploy-network - dokploy-network
build: build:
context: .. context: .
dockerfile: apps/web/Dockerfile dockerfile: ./apps/web/Dockerfile
env_file: env_file:
- .env - .env
restart: unless-stopped
environment: environment:
- NEXT_PUBLIC_KAN_ENV=${NEXT_PUBLIC_KAN_ENV} - NEXT_PUBLIC_KAN_ENV=${NEXT_PUBLIC_KAN_ENV}
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL} - NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL}
@@ -28,7 +27,6 @@ services:
- SMTP_PORT=${SMTP_PORT} - SMTP_PORT=${SMTP_PORT}
- SMTP_USER=${SMTP_USER} - SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD} - SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_SECURE=${SMTP_SECURE}
- EMAIL_FROM=${EMAIL_FROM} - EMAIL_FROM=${EMAIL_FROM}
# S3 storage # S3 storage

View File

@@ -81,6 +81,14 @@ services:
- APPLE_CLIENT_ID=${APPLE_CLIENT_ID} - APPLE_CLIENT_ID=${APPLE_CLIENT_ID}
- APPLE_CLIENT_SECRET=${APPLE_CLIENT_SECRET} - APPLE_CLIENT_SECRET=${APPLE_CLIENT_SECRET}
- APPLE_APP_BUNDLE_IDENTIFIER=${APPLE_APP_BUNDLE_IDENTIFIER} - APPLE_APP_BUNDLE_IDENTIFIER=${APPLE_APP_BUNDLE_IDENTIFIER}
# Cloud (not required)
- NEXT_PUBLIC_KAN_ENV=${NEXT_PUBLIC_KAN_ENV}
- NEXT_PUBLIC_UMAMI_ID=${NEXT_PUBLIC_UMAMI_ID}
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
- NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST}
depends_on: depends_on:
- postgres - postgres
restart: unless-stopped restart: unless-stopped

View File

@@ -16,14 +16,11 @@ const emailTemplates: Record<Templates, React.FC> = {
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT), port: Number(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE !== "false", secure: true,
...(process.env.SMTP_USER && auth: {
process.env.SMTP_PASSWORD && { user: process.env.SMTP_USER,
auth: { pass: process.env.SMTP_PASSWORD,
user: process.env.SMTP_USER, },
pass: process.env.SMTP_PASSWORD,
},
}),
}); });
export const sendEmail = async ( export const sendEmail = async (
@@ -32,34 +29,22 @@ export const sendEmail = async (
template: Templates, template: Templates,
data: Record<string, string>, data: Record<string, string>,
) => { ) => {
try { const EmailTemplate = emailTemplates[template];
const EmailTemplate = emailTemplates[template];
const html = await render(<EmailTemplate {...data} />, { pretty: true }); const html = await render(<EmailTemplate {...data} />, { pretty: true });
const options = { const options = {
from: process.env.EMAIL_FROM, from: process.env.EMAIL_FROM,
to, to,
subject, subject,
html, html,
}; };
const response = await transporter.sendMail(options); const response = await transporter.sendMail(options);
if (!response.accepted.length) { if (!response.accepted.length) {
throw new Error(`Failed to send email: ${response.response}`); throw new Error(`Failed to send email: ${response.response}`);
}
return response;
} catch (error) {
console.error("Email sending failed:", {
to,
from: process.env.EMAIL_FROM,
subject,
template,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
} }
return response;
}; };

View File

@@ -91,7 +91,6 @@
"SMTP_PORT", "SMTP_PORT",
"SMTP_USER", "SMTP_USER",
"SMTP_PASSWORD", "SMTP_PASSWORD",
"SMTP_SECURE",
"NEXT_PUBLIC_KAN_ENV", "NEXT_PUBLIC_KAN_ENV",
"STRIPE_SECRET_KEY", "STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET", "STRIPE_WEBHOOK_SECRET",