diff --git a/.gitignore b/.gitignore index 1c39b5fc..35a6ee69 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ dump-* # Logs logs +!self-hosting/logs _.log npm-debug.log_ yarn-debug.log* diff --git a/README.md b/README.md index e300dbf1..7bcc0723 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,12 @@ Openpanel is a simple analytics tool for logging events on web, apps and backend - tRPC - will probably migrate this to server actions - Clerk - for authentication -## Self hosting +## Self-hosting -I'll fill out this section when we're out of beta (might be sooner than that). +OpenPanel can be self-hosted and we have tried to make it as simple as possible. -But it will probably be a CapRover recipe and Docker Compose scheme. +You can find the how to [here](https://docs.openpanel.dev/docs/self-hosting) + +**Give us a star if you like it!** + +[![Star History Chart](https://api.star-history.com/svg?repos=Openpanel-dev/openpanel&type=Date)](https://star-history.com/#Openpanel-dev/openpanel&Date) diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 189921f4..5295ebda 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,86 +1,95 @@ -# Dockerfile that builds the web app only ARG NODE_VERSION=20.15.1 -FROM --platform=linux/amd64 node:${NODE_VERSION}-slim AS base + +FROM node:${NODE_VERSION}-slim AS base + +RUN corepack enable && apt-get update && \ +apt-get install -y --no-install-recommends \ +ca-certificates \ +openssl \ +libssl3 \ +&& apt-get clean && \ +rm -rf /var/lib/apt/lists/* ARG DATABASE_URL ENV DATABASE_URL=$DATABASE_URL - ENV PNPM_HOME="/pnpm" - ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable - -RUN apt update \ - && apt install -y curl python3 make g++ \ - && curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \ - && bash n $NODE_VERSION \ - && rm n \ - && npm install -g n \ - && rm -rf /var/cache/apk/* - WORKDIR /app -COPY package.json package.json -COPY pnpm-lock.yaml pnpm-lock.yaml -COPY pnpm-workspace.yaml pnpm-workspace.yaml -COPY apps/api/package.json apps/api/package.json -COPY packages/db/package.json packages/db/package.json -COPY packages/redis/package.json packages/redis/package.json -COPY packages/trpc/package.json packages/trpc/package.json -COPY packages/queue/package.json packages/queue/package.json -COPY packages/common/package.json packages/common/package.json -COPY packages/constants/package.json packages/constants/package.json -COPY packages/validation/package.json packages/validation/package.json -COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json +# Workspace +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +# Apps +COPY apps/api/package.json ./apps/api/ +# Packages +COPY packages/db/package.json packages/db/ +COPY packages/trpc/package.json packages/trpc/ +COPY packages/queue/package.json packages/queue/ +COPY packages/redis/package.json packages/redis/ +COPY packages/common/package.json packages/common/ +COPY packages/sdks/sdk/package.json packages/sdks/sdk/ +COPY packages/constants/package.json packages/constants/ +COPY packages/validation/package.json packages/validation/ +COPY packages/sdks/sdk/package.json packages/sdks/sdk/ + +# Patches COPY patches patches # BUILD FROM base AS build -WORKDIR /app/apps/api -RUN pnpm install --frozen-lockfile +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + make \ + g++ -WORKDIR /app -COPY apps/api apps/api -COPY packages packages -COPY tooling tooling -RUN pnpm db:codegen +WORKDIR /app +RUN pnpm install --frozen-lockfile && \ + pnpm store prune -WORKDIR /app/apps/api -RUN pnpm run build +COPY apps/api ./apps/api +COPY packages ./packages +COPY tooling ./tooling + +RUN pnpm db:codegen && \ + pnpm --filter api run build # PROD FROM base AS prod -WORKDIR /app/apps/api -RUN pnpm install --frozen-lockfile --prod +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + make \ + g++ + +WORKDIR /app +COPY --from=build /app/package.json ./ +COPY --from=build /app/pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod && \ + pnpm store prune # FINAL FROM base AS runner -COPY --from=build /app/package.json /app/package.json -COPY --from=prod /app/node_modules /app/node_modules +ENV NODE_ENV=production + +WORKDIR /app + +COPY --from=build /app/package.json ./ +COPY --from=prod /app/node_modules ./node_modules # Apps -COPY --from=build /app/apps/api /app/apps/api - -# Apps node_modules -COPY --from=prod /app/apps/api/node_modules /app/apps/api/node_modules +COPY --from=build /app/apps/api ./apps/api # Packages -COPY --from=build /app/packages/db /app/packages/db -COPY --from=build /app/packages/redis /app/packages/redis -COPY --from=build /app/packages/trpc /app/packages/trpc -COPY --from=build /app/packages/queue /app/packages/queue -COPY --from=build /app/packages/common /app/packages/common - -# Packages node_modules -COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules -COPY --from=prod /app/packages/redis/node_modules /app/packages/redis/node_modules -COPY --from=prod /app/packages/trpc/node_modules /app/packages/trpc/node_modules -COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules -COPY --from=prod /app/packages/common/node_modules /app/packages/common/node_modules +COPY --from=build /app/packages/db ./packages/db +COPY --from=build /app/packages/trpc ./packages/trpc +COPY --from=build /app/packages/queue ./packages/queue +COPY --from=build /app/packages/redis ./packages/redis +COPY --from=build /app/packages/common ./packages/common +COPY --from=build /app/packages/sdks/sdk ./packages/sdks/sdk +COPY --from=build /app/packages/constants ./packages/constants +COPY --from=build /app/packages/validation ./packages/validation RUN pnpm db:codegen diff --git a/apps/api/src/controllers/misc.controller.ts b/apps/api/src/controllers/misc.controller.ts index 08861501..3a094fb0 100644 --- a/apps/api/src/controllers/misc.controller.ts +++ b/apps/api/src/controllers/misc.controller.ts @@ -5,6 +5,7 @@ import icoToPng from 'ico-to-png'; import sharp from 'sharp'; import { createHash } from '@openpanel/common'; +import { ch, TABLE_NAMES } from '@openpanel/db'; import { getRedisCache } from '@openpanel/redis'; interface GetFaviconParams { @@ -110,3 +111,37 @@ export async function clearFavicons( } return reply.status(404).send('OK'); } + +export async function ping( + request: FastifyRequest<{ + Body: { + domain: string; + count: number; + }; + }>, + reply: FastifyReply +) { + try { + await ch.insert({ + table: TABLE_NAMES.self_hosting, + values: [ + { + domain: request.body.domain, + count: request.body.count, + created_at: new Date(), + }, + ], + format: 'JSONEachRow', + }); + reply.status(200).send({ + message: `Success`, + count: request.body.count, + domain: request.body.domain, + }); + } catch (e) { + logger.error(e, 'Failed to insert ping'); + reply.status(500).send({ + error: 'Failed to insert ping', + }); + } +} diff --git a/apps/api/src/controllers/webhook.controller.ts b/apps/api/src/controllers/webhook.controller.ts index 941fbf75..2326e30b 100644 --- a/apps/api/src/controllers/webhook.controller.ts +++ b/apps/api/src/controllers/webhook.controller.ts @@ -44,6 +44,7 @@ export async function clerkWebhook( if (payload.type === 'user.created') { const email = payload.data.email_addresses[0]?.email_address; + const emails = payload.data.email_addresses.map((e) => e.email_address); if (!email) { return Response.json( @@ -63,14 +64,16 @@ export async function clerkWebhook( const memberships = await db.member.findMany({ where: { - email, + email: { + in: emails, + }, userId: null, }, }); for (const membership of memberships) { const access = pathOr([], ['meta', 'access'], membership); - db.$transaction([ + await db.$transaction([ // Update the member to link it to the user // This will remove the item from invitations db.member.update({ @@ -123,7 +126,6 @@ export async function clerkWebhook( deletedAt: new Date(), firstName: null, lastName: null, - email: `deleted+${payload.data.id}@openpanel.dev`, }, }), db.projectAccess.deleteMany({ diff --git a/apps/api/src/routes/misc.router.ts b/apps/api/src/routes/misc.router.ts index 35af774c..e03c9cb1 100644 --- a/apps/api/src/routes/misc.router.ts +++ b/apps/api/src/routes/misc.router.ts @@ -2,6 +2,12 @@ import * as controller from '@/controllers/misc.controller'; import type { FastifyPluginCallback } from 'fastify'; const miscRouter: FastifyPluginCallback = (fastify, opts, done) => { + fastify.route({ + method: 'POST', + url: '/ping', + handler: controller.ping, + }); + fastify.route({ method: 'GET', url: '/favicon', diff --git a/apps/dashboard/Dockerfile b/apps/dashboard/Dockerfile index 106a3e8e..089a7480 100644 --- a/apps/dashboard/Dockerfile +++ b/apps/dashboard/Dockerfile @@ -1,6 +1,6 @@ ARG NODE_VERSION=20.15.1 -FROM --platform=linux/amd64 node:${NODE_VERSION}-slim AS base +FROM node:${NODE_VERSION}-slim AS base ENV SKIP_ENV_VALIDATION="1" @@ -11,17 +11,15 @@ ARG ENABLE_INSTRUMENTATION_HOOK ENV ENABLE_INSTRUMENTATION_HOOK=$ENABLE_INSTRUMENTATION_HOOK ENV PNPM_HOME="/pnpm" - ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable +# Install necessary dependencies for prisma +RUN apt-get update && apt-get install -y \ + openssl \ + libssl3 \ + && rm -rf /var/lib/apt/lists/* -RUN apt update \ - && apt install -y curl \ - && curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \ - && bash n $NODE_VERSION \ - && rm n \ - && npm install -g n +RUN corepack enable WORKDIR /app @@ -39,14 +37,14 @@ COPY packages/common/package.json packages/common/package.json COPY packages/constants/package.json packages/constants/package.json COPY packages/validation/package.json packages/validation/package.json COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json +COPY patches patches # BUILD FROM base AS build -WORKDIR /app/apps/dashboard +WORKDIR /app RUN pnpm install --frozen-lockfile --ignore-scripts -WORKDIR /app COPY apps/dashboard apps/dashboard COPY packages packages COPY tooling tooling @@ -57,48 +55,44 @@ WORKDIR /app/apps/dashboard # Will be replaced on runtime ENV NEXT_PUBLIC_DASHBOARD_URL="__NEXT_PUBLIC_DASHBOARD_URL__" ENV NEXT_PUBLIC_API_URL="__NEXT_PUBLIC_API_URL__" -# Check entrypoint for this little fellow ENV NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_eW9sby5jb20k" +# Does not need to be replaced +ENV NEXT_PUBLIC_CLERK_SIGN_IN_URL="/login" +ENV NEXT_PUBLIC_CLERK_SIGN_UP_URL="/register" +ENV NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/" +ENV NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/" RUN pnpm run build -# PROD -FROM base AS prod - -WORKDIR /app/apps/dashboard -RUN pnpm install --frozen-lockfile --prod --ignore-scripts - -# FINAL +# RUNNER FROM base AS runner -COPY --from=build /app/package.json /app/package.json -COPY --from=prod /app/node_modules /app/node_modules -# Apps -COPY --from=build /app/apps/dashboard /app/apps/dashboard -# Apps node_modules -COPY --from=prod /app/apps/dashboard/node_modules /app/apps/dashboard/node_modules -# Packages -COPY --from=build /app/packages/db /app/packages/db -COPY --from=build /app/packages/redis /app/packages/redis -COPY --from=build /app/packages/common /app/packages/common -COPY --from=build /app/packages/queue /app/packages/queue -COPY --from=build /app/packages/constants /app/packages/constants -COPY --from=build /app/packages/validation /app/packages/validation -COPY --from=build /app/packages/sdks/sdk /app/packages/sdks/sdk -# Packages node_modules -COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules -COPY --from=prod /app/packages/redis/node_modules /app/packages/redis/node_modules -COPY --from=prod /app/packages/common/node_modules /app/packages/common/node_modules -COPY --from=prod /app/packages/validation/node_modules /app/packages/validation/node_modules -COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules +WORKDIR /app -RUN pnpm db:codegen +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 -WORKDIR /app/apps/dashboard +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Set the correct permissions for the entire /app directory +COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/.next/standalone ./ +COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/.next/static ./apps/dashboard/.next/static +COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/public ./apps/dashboard/public + +# Copy and set permissions for the entrypoint script +COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh + +USER nextjs EXPOSE 3000 -# CMD ["pnpm", "start"] -COPY --from=build /app/apps/dashboard/entrypoint.sh /usr/bin/ -RUN chmod +x /usr/bin/entrypoint.sh -ENTRYPOINT ["entrypoint.sh", "pnpm", "start"] \ No newline at end of file +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +ENTRYPOINT [ "/app/entrypoint.sh", "node", "/app/apps/dashboard/server.js"] \ No newline at end of file diff --git a/apps/dashboard/entrypoint.sh b/apps/dashboard/entrypoint.sh index 7bfe81de..ef22975e 100644 --- a/apps/dashboard/entrypoint.sh +++ b/apps/dashboard/entrypoint.sh @@ -1,32 +1,39 @@ -#!/bin/bash +#!/bin/sh set -e echo "> Replace env variable placeholders with runtime values..." -# Define an array of environment variables to check -variables_to_replace=( - "NEXT_PUBLIC_DASHBOARD_URL" - "NEXT_PUBLIC_API_URL" - "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY" -) + +# Define environment variables to check (space-separated string) +variables_to_replace="NEXT_PUBLIC_DASHBOARD_URL NEXT_PUBLIC_API_URL NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY" # Replace env variable placeholders with real values -for key in "${variables_to_replace[@]}"; do - value=$(printenv $key) - if [ ! -z "$value" ]; then - echo " - Searching for $key with value $value..." - # Use a custom placeholder for 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY' or use the actual key otherwise - if [ "$key" = "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY" ]; then - placeholder="pk_test_eW9sby5jb20k" +for key in $variables_to_replace; do + value=$(eval echo \$"$key") + if [ -n "$value" ]; then + echo " - Searching for $key with value $value..." + # Use a custom placeholder for 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY' or use the actual key otherwise + case "$key" in + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) + placeholder="pk_test_eW9sby5jb20k" + ;; + *) + placeholder="__${key}__" + ;; + esac + # Run the replacement + find /app -type f \( -name "*.js" -o -name "*.html" \) | while read -r file; do + if grep -q "$placeholder" "$file"; then + echo " - Replacing in file: $file" + sed -i "s|$placeholder|$value|g" "$file" + fi + done else - placeholder="__${key}__" + echo " - Skipping $key as it has no value set." fi - # Run the replacement - find /app/apps/dashboard/.next/ -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i "s|$placeholder|$value|g" {} \; - - else - echo " - Skipping $key as it has no value set." - fi done +echo "> Done!" +echo "> Running $@" + # Execute the container's main process (CMD in Dockerfile) exec "$@" \ No newline at end of file diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index dd91bdc0..a3b94d9a 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -9,6 +9,7 @@ await import('./src/env.mjs'); /** @type {import("next").NextConfig} */ const config = { + output: 'standalone', webpack: (config, { isServer }) => { if (isServer) { config.plugins = [...config.plugins, new PrismaPlugin()]; diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index eab5d1fc..4213005f 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "rm -rf .next && pnpm with-env next dev", "testing": "pnpm dev", - "build": "next build", + "build": "pnpm with-env next build", "start": "next start", "lint": "eslint .", "format": "prettier --check \"**/*.{tsx,mjs,ts,md,json}\"", diff --git a/apps/dashboard/src/app/(onboarding)/skip-onboarding.tsx b/apps/dashboard/src/app/(onboarding)/skip-onboarding.tsx index cc925789..d5da4359 100644 --- a/apps/dashboard/src/app/(onboarding)/skip-onboarding.tsx +++ b/apps/dashboard/src/app/(onboarding)/skip-onboarding.tsx @@ -16,9 +16,8 @@ const SkipOnboarding = () => { res.refetch(); }, [pathname]); - console.log(res.data); - if (!pathname.startsWith('/onboarding')) return null; + return (