Compare commits

...

7 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
khalt00
02d3d300fb change volume name (#88)
Co-authored-by: Le Trong Kha <kha.le@ao360.com.vn>
2025-06-17 23:20:15 +01:00
Henry
0bd28f9f95 fix: set container name env var 2025-06-17 23:02:24 +01:00
Henry
d0c2314ebf feat: add posthog analytics 2025-06-17 22:45:06 +01:00
Henry
268871c348 fix: bump compose 2025-06-17 11:00:28 +01:00
Henry
2e11199764 fix: add old docker compose and revert entrypoint 2025-06-17 10:57:54 +01:00
Henry
fc6bb22400 fix: copy static files and use explicit shell in Docker (#86) (#87)
* fix: copy static files to standalone dir

* fix: update Dockerfile to use /bin/sh for entrypoint execution

* fix: update Dockerfile to copy static files from apps/web directory

* fix: copy from root

* fix: set workdir

* fix: copy files from inside the container

* fix: add logs to debug

* fix: remove workdir

* fix: log contents of build

* fix: copy static files

* fix: remove log

* chore: remove spacing
2025-06-16 22:50:21 +01:00
11 changed files with 239 additions and 7 deletions

View File

@@ -88,7 +88,7 @@ networks:
kan-network:
volumes:
postgres_data:
kan_postgres_data:
```
2. Start the containers in detached mode:

View File

@@ -53,8 +53,14 @@ RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfi
# 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
@@ -75,5 +81,5 @@ ARG PORT=3000
ENV PORT=${PORT}
EXPOSE ${PORT}
CMD ["./entrypoint.sh"]
CMD ["sh", "-c", "cd /app && pnpm db:migrate && cd /app/apps/web && pnpm start"]

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

@@ -10,7 +10,10 @@ configureRuntimeEnv();
/** @type {import("next").NextConfig} */
const config = {
output: "standalone",
output:
env("NEXT_PUBLIC_USE_STANDALONE_OUTPUT") === "true"
? "standalone"
: "export",
reactStrictMode: true,
/** Enables hot reloading for local packages without a build step */

View File

@@ -42,6 +42,7 @@
"next-runtime-env": "^1.7.2",
"nextjs-cors": "^2.2.0",
"pino": "^9.6.0",
"posthog-js": "^1.254.0",
"react": "catalog:react18",
"react-beautiful-dnd": "^13.1.1",
"react-contenteditable": "^3.3.7",

View File

@@ -79,6 +79,9 @@ export const env = createEnv({
client: {
NEXT_PUBLIC_KAN_ENV: z.string().optional(),
NEXT_PUBLIC_UMAMI_ID: z.string().optional(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
NEXT_PUBLIC_USE_STANDALONE_OUTPUT: z.string().optional(),
NEXT_PUBLIC_BASE_URL: z.string().url(),
NEXT_PUBLIC_STORAGE_URL: z.string().url().optional(),
NEXT_PUBLIC_AVATAR_BUCKET_NAME: z.string().optional(),
@@ -105,12 +108,16 @@ export const env = createEnv({
NEXT_PUBLIC_KAN_ENV: process.env.NEXT_PUBLIC_KAN_ENV,
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_UMAMI_ID: process.env.NEXT_PUBLIC_UMAMI_ID,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_STORAGE_URL: process.env.NEXT_PUBLIC_STORAGE_URL,
NEXT_PUBLIC_AVATAR_BUCKET_NAME: process.env.NEXT_PUBLIC_AVATAR_BUCKET_NAME,
NEXT_PUBLIC_STORAGE_DOMAIN: process.env.NEXT_PUBLIC_STORAGE_DOMAIN,
NEXT_PUBLIC_ALLOW_CREDENTIALS: process.env.NEXT_PUBLIC_ALLOW_CREDENTIALS,
NEXT_PUBLIC_DISABLE_SIGN_UP: process.env.NEXT_PUBLIC_DISABLE_SIGN_UP,
NEXT_PUBLIC_USE_STANDALONE_OUTPUT:
process.env.NEXT_PUBLIC_USE_STANDALONE_OUTPUT,
},
skipValidation:
!!process.env.CI || process.env.npm_lifecycle_event === "lint",

View File

@@ -2,7 +2,11 @@ import "~/styles/globals.css";
import type { AppType } from "next/app";
import { Plus_Jakarta_Sans } from "next/font/google";
import Script from "next/script";
import { env } from "next-runtime-env";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { useEffect } from "react";
import { ModalProvider } from "~/providers/modal";
import { PopupProvider } from "~/providers/popup";
@@ -21,6 +25,21 @@ export const metadata = {
};
const MyApp: AppType = ({ Component, pageProps }) => {
const posthogKey = env("NEXT_PUBLIC_POSTHOG_KEY");
useEffect(() => {
if (posthogKey) {
posthog.init(posthogKey, {
api_host: env("NEXT_PUBLIC_POSTHOG_HOST"),
person_profiles: "identified_only",
defaults: "2025-05-24",
loaded: (posthog) => {
if (process.env.NODE_ENV === "development") posthog.debug();
},
});
}
}, [posthogKey]);
return (
<>
<style jsx global>{`
@@ -32,7 +51,7 @@ const MyApp: AppType = ({ Component, pageProps }) => {
}
`}</style>
{env("NEXT_PUBLIC_UMAMI_ID") && (
<script
<Script
defer
src="https://cloud.umami.is/script.js"
data-website-id={env("NEXT_PUBLIC_UMAMI_ID")}
@@ -43,7 +62,13 @@ const MyApp: AppType = ({ Component, pageProps }) => {
<ThemeProvider>
<ModalProvider>
<PopupProvider>
<Component {...pageProps} />
{posthogKey ? (
<PostHogProvider client={posthog}>
<Component {...pageProps} />
</PostHogProvider>
) : (
<Component {...pageProps} />
)}
</PopupProvider>
</ModalProvider>
</ThemeProvider>

62
cloud/docker-compose.yml Normal file
View File

@@ -0,0 +1,62 @@
services:
web:
image: ghcr.io/kanbn/kan:latest
container_name: ${CONTAINER_NAME:-kan-web}
ports:
- "${WEB_PORT:-3000}:3000"
networks:
- dokploy-network
build:
context: .
dockerfile: ./apps/web/Dockerfile
env_file:
- .env
environment:
- NEXT_PUBLIC_KAN_ENV=${NEXT_PUBLIC_KAN_ENV}
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- POSTGRES_URL=${POSTGRES_URL}
- NEXT_PUBLIC_USE_STANDALONE_OUTPUT=${NEXT_PUBLIC_USE_STANDALONE_OUTPUT}
# Stripe
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
# SMTP
- SMTP_HOST=${SMTP_HOST}
- SMTP_PORT=${SMTP_PORT}
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- EMAIL_FROM=${EMAIL_FROM}
# S3 storage
- 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
- NEXT_PUBLIC_ALLOW_CREDENTIALS=${NEXT_PUBLIC_ALLOW_CREDENTIALS}
- NEXT_PUBLIC_DISABLE_SIGN_UP=${NEXT_PUBLIC_DISABLE_SIGN_UP}
# Integration providers
- 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}
# Analytics
- NEXT_PUBLIC_UMAMI_ID=${NEXT_PUBLIC_UMAMI_ID}
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
- NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST}
networks:
dokploy-network:
external: true
#bump

View File

@@ -1,7 +1,7 @@
services:
web:
image: ghcr.io/kanbn/kan:latest
container_name: kan-web
container_name: ${CONTAINER_NAME:-kan-web}
ports:
- "${WEB_PORT:-3000}:3000"
networks:
@@ -9,7 +9,7 @@ services:
- dokploy-network
build:
context: .
dockerfile: ./apps/web/Dockerfile
dockerfile: ./apps/web/Dockerfile.new
env_file:
- .env
environment:
@@ -87,6 +87,8 @@ services:
- 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:
- postgres
restart: unless-stopped

41
pnpm-lock.yaml generated
View File

@@ -159,6 +159,9 @@ importers:
pino:
specifier: ^9.6.0
version: 9.6.0
posthog-js:
specifier: ^1.254.0
version: 1.254.0
react:
specifier: catalog:react18
version: 18.3.1
@@ -3064,6 +3067,9 @@ packages:
core-js-pure@3.39.0:
resolution: {integrity: sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==}
core-js@3.43.0:
resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==}
cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
@@ -3664,6 +3670,9 @@ packages:
resolution: {integrity: sha512-k/2rVBRIRzOeom3wI9jBPaSEvoTSQEW4iM0EveBmBBKFxO8mSyyRWtDlfC3VnEfu0avmjrMzy8/ZFPSe6F71Hw==}
engines: {node: '>=14.0.0'}
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
@@ -5199,6 +5208,20 @@ packages:
resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==}
engines: {node: '>=12'}
posthog-js@1.254.0:
resolution: {integrity: sha512-ISJEsYTIx8FJ0W78q2lfueSrrcuNKCjGfgk9LLuRhAfxRJ+N5ljyGreeEWVrkli2QJBAWje0qYUcregBcIs33A==}
peerDependencies:
'@rrweb/types': 2.0.0-alpha.17
rrweb-snapshot: 2.0.0-alpha.17
peerDependenciesMeta:
'@rrweb/types':
optional: true
rrweb-snapshot:
optional: true
preact@10.26.9:
resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -6250,6 +6273,9 @@ packages:
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -9375,6 +9401,8 @@ snapshots:
core-js-pure@3.39.0: {}
core-js@3.43.0: {}
cors@2.8.5:
dependencies:
object-assign: 4.1.1
@@ -10079,6 +10107,8 @@ snapshots:
sharp: 0.33.5
xml2js: 0.6.2
fflate@0.4.8: {}
figures@3.2.0:
dependencies:
escape-string-regexp: 1.0.5
@@ -12078,6 +12108,15 @@ snapshots:
postgres@3.4.5:
optional: true
posthog-js@1.254.0:
dependencies:
core-js: 3.43.0
fflate: 0.4.8
preact: 10.26.9
web-vitals: 4.2.4
preact@10.26.9: {}
prelude-ls@1.2.1: {}
prettier-plugin-tailwindcss@0.6.9(@ianvs/prettier-plugin-sort-imports@4.4.0(prettier@3.4.2))(prettier@3.4.2):
@@ -13320,6 +13359,8 @@ snapshots:
dependencies:
defaults: 1.0.4
web-vitals@4.2.4: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:

View File

@@ -100,6 +100,7 @@
"NEXT_PUBLIC_AVATAR_BUCKET_NAME",
"NEXT_PUBLIC_ALLOW_CREDENTIALS",
"NEXT_PUBLIC_DISABLE_SIGN_UP",
"NEXT_PUBLIC_USE_STANDALONE_OUTPUT",
"S3_REGION",
"S3_ACCESS_KEY_ID",
"S3_SECRET_ACCESS_KEY",