Self-hosting! (#49)

* added self-hosting
This commit is contained in:
Carl-Gerhard Lindesvärd
2024-08-28 09:28:44 +02:00
committed by GitHub
parent f0b7526847
commit df05e2dab3
70 changed files with 2310 additions and 272 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ dump-*
# Logs
logs
!self-hosting/logs
_.log
npm-debug.log_
yarn-debug.log*

View File

@@ -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)

View File

@@ -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

View File

@@ -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',
});
}
}

View File

@@ -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<string[]>([], ['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({

View File

@@ -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',

View File

@@ -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"]
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
ENTRYPOINT [ "/app/entrypoint.sh", "node", "/app/apps/dashboard/server.js"]

View File

@@ -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 "$@"

View File

@@ -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()];

View File

@@ -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}\"",

View File

@@ -16,9 +16,8 @@ const SkipOnboarding = () => {
res.refetch();
}, [pathname]);
console.log(res.data);
if (!pathname.startsWith('/onboarding')) return null;
return (
<button
onClick={() => {
@@ -30,6 +29,7 @@ const SkipOnboarding = () => {
text: 'Are you sure you want to skip onboarding? Since you do not have any projects, you will be logged out.',
onConfirm() {
auth.signOut();
router.replace(process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL);
},
});
}

View File

@@ -2,6 +2,7 @@ import type { MetadataRoute } from 'next';
export default function manifest(): MetadataRoute.Manifest {
return {
id: process.env.NEXT_PUBLIC_DASHBOARD_URL,
name: 'Openpanel.dev',
short_name: 'Openpanel.dev',
description: '',
@@ -9,12 +10,5 @@ export default function manifest(): MetadataRoute.Manifest {
display: 'standalone',
background_color: '#fff',
theme_color: '#fff',
icons: [
{
src: '/favicon.ico',
sizes: 'any',
type: 'image/x-icon',
},
],
};
}

View File

@@ -182,7 +182,7 @@ $openpanel->event(
<strong>Usage</strong>
<p>Create a custom event called &quot;my_event&quot;.</p>
<Syntax
code={`curl 'https://api.openpanel.dev/track' \\
code={`curl '${process.env.NEXT_PUBLIC_API_URL}/track' \\
-H 'content-type: application/json' \\
-H 'openpanel-client-id: ${clientId}' \\
-H 'openpanel-client-secret: ${clientSecret}' \\

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 424 KiB

View File

@@ -1,10 +1,10 @@
{
"index": {
"title": "Introduction",
"title": "Home",
"type": "page"
},
"docs": {
"title": "Documentation",
"type": "page"
}
}
}

View File

@@ -1,25 +1,6 @@
{
"index": "Get Started",
"-- Frameworks": {
"type": "separator",
"title": "Frameworks"
},
"script": "Web (Script Tag)",
"web": "Web (Module)",
"nextjs": "Next.js",
"react": "React",
"react-native": "React-Native",
"remix": "Remix",
"vue": "Vue",
"astro": "Astro",
"node": "Node",
"express": "Express (backend)",
"-- Others": {
"type": "separator",
"title": "Others"
},
"javascript": "JavaScript",
"api": "API",
"export": "Export",
"migration": "Migrations"
"sdks": "SDKs",
"migration": "Migrations",
"self-hosting": "Self-hosting"
}

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# Astro
You can use <Link href="/docs/script">script tag</Link> or <Link href="/docs/web">Web SDK</Link> to track events in Astro.

View File

@@ -68,7 +68,7 @@ Clears the current user identifier and ends the session.
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/240px-HTML5_logo_and_wordmark.svg.png" />
}
title="HTML / Script"
href="/docs/script"
href="/docs/sdks/script"
>
{' '}
</Card>
@@ -77,7 +77,7 @@ Clears the current user identifier and ends the session.
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png" />
}
title="React"
href="/docs/react"
href="/docs/sdks/react"
>
{' '}
</Card>
@@ -86,7 +86,7 @@ Clears the current user identifier and ends the session.
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png" />
}
title="React-Native"
href="/docs/react-native"
href="/docs/sdks/react-native"
>
{' '}
</Card>
@@ -94,11 +94,11 @@ Clears the current user identifier and ends the session.
icon={
<BrandLogo
isDark
src="https://static-00.iconduck.com/assets.00/nextjs-icon-512x512-y563b8iq.png"
src="https://pbs.twimg.com/profile_images/1565710214019444737/if82cpbS_400x400.jpg"
/>
}
title="Next.js"
href="/docs/nextjs"
href="/docs/sdks/nextjs"
>
{' '}
</Card>
@@ -110,7 +110,7 @@ Clears the current user identifier and ends the session.
/>
}
title="Remix"
href="/docs/remix"
href="/docs/sdks/remix"
>
{' '}
</Card>
@@ -119,7 +119,7 @@ Clears the current user identifier and ends the session.
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1024px-Vue.js_Logo_2.svg.png" />
}
title="Vue"
href="/docs/vue"
href="/docs/sdks/vue"
>
{' '}
</Card>
@@ -131,7 +131,7 @@ Clears the current user identifier and ends the session.
/>
}
title="Astro"
href="/docs/astro"
href="/docs/sdks/astro"
>
{' '}
</Card>
@@ -140,3 +140,28 @@ Clears the current user identifier and ends the session.
## Unofficial SDKs
While not officially supported, the following community-contributed SDKs are available.
<Cards>
<Card
icon={
<BrandLogo
src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Laravel.svg/1200px-Laravel.svg.png"
/>
}
title="Laravel"
href="https://github.com/tbleckert/openpanel-laravel"
>
{' '}
</Card>
<Card
icon={
<BrandLogo
src="https://storage.googleapis.com/cms-storage-bucket/0dbfcc7a59cd1cf16282.png"
/>
}
title="Flutter"
href="https://github.com/stevenosse/openpanel_flutter"
>
{' '}
</Card>
</Cards>

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# Node
Use <Link href="/docs/javascript">Javascript SDK</Link> to track events in Node.

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# React
Use <Link href="/docs/script">script tag</Link> or <Link href="/docs/web">Web SDK</Link> for now. We'll add a dedicated react sdk soon.

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# Remix
Use <Link href="/docs/script">script tag</Link> or <Link href="/docs/web">Web SDK</Link> for now. We'll add a dedicated remix sdk soon.

View File

@@ -0,0 +1,19 @@
{
"script": "Web (Script Tag)",
"web": "Web (Module)",
"nextjs": "Next.js",
"react": "React",
"react-native": "React-Native",
"remix": "Remix",
"vue": "Vue",
"astro": "Astro",
"node": "Node",
"express": "Express (backend)",
"-- Others": {
"type": "separator",
"title": "Others"
},
"javascript": "JavaScript",
"api": "API",
"export": "Export"
}

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# Astro
You can use <Link href="/docs/sdks/script">script tag</Link> or <Link href="/docs/sdks/web">Web SDK</Link> to track events in Astro.

View File

@@ -7,7 +7,7 @@ import CommonSdkConfig from 'src/components/common-sdk-config.mdx';
# Express
The Express middleware is a basic wrapper around [Javascript SDK](/docs/javascript). It provides a simple way to add the SDK to your Express application.
The Express middleware is a basic wrapper around [Javascript SDK](/docs/sdks/javascript). It provides a simple way to add the SDK to your Express application.
## Installation

View File

@@ -121,7 +121,7 @@ export const op = new Openpanel({
op.track('my_event', { foo: 'bar' });
```
Refer to the [Javascript SDK](/docs/javascript#usage) for usage instructions.
Refer to the [Javascript SDK](/docs/sdks/javascript#usage) for usage instructions.
### Tracking Events

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# Node
Use <Link href="/docs/sdks/javascript">Javascript SDK</Link> to track events in Node.

View File

@@ -108,4 +108,4 @@ op.track('my_event', { foo: 'bar' });
</Tabs.Tab>
</Tabs>
For more information on how to use the SDK, check out the [Javascript SDK](/docs/javascript#usage).
For more information on how to use the SDK, check out the [Javascript SDK](/docs/sdks/javascript#usage).

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# React
Use <Link href="/docs/sdks/script">script tag</Link> or <Link href="/docs/sdks/web">Web SDK</Link> for now. We'll add a dedicated react sdk soon.

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# Remix
Use <Link href="/docs/sdks/script">script tag</Link> or <Link href="/docs/sdks/web">Web SDK</Link> for now. We'll add a dedicated remix sdk soon.

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# Vue
Use <Link href="/docs/sdks/script">script tag</Link> or <Link href="/docs/sdks/web">Web SDK</Link> for now. We'll add a dedicated react sdk soon.

View File

@@ -45,4 +45,4 @@ op.track('my_event', { foo: 'bar' });
## Usage
Refer to the [Javascript SDK](/docs/javascript#usage) for usage instructions.
Refer to the [Javascript SDK](/docs/sdks/javascript#usage) for usage instructions.

View File

@@ -0,0 +1,3 @@
{
"index": "Get started"
}

View File

@@ -0,0 +1,121 @@
import { Callout, Tabs, Steps } from 'nextra/components';
# Self-hosting
<Callout>OpenPanel is not stable yet. If you still want to self-host you can go ahead. Bear in mind that new changes might give a little headache to keep up with.</Callout>
This is a simple guide how to get started with OpenPanel on your own VPS.
## Instructions
### Prerequisites
- VPS of any kind (only tested on Ubuntu 24.04)
- [Clerk.com](https://clerk.com) account (they have a free tier)
### Quickstart
```bash
git clone https://github.com/Openpanel-dev/openpanel && cd openpanel/self-hosting && ./start
# After setup is complete run `./start` to start OpenPanel
```
<Steps>
### Clone
Clone the repository to your VPS
```bash
git clone https://github.com/Openpanel-dev/openpanel.git
```
### Run the setup script
The setup script will do 3 things
1. Install node (if you accept)
2. Install docker (if you accept)
3. Execute a node script that will ask some questions about your setup
4. After this is done you'll need to point a webhook inside Clerk (https://your-domain.com/api/webhook/clerk)
> Setup takes 5-10 minutes depending on your VPS. It'll build all the docker images.
```bash
cd openpanel/self-hosting
./setup
```
### Start 🚀
Run the `./start` script located inside the self-hosting folder
```bash
./start
```
</Steps>
## Clerk.com
<Callout>
Some might wonder why we use Clerk.com for authentication. The main reason for this is that Clerk have great support for iOS and Android apps. We're in the process of building an iOS app and we want to have a seamless experience for our users.
**next-auth** is great, but lacks good support for mobile apps.
</Callout>
You'll need to create an account at [Clerk.com](https://clerk.com) and create a new project. You'll need the 3 keys that Clerk provides you with.
- **Publishable key** `pk_live_xxx`
- **Secret key** `sk_live_xxx`
- **Signing secret** `"whsec_xxx"`
### Webhooks
You'll also need to add a webhook to your domain. We listen on some events from Clerk to keep our database in sync.
#### URL
- **Path**: `/api/webhook/clerk`
- **Example**: `https://your-domain.com/api/webhook/clerk`
#### Events we listen to
- `organizationMembership.created`
- `user.created`
- `organizationMembership.deleted`
- `user.updated`
- `user.deleted`
## Good to know
### Always use correct api url
When self-hosting you'll need to provide your api url when initializing the SDK.
The path should be `/api` and the domain should be your domain.
```html filename="index.html" {4}
<script>
window.op = window.op||function(...args){(window.op.q=window.op.q||[]).push(args);};
window.op('init', {
apiUrl: 'https://your-domain.com/api',
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
</script>
<script src="https://openpanel.dev/op1.js" defer async></script>
```
```js filename="op.ts" {4}
import { OpenPanel } from '@openpanel/sdk';
const op = new OpenPanel({
apiUrl: 'https://your-domain.com/api',
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# Vue
Use <Link href="/docs/script">script tag</Link> or <Link href="/docs/web">Web SDK</Link> for now. We'll add a dedicated vue sdk soon.

View File

@@ -1,3 +1,5 @@
<img src="https://openpanel.dev/ogimage.png" />
# Introduction
Openpanel is an open-source alternative to Mixpanel. Combining the power of Mixpanel with the ease of Plausible, Openpanel is a privacy-focused analytics tool that gives you the insights you need to make data-driven decisions.

View File

@@ -19,7 +19,7 @@ export default {
height="32"
width="32"
/>
<strong style={{ marginLeft: '8px' }}>openpanel</strong>
<strong style={{ marginLeft: '8px' }}>OpenPanel</strong>
</>
),
head: () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 428 KiB

View File

@@ -1,85 +1,77 @@
ARG NODE_VERSION=20.15.1
FROM --platform=linux/amd64 node:${NODE_VERSION}-slim AS base
FROM node:${NODE_VERSION}-slim AS base
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 \
&& 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 && \
apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
openssl \
libssl3 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json package.json
COPY pnpm-lock.yaml pnpm-lock.yaml
COPY pnpm-workspace.yaml pnpm-workspace.yaml
COPY apps/worker/package.json apps/worker/package.json
COPY packages/db/package.json packages/db/package.json
COPY packages/redis/package.json packages/redis/package.json
COPY packages/logger/package.json packages/logger/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
COPY patches patches
# Workspace
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
# Apps
COPY apps/worker/package.json ./apps/worker/
# Packages
COPY packages/db/package.json ./packages/db/
COPY packages/redis/package.json ./packages/redis/
COPY packages/queue/package.json ./packages/queue/
COPY packages/logger/package.json ./packages/logger/
COPY packages/common/package.json ./packages/common/
COPY packages/constants/package.json ./packages/constants/
# Patches
COPY patches ./patches
# BUILD
FROM base AS build
WORKDIR /app/apps/worker
RUN pnpm install --frozen-lockfile --ignore-scripts
WORKDIR /app
RUN pnpm install --frozen-lockfile && \
pnpm store prune
WORKDIR /app
COPY apps/worker apps/worker
COPY packages packages
COPY tooling tooling
RUN pnpm db:codegen
COPY apps/worker ./apps/worker
COPY packages ./packages
COPY tooling ./tooling
WORKDIR /app/apps/worker
RUN pnpm run build
RUN pnpm db:codegen && \
pnpm --filter worker run build
# PROD
FROM base AS prod
WORKDIR /app/apps/worker
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
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
WORKDIR /app
COPY --from=build /app/package.json ./
COPY --from=prod /app/node_modules ./node_modules
# Apps
COPY --from=build /app/apps/worker /app/apps/worker
# Apps node_modules
COPY --from=prod /app/apps/worker/node_modules /app/apps/worker/node_modules
COPY --from=build /app/apps/worker ./apps/worker
# Packages
COPY --from=build /app/packages/db /app/packages/db
COPY --from=build /app/packages/redis /app/packages/redis
COPY --from=build /app/packages/logger /app/packages/logger
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/logger/node_modules /app/packages/logger/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/redis ./packages/redis
COPY --from=build /app/packages/logger ./packages/logger
COPY --from=build /app/packages/queue ./packages/queue
COPY --from=build /app/packages/common ./packages/common
RUN pnpm db:codegen

View File

@@ -12,8 +12,8 @@
},
"dependencies": {
"@baselime/pino-transport": "^0.1.5",
"@bull-board/api": "^5.21.0",
"@bull-board/express": "^5.21.0",
"@bull-board/api": "5.21.0",
"@bull-board/express": "5.21.0",
"@openpanel/common": "workspace:*",
"@openpanel/db": "workspace:*",
"@openpanel/logger": "workspace:*",

View File

@@ -5,6 +5,7 @@ import type { WorkerOptions } from 'bullmq';
import { Worker } from 'bullmq';
import express from 'express';
import { createInitialSalts } from '@openpanel/db';
import { cronQueue, eventsQueue, sessionsQueue } from '@openpanel/queue';
import { getRedisQueue } from '@openpanel/redis';
@@ -15,7 +16,7 @@ import { register } from './metrics';
const PORT = parseInt(process.env.WORKER_PORT || '3000', 10);
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath(process.env.SELF_HOSTED ? '/worker' : '/');
serverAdapter.setBasePath('/');
const app = express();
const workerOptions: WorkerOptions = {
@@ -162,10 +163,28 @@ async function start() {
}
);
if (process.env.SELF_HOSTED && process.env.NODE_ENV === 'production') {
await cronQueue.add(
'ping',
{
type: 'ping',
payload: undefined,
},
{
jobId: 'ping',
repeat: {
pattern: '0 0 * * *',
},
}
);
}
const repeatableJobs = await cronQueue.getRepeatableJobs();
console.log('Repeatable jobs:');
console.log(repeatableJobs);
await createInitialSalts();
}
start();

View File

@@ -0,0 +1,26 @@
import { chQuery, TABLE_NAMES } from '@openpanel/db';
export async function ping() {
const [res] = await chQuery<{ count: number }>(
`SELECT COUNT(*) as count FROM ${TABLE_NAMES.events}`
);
if (typeof res?.count === 'number') {
const response = await fetch('https://api.openpanel.com/misc/ping', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
domain: process.env.NEXT_PUBLIC_DASHBOARD_URL,
count: res?.count,
}),
});
if (response.ok) {
return await response.json();
}
throw new Error('Failed to ping the server');
}
}

View File

@@ -3,6 +3,7 @@ import type { Job } from 'bullmq';
import { eventBuffer, profileBuffer } from '@openpanel/db';
import type { CronQueuePayload } from '@openpanel/queue';
import { ping } from './cron.ping';
import { salt } from './cron.salt';
export async function cronJob(job: Job<CronQueuePayload>) {
@@ -16,5 +17,8 @@ export async function cronJob(job: Job<CronQueuePayload>) {
case 'flushProfiles': {
return await profileBuffer.flush();
}
case 'ping': {
return await ping();
}
}
}

View File

@@ -1,6 +1,17 @@
CREATE DATABASE IF NOT EXISTS openpanel;
CREATE TABLE openpanel.events_v2 (
CREATE TABLE IF NOT EXISTS openpanel.self_hosting
(
created_at Date,
domain String,
count UInt64
)
ENGINE = MergeTree()
ORDER BY (domain, created_at)
PARTITION BY toYYYYMM(created_at);
CREATE TABLE IF NOT EXISTS openpanel.events_v2 (
`id` UUID DEFAULT generateUUIDv4(),
`name` String,
`sdk_name` String,
@@ -80,7 +91,7 @@ SELECT
uniqState(profile_id) as profile_id,
project_id
FROM
events
events_v2
GROUP BY
date,
project_id;

View File

@@ -5,6 +5,7 @@ export const TABLE_NAMES = {
events: 'events_v2',
profiles: 'profiles',
alias: 'profile_aliases',
self_hosting: 'self_hosting',
};
export const originalCh = createClient({

View File

@@ -1,3 +1,5 @@
import { generateSalt } from '@openpanel/common';
import { db } from '../prisma-client';
export async function getCurrentSalt() {
@@ -27,7 +29,7 @@ export async function getSalts() {
}
if (!prev) {
throw new Error('No previous salt found');
throw new Error('No salt found');
}
return {
@@ -35,3 +37,44 @@ export async function getSalts() {
previous: prev.salt,
};
}
export async function createInitialSalts() {
const MAX_RETRIES = 5;
const BASE_DELAY = 1000; // 1 second
const createSaltsWithRetry = async (retryCount = 0): Promise<void> => {
try {
await getSalts();
} catch (error) {
if (error instanceof Error && error.message === 'No salt found') {
console.log('Creating salts for the first time');
await db.salt.create({
data: {
salt: generateSalt(),
createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 24),
},
});
await db.salt.create({
data: {
salt: generateSalt(),
},
});
} else {
console.log('Error getting salts', error);
if (retryCount < MAX_RETRIES) {
const delay = BASE_DELAY * Math.pow(2, retryCount);
console.log(
`Retrying in ${delay}ms... (Attempt ${retryCount + 1}/${MAX_RETRIES})`
);
await new Promise((resolve) => setTimeout(resolve, delay));
return createSaltsWithRetry(retryCount + 1);
} else {
throw new Error(
`Failed to create salts after ${MAX_RETRIES} attempts`
);
}
}
}
};
await createSaltsWithRetry();
}

View File

@@ -56,10 +56,15 @@ export type CronQueuePayloadFlushProfiles = {
type: 'flushProfiles';
payload: undefined;
};
export type CronQueuePayloadPing = {
type: 'ping';
payload: undefined;
};
export type CronQueuePayload =
| CronQueuePayloadSalt
| CronQueuePayloadFlushEvents
| CronQueuePayloadFlushProfiles;
| CronQueuePayloadFlushProfiles
| CronQueuePayloadPing;
export const eventsQueue = new Queue<EventsQueuePayload>('events', {
connection: getRedisQueue(),

View File

@@ -1,7 +1,7 @@
const api = {
logo: 'https://cdn-icons-png.flaticon.com/512/10169/10169724.png',
name: 'Rest API',
href: 'https://docs.openpanel.dev/docs/api',
href: 'https://docs.openpanel.dev/docs/sdks/api',
} as const;
export const frameworks = {
@@ -9,32 +9,32 @@ export const frameworks = {
{
logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/240px-HTML5_logo_and_wordmark.svg.png',
name: 'HTML / Script',
href: 'https://docs.openpanel.dev/docs/script',
href: 'https://docs.openpanel.dev/docs/sdks/script',
},
{
logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png',
name: 'React',
href: 'https://docs.openpanel.dev/docs/react',
href: 'https://docs.openpanel.dev/docs/sdks/react',
},
{
logo: 'https://static-00.iconduck.com/assets.00/nextjs-icon-512x512-y563b8iq.png',
name: 'Next.js',
href: 'https://docs.openpanel.dev/docs/nextjs',
href: 'https://docs.openpanel.dev/docs/sdks/nextjs',
},
{
logo: 'https://www.datocms-assets.com/205/1642515307-square-logo.svg',
name: 'Remix',
href: 'https://docs.openpanel.dev/docs/remix',
href: 'https://docs.openpanel.dev/docs/sdks/remix',
},
{
logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1024px-Vue.js_Logo_2.svg.png',
name: 'Vue',
href: 'https://docs.openpanel.dev/docs/vue',
href: 'https://docs.openpanel.dev/docs/sdks/vue',
},
{
logo: 'https://astro.build/assets/press/astro-icon-dark.png',
name: 'Astro',
href: 'https://docs.openpanel.dev/docs/astro',
href: 'https://docs.openpanel.dev/docs/sdks/astro',
},
api,
],
@@ -42,7 +42,7 @@ export const frameworks = {
{
logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png',
name: 'React-Native',
href: 'https://docs.openpanel.dev/docs/react-native',
href: 'https://docs.openpanel.dev/docs/sdks/react-native',
},
api,
],
@@ -50,12 +50,12 @@ export const frameworks = {
{
logo: 'https://static-00.iconduck.com/assets.00/node-js-icon-454x512-nztofx17.png',
name: 'Node',
href: 'https://docs.openpanel.dev/docs/node',
href: 'https://docs.openpanel.dev/docs/sdks/node',
},
{
logo: 'https://expressjs.com/images/favicon.png',
name: 'Express',
href: 'https://docs.openpanel.dev/docs/express',
href: 'https://docs.openpanel.dev/docs/sdks/express',
},
{
logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Laravel.svg/1969px-Laravel.svg.png',

9
pnpm-lock.yaml generated
View File

@@ -735,10 +735,10 @@ importers:
specifier: ^0.1.5
version: 0.1.5
'@bull-board/api':
specifier: ^5.21.0
specifier: 5.21.0
version: 5.21.0(patch_hash=25udjn3ygs6h4rrgl46tnrqrn4)(@bull-board/ui@5.21.0)
'@bull-board/express':
specifier: ^5.21.0
specifier: 5.21.0
version: 5.21.0
'@openpanel/common':
specifier: workspace:*
@@ -8194,6 +8194,7 @@ packages:
/are-we-there-yet@2.0.0:
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
dependencies:
delegates: 1.0.0
readable-stream: 3.6.2
@@ -11674,6 +11675,7 @@ packages:
/gauge@3.0.2:
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
dependencies:
aproba: 2.0.0
color-support: 1.1.3
@@ -12307,6 +12309,7 @@ packages:
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
dependencies:
once: 1.4.0
wrappy: 1.0.2
@@ -15036,6 +15039,7 @@ packages:
/npmlog@5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
deprecated: This package is no longer supported.
dependencies:
are-we-there-yet: 2.0.0
console-control-strings: 1.1.0
@@ -16924,6 +16928,7 @@ packages:
/rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
dependencies:
glob: 7.2.3

View File

@@ -0,0 +1,22 @@
NODE_ENV="production"
SELF_HOSTED="true"
GEO_IP_HOST="http://op-geo:8080"
NEXT_PUBLIC_CLERK_SIGN_IN_URL="/login"
NEXT_PUBLIC_CLERK_SIGN_UP_URL="/register"
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/"
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/"
BATCH_SIZE="5000"
BATCH_INTERVAL="10000"
# Will be replaced with the setup script
REDIS_URL="$REDIS_URL"
CLICKHOUSE_URL="$CLICKHOUSE_URL"
CLICKHOUSE_DB="$CLICKHOUSE_DB"
CLICKHOUSE_USER="$CLICKHOUSE_USER"
CLICKHOUSE_PASSWORD="$CLICKHOUSE_PASSWORD"
DATABASE_URL="$DATABASE_URL"
DATABASE_URL_DIRECT="$DATABASE_URL_DIRECT"
NEXT_PUBLIC_DASHBOARD_URL="$NEXT_PUBLIC_DASHBOARD_URL"
NEXT_PUBLIC_API_URL="$NEXT_PUBLIC_API_URL"
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
CLERK_SECRET_KEY="$CLERK_SECRET_KEY"
CLERK_SIGNING_SECRET="$CLERK_SIGNING_SECRET"

3
self-hosting/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.env
docker-compose.yml
caddy/Caddyfile

View File

@@ -0,0 +1,19 @@
$DOMAIN_NAME {$SSL_CONFIG
encode gzip
handle_path /api* {
reverse_proxy op-api:3000
}
reverse_proxy /* op-dashboard:3000
}
worker.$DOMAIN_NAME {$SSL_CONFIG
encode gzip
basic_auth {
admin $BASIC_AUTH_PASSWORD
}
reverse_proxy op-worker:3000
}

View File

@@ -0,0 +1,25 @@
<clickhouse>
<logger>
<level>warning</level>
<console>true</console>
</logger>
<keep_alive_timeout>10</keep_alive_timeout>
<!--
Avoid the warning: "Listen [::]:9009 failed: Address family for hostname not supported".
If Docker has IPv6 disabled, bind ClickHouse to IPv4 to prevent this issue.
Add this to the configuration to ensure it listens on all IPv4 interfaces:
<listen_host>0.0.0.0</listen_host>
-->
<!-- Stop all the unnecessary logging -->
<query_thread_log remove="remove"/>
<query_log remove="remove"/>
<text_log remove="remove"/>
<trace_log remove="remove"/>
<metric_log remove="remove"/>
<asynchronous_metric_log remove="remove"/>
<session_log remove="remove"/>
<part_log remove="remove"/>
</clickhouse>

View File

@@ -0,0 +1,8 @@
<clickhouse>
<profiles>
<default>
<log_queries>0</log_queries>
<log_query_threads>0</log_query_threads>
</default>
</profiles>
</clickhouse>

View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Set the project name if it's not the directory name
# COMPOSE_PROJECT_NAME=your_project_name
# Use the directory name as the project name if not set
PROJECT_NAME=${COMPOSE_PROJECT_NAME:-$(basename "$(pwd)")}
echo "Cleaning up Docker resources for project: $PROJECT_NAME"
# Stop and remove containers, networks, and volumes
echo "Stopping and removing containers, networks, and volumes..."
docker-compose down --volumes --remove-orphans
# Remove any remaining project-specific volumes
echo "Removing any remaining project volumes..."
project_volumes=$(docker volume ls --filter name="$PROJECT_NAME" -q)
if [ -n "$project_volumes" ]; then
docker volume rm $project_volumes
fi
# Remove project-specific images
echo "Removing project-specific images..."
project_images=$(docker-compose config --images)
if [ -n "$project_images" ]; then
docker rmi $project_images
fi
# Remove any dangling images
echo "Removing dangling images..."
docker image prune -f
# Remove any dangling volumes
echo "Removing dangling volumes..."
docker volume prune -f
echo "Cleanup complete. All project containers, images, volumes, and related resources have been removed."
# List remaining containers, images, and volumes
echo "Remaining containers:"
docker ps -a
echo "Remaining images:"
docker images
echo "Remaining volumes:"
docker volume ls

View File

@@ -0,0 +1,150 @@
version: '3'
services:
op-proxy:
image: caddy:2-alpine
restart: always
ports:
- '80:80'
- '443:443'
volumes:
- op-proxy-data:/data
- op-proxy-config:/config
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
depends_on:
- op-dashboard
- op-api
op-db:
image: postgres:14-alpine
restart: always
volumes:
- op-db-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
ports:
- 5431:5432
op-kv:
image: redis:7.2.5-alpine
restart: always
volumes:
- op-kv-data:/data
command:
[
'redis-server',
'--requirepass',
'${REDIS_PASSWORD}',
'--maxmemory-policy',
'noeviction',
]
ports:
- 6378:6379
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
op-geo:
image: observabilitystack/geoip-api:latest
restart: always
op-ch:
image: clickhouse/clickhouse-server:23.3.7.5-alpine
restart: always
volumes:
- op-ch-data:/var/lib/clickhouse
- op-ch-logs:/var/log/clickhouse-server
- ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/op-config.xml:ro
- ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/op-user-config.xml:ro
environment:
- CLICKHOUSE_DB
- CLICKHOUSE_USER
- CLICKHOUSE_PASSWORD
healthcheck:
test: ['CMD-SHELL', 'clickhouse-client --query "SELECT 1"']
interval: 10s
timeout: 5s
retries: 5
ulimits:
nofile:
soft: 262144
hard: 262144
ports:
- 8999:9000
- 8122:8123
op-ch-migrator:
image: clickhouse/clickhouse-server:23.3.7.5-alpine
depends_on:
- op-ch
volumes:
- ../packages/db/clickhouse_init.sql:/migrations/clickhouse_init.sql
environment:
- CLICKHOUSE_DB
- CLICKHOUSE_USER
- CLICKHOUSE_PASSWORD
entrypoint: /bin/sh -c
command: >
"
echo 'Waiting for ClickHouse to start...';
while ! clickhouse-client --host op-ch --user=$CLICKHOUSE_USER --password=$CLICKHOUSE_PASSWORD --query 'SELECT 1;' 2>/dev/null; do
echo 'ClickHouse is unavailable - sleeping 1s...';
sleep 1;
done;
echo 'ClickHouse started. Running migrations...';
clickhouse-client --host op-ch --database=$CLICKHOUSE_DB --user=$CLICKHOUSE_USER --password=$CLICKHOUSE_PASSWORD --queries-file /migrations/clickhouse_init.sql;
"
op-api:
image: lindesvard/openpanel-api:latest
restart: always
command: sh -c "sleep 10 && pnpm -r run migrate:deploy && pnpm start"
depends_on:
- op-db
- op-ch
- op-kv
- op-geo
env_file:
- .env
op-dashboard:
image: lindesvard/openpanel-dashboard:latest
restart: always
depends_on:
- op-db
- op-ch
- op-kv
env_file:
- .env
op-worker:
image: lindesvard/openpanel-worker:latest
restart: always
depends_on:
- op-db
- op-ch
- op-kv
env_file:
- .env
deploy:
mode: replicated
replicas: $OP_WORKER_REPLICAS
volumes:
op-db-data:
driver: local
op-kv-data:
driver: local
op-ch-data:
driver: local
op-ch-logs:
driver: local
op-proxy-data:
driver: local
op-proxy-config:
driver: local

3
self-hosting/logs Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker compose logs -f

22
self-hosting/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "@openpanel/self-hosting",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/inquirer": "^9.0.7",
"@types/js-yaml": "^4.0.9",
"bcrypt": "^5.1.1",
"inquirer": "^9.3.1",
"jiti": "^1.21.6",
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2"
}
}

716
self-hosting/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,716 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@types/inquirer':
specifier: ^9.0.7
version: 9.0.7
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
bcrypt:
specifier: ^5.1.1
version: 5.1.1
inquirer:
specifier: ^9.3.1
version: 9.3.1
jiti:
specifier: ^1.21.6
version: 1.21.6
js-yaml:
specifier: ^4.1.0
version: 4.1.0
devDependencies:
'@types/bcrypt':
specifier: ^5.0.2
version: 5.0.2
packages:
/@inquirer/figures@1.0.3:
resolution: {integrity: sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==}
engines: {node: '>=18'}
dev: false
/@mapbox/node-pre-gyp@1.0.11:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true
dependencies:
detect-libc: 2.0.3
https-proxy-agent: 5.0.1
make-dir: 3.1.0
node-fetch: 2.7.0
nopt: 5.0.0
npmlog: 5.0.1
rimraf: 3.0.2
semver: 7.6.3
tar: 6.2.1
transitivePeerDependencies:
- encoding
- supports-color
dev: false
/@types/bcrypt@5.0.2:
resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
dependencies:
'@types/node': 20.14.9
dev: true
/@types/inquirer@9.0.7:
resolution: {integrity: sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==}
dependencies:
'@types/through': 0.0.33
rxjs: 7.8.1
dev: false
/@types/js-yaml@4.0.9:
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
dev: false
/@types/node@20.14.9:
resolution: {integrity: sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==}
dependencies:
undici-types: 5.26.5
/@types/through@0.0.33:
resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==}
dependencies:
'@types/node': 20.14.9
dev: false
/abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: false
/agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
dependencies:
debug: 4.3.6
transitivePeerDependencies:
- supports-color
dev: false
/ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
dependencies:
type-fest: 0.21.3
dev: false
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
dev: false
/ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
dependencies:
color-convert: 2.0.1
dev: false
/aproba@2.0.0:
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
dev: false
/are-we-there-yet@2.0.0:
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
dependencies:
delegates: 1.0.0
readable-stream: 3.6.2
dev: false
/argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: false
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: false
/base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: false
/bcrypt@5.1.1:
resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==}
engines: {node: '>= 10.0.0'}
requiresBuild: true
dependencies:
'@mapbox/node-pre-gyp': 1.0.11
node-addon-api: 5.1.0
transitivePeerDependencies:
- encoding
- supports-color
dev: false
/bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
dev: false
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
dev: false
/buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
dev: false
/chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
dev: false
/chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: false
/chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
dev: false
/cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
dependencies:
restore-cursor: 3.1.0
dev: false
/cli-spinners@2.9.2:
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
engines: {node: '>=6'}
dev: false
/cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
dev: false
/clone@1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
dev: false
/color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
dependencies:
color-name: 1.1.4
dev: false
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: false
/color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
dev: false
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: false
/console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
dev: false
/debug@4.3.6:
resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
dev: false
/defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
dependencies:
clone: 1.0.4
dev: false
/delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
dev: false
/detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
dev: false
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: false
/external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
dependencies:
chardet: 0.7.0
iconv-lite: 0.4.24
tmp: 0.0.33
dev: false
/fs-minipass@2.1.0:
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
engines: {node: '>= 8'}
dependencies:
minipass: 3.3.6
dev: false
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: false
/gauge@3.0.2:
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
dependencies:
aproba: 2.0.0
color-support: 1.1.3
console-control-strings: 1.1.0
has-unicode: 2.0.1
object-assign: 4.1.1
signal-exit: 3.0.7
string-width: 4.2.3
strip-ansi: 6.0.1
wide-align: 1.1.5
dev: false
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
dev: false
/has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
dev: false
/has-unicode@2.0.1:
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
dev: false
/https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
dependencies:
agent-base: 6.0.2
debug: 4.3.6
transitivePeerDependencies:
- supports-color
dev: false
/iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
dependencies:
once: 1.4.0
wrappy: 1.0.2
dev: false
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: false
/inquirer@9.3.1:
resolution: {integrity: sha512-A5IdVr1I04XqPlwrGgTJMKmzRg5ropqNpSeqo0vj1ZmluSCNSFaPZz4eazdPrhVcZfej7fCEYvD2NYa1KjkTJA==}
engines: {node: '>=18'}
dependencies:
'@inquirer/figures': 1.0.3
ansi-escapes: 4.3.2
cli-width: 4.1.0
external-editor: 3.1.0
mute-stream: 1.0.0
ora: 5.4.1
picocolors: 1.0.1
run-async: 3.0.0
rxjs: 7.8.1
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
dev: false
/is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
dev: false
/is-interactive@1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
dev: false
/is-unicode-supported@0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
dev: false
/jiti@1.21.6:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
dev: false
/js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
dependencies:
argparse: 2.0.1
dev: false
/log-symbols@4.1.0:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
dependencies:
chalk: 4.1.2
is-unicode-supported: 0.1.0
dev: false
/make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
dependencies:
semver: 6.3.1
dev: false
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
dev: false
/minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
brace-expansion: 1.1.11
dev: false
/minipass@3.3.6:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
dependencies:
yallist: 4.0.0
dev: false
/minipass@5.0.0:
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
engines: {node: '>=8'}
dev: false
/minizlib@2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
dependencies:
minipass: 3.3.6
yallist: 4.0.0
dev: false
/mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
hasBin: true
dev: false
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: false
/mute-stream@1.0.0:
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dev: false
/node-addon-api@5.1.0:
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
dev: false
/node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: false
/nopt@5.0.0:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
engines: {node: '>=6'}
hasBin: true
dependencies:
abbrev: 1.1.1
dev: false
/npmlog@5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
deprecated: This package is no longer supported.
dependencies:
are-we-there-yet: 2.0.0
console-control-strings: 1.1.0
gauge: 3.0.2
set-blocking: 2.0.0
dev: false
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
dev: false
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
dev: false
/onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
dependencies:
mimic-fn: 2.1.0
dev: false
/ora@5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
dependencies:
bl: 4.1.0
chalk: 4.1.2
cli-cursor: 3.1.0
cli-spinners: 2.9.2
is-interactive: 1.0.0
is-unicode-supported: 0.1.0
log-symbols: 4.1.0
strip-ansi: 6.0.1
wcwidth: 1.0.1
dev: false
/os-tmpdir@1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
dev: false
/path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
dev: false
/picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
dev: false
/readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
dev: false
/restore-cursor@3.1.0:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
dev: false
/rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
dependencies:
glob: 7.2.3
dev: false
/run-async@3.0.0:
resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==}
engines: {node: '>=0.12.0'}
dev: false
/rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
dependencies:
tslib: 2.6.3
dev: false
/safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
dev: false
/semver@7.6.3:
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
engines: {node: '>=10'}
hasBin: true
dev: false
/set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: false
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: false
/string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
dev: false
/string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies:
safe-buffer: 5.2.1
dev: false
/strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
dependencies:
ansi-regex: 5.0.1
dev: false
/supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
dependencies:
has-flag: 4.0.0
dev: false
/tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
dependencies:
chownr: 2.0.0
fs-minipass: 2.1.0
minipass: 5.0.0
minizlib: 2.1.2
mkdirp: 1.0.4
yallist: 4.0.0
dev: false
/tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
dependencies:
os-tmpdir: 1.0.2
dev: false
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false
/tslib@2.6.3:
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
dev: false
/type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
dev: false
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false
/wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
dependencies:
defaults: 1.0.4
dev: false
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false
/whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
dev: false
/wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
dependencies:
string-width: 4.2.3
dev: false
/wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
dev: false
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: false
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: false

476
self-hosting/quiz.ts Normal file
View File

@@ -0,0 +1,476 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import bcrypt from 'bcrypt';
import inquirer from 'inquirer';
import yaml from 'js-yaml';
function generatePassword(length: number) {
const charset =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let password = '';
for (let i = 0, n = charset.length; i < length; ++i) {
password += charset.charAt(Math.floor(Math.random() * n));
}
return password;
}
function writeCaddyfile(domainName: string, basicAuthPassword: string) {
const caddyfileTemplatePath = path.resolve(
__dirname,
'caddy',
'Caddyfile.template'
);
const caddyfilePath = path.resolve(__dirname, 'caddy', 'Caddyfile');
fs.writeFileSync(
caddyfilePath,
fs
.readFileSync(caddyfileTemplatePath, 'utf-8')
.replaceAll('$DOMAIN_NAME', domainName.replace(/https?:\/\//, ''))
.replaceAll(
'$BASIC_AUTH_PASSWORD',
bcrypt.hashSync(basicAuthPassword, 10)
)
.replaceAll(
'$SSL_CONFIG',
domainName.includes('localhost:443') ? '\n\ttls internal' : ''
)
);
}
export interface DockerComposeFile {
version: string;
services: Record<
string,
{
image: string;
restart: string;
ports: string[];
volumes: string[];
depends_on: string[];
}
>;
volumes?: Record<string, unknown>;
}
const stripTrailingSlash = (str: string) =>
str.endsWith('/') ? str.slice(0, -1) : str;
function searchAndReplaceDockerCompose(replacements: [string, string][]) {
const dockerComposePath = path.resolve(__dirname, 'docker-compose.yml');
const dockerComposeContent = fs.readFileSync(dockerComposePath, 'utf-8');
const dockerComposeReplaced = replacements.reduce(
(acc, [search, replace]) => acc.replaceAll(search, replace),
dockerComposeContent
);
fs.writeFileSync(dockerComposePath, dockerComposeReplaced);
}
function removeServiceFromDockerCompose(serviceName: string) {
const dockerComposePath = path.resolve(__dirname, 'docker-compose.yml');
const dockerComposeContent = fs.readFileSync(dockerComposePath, 'utf-8');
// Parse the YAML file
const dockerCompose = yaml.load(dockerComposeContent) as DockerComposeFile;
// Remove the service
if (dockerCompose.services && dockerCompose.services[serviceName]) {
delete dockerCompose.services[serviceName];
console.log(`Service '${serviceName}' has been removed.`);
} else {
console.log(`Service '${serviceName}' not found.`);
// return;
}
// filter depends_on
Object.keys(dockerCompose.services).forEach((service) => {
if (dockerCompose.services[service]?.depends_on) {
// @ts-expect-error
dockerCompose.services[service].depends_on = dockerCompose.services[
service
].depends_on.filter((dep) => dep !== serviceName);
}
});
// filter volumes
Object.keys(dockerCompose.volumes ?? {}).forEach((volume) => {
if (dockerCompose.volumes && volume.startsWith(serviceName)) {
delete dockerCompose.volumes[volume];
}
});
if (Object.keys(dockerCompose.volumes ?? {}).length === 0) {
delete dockerCompose.volumes;
}
// Convert the object back to YAML
const newYaml = yaml.dump(dockerCompose, {
lineWidth: -1,
});
fs.writeFileSync(dockerComposePath, newYaml);
}
function writeEnvFile(envs: {
POSTGRES_PASSWORD: string | undefined;
REDIS_PASSWORD: string | undefined;
CLICKHOUSE_URL: string;
CLICKHOUSE_DB: string;
CLICKHOUSE_USER: string;
CLICKHOUSE_PASSWORD: string;
REDIS_URL: string;
DATABASE_URL: string;
DOMAIN_NAME: string;
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: string;
CLERK_SECRET_KEY: string;
CLERK_SIGNING_SECRET: string;
}) {
const envTemplatePath = path.resolve(__dirname, '.env.template');
const envPath = path.resolve(__dirname, '.env');
const envTemplate = fs.readFileSync(envTemplatePath, 'utf-8');
let newEnvFile = envTemplate
.replace('$CLICKHOUSE_URL', envs.CLICKHOUSE_URL)
.replace('$CLICKHOUSE_DB', envs.CLICKHOUSE_DB)
.replace('$CLICKHOUSE_USER', envs.CLICKHOUSE_USER)
.replace('$CLICKHOUSE_PASSWORD', envs.CLICKHOUSE_PASSWORD)
.replace('$REDIS_URL', envs.REDIS_URL)
.replace('$DATABASE_URL', envs.DATABASE_URL)
.replace('$DATABASE_URL_DIRECT', envs.DATABASE_URL)
.replace('$NEXT_PUBLIC_DASHBOARD_URL', stripTrailingSlash(envs.DOMAIN_NAME))
.replace(
'$NEXT_PUBLIC_API_URL',
`${stripTrailingSlash(envs.DOMAIN_NAME)}/api`
)
.replace(
'$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY',
envs.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
)
.replace('$CLERK_SECRET_KEY', envs.CLERK_SECRET_KEY)
.replace('$CLERK_SIGNING_SECRET', envs.CLERK_SIGNING_SECRET);
if (envs.POSTGRES_PASSWORD) {
newEnvFile += `\nPOSTGRES_PASSWORD=${envs.POSTGRES_PASSWORD}`;
}
fs.writeFileSync(
envPath,
newEnvFile
.split('\n')
.filter((line) => {
return !line.includes('=""');
})
.join('\n')
);
}
async function initiateOnboarding() {
const T = ' ';
const message = [
'',
'DISCLAIMER: This script is provided as-is and without warranty. Use at your own risk.',
'',
'',
'WORTH MENTIONING: This is an early version of the script and it may not cover all scenarios.',
' We recommend using our cloud service for production workloads until we release a stable version of self-hosting.',
'',
'',
"With that said let's get started! 🤠",
'',
`Hey and welcome to Openpanel's self-hosting setup! 🚀\n`,
`Before you continue, please make sure you have the following:`,
`${T}1. Docker and Docker Compose installed on your machine.`,
`${T}2. A domain name that you can use for this setup and point it to this machine's ip`,
`${T}3. A Clerk.com account`,
`${T}${T}- If you don't have one, you can create one at https://clerk.dev`,
`${T}${T}- We'll need NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, CLERK_SIGNING_SECRET`,
`${T}${T}- Create a webhook pointing to https://your_domain/api/webhook/clerk\n`,
'For more information you can read our article on self-hosting at https://docs.openpanel.dev/docs/self-hosting\n',
];
console.log(
'******************************************************************************\n'
);
console.log(message.join('\n'));
console.log(
'\n******************************************************************************'
);
// Domain name
const domainNameResponse = await inquirer.prompt([
{
type: 'input',
name: 'domainName',
message: "What's the domain name you want to use?",
default: process.env.DEBUG ? 'http://localhost' : undefined,
prefix: '🌐',
validate: (value) => {
if (value.startsWith('http://') || value.startsWith('https://')) {
return true;
}
return 'Please enter a valid domain name. Should start with "http://" or "https://"';
},
},
]);
// Dependencies
const dependenciesResponse = await inquirer.prompt([
{
type: 'checkbox',
name: 'dependencies',
message: 'Which of these dependencies will you need us to install?',
choices: ['Clickhouse', 'Redis', 'Postgres'],
default: ['Clickhouse', 'Redis', 'Postgres'],
prefix: '📦',
},
]);
let envs: Record<string, string> = {};
if (!dependenciesResponse.dependencies.includes('Clickhouse')) {
const clickhouseResponse = await inquirer.prompt([
{
type: 'input',
name: 'CLICKHOUSE_URL',
message: 'Enter your ClickHouse URL:',
default: process.env.DEBUG ? 'http://clickhouse:8123' : undefined,
},
{
type: 'input',
name: 'CLICKHOUSE_DB',
message: 'Enter your ClickHouse DB name:',
default: process.env.DEBUG ? 'db_openpanel' : undefined,
},
{
type: 'input',
name: 'CLICKHOUSE_USER',
message: 'Enter your ClickHouse user name:',
default: process.env.DEBUG ? 'user_openpanel' : undefined,
},
{
type: 'input',
name: 'CLICKHOUSE_PASSWORD',
message: 'Enter your ClickHouse password:',
default: process.env.DEBUG ? 'ch_password' : undefined,
},
]);
envs = {
...envs,
...clickhouseResponse,
};
}
if (!dependenciesResponse.dependencies.includes('Redis')) {
const redisResponse = await inquirer.prompt([
{
type: 'input',
name: 'REDIS_URL',
message: 'Enter your Redis URL:',
default: process.env.DEBUG ? 'redis://redis:6379' : undefined,
},
]);
envs = {
...envs,
...redisResponse,
};
}
if (!dependenciesResponse.dependencies.includes('Postgres')) {
const dbResponse = await inquirer.prompt([
{
type: 'input',
name: 'DATABASE_URL',
message: 'Enter your Database URL:',
default: process.env.DEBUG
? 'postgresql://postgres:postgres@postgres:5432/postgres?schema=public'
: undefined,
},
]);
envs = {
...envs,
...dbResponse,
};
}
// Proxy
const proxyResponse = await inquirer.prompt([
{
type: 'list',
name: 'proxy',
message:
'Do you already have a web service setup or would you like us to install Caddy with SSL?',
choices: ['Install Caddy with SSL', 'Bring my own'],
},
]);
// Clerk
const clerkResponse = await inquirer.prompt([
{
type: 'input',
name: 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY',
message: 'Enter your Clerk Publishable Key:',
default: process.env.DEBUG ? 'pk_test_1234567890' : undefined,
validate: (value) => {
if (value.startsWith('pk_live_') || value.startsWith('pk_test_')) {
return true;
}
return 'Please enter a valid Clerk Publishable Key. Should start with "pk_live_" or "pk_test_"';
},
},
{
type: 'input',
name: 'CLERK_SECRET_KEY',
message: 'Enter your Clerk Secret Key:',
default: process.env.DEBUG ? 'sk_test_1234567890' : undefined,
validate: (value) => {
if (value.startsWith('sk_live_') || value.startsWith('sk_test_')) {
return true;
}
return 'Please enter a valid Clerk Secret Key. Should start with "sk_live_" or "sk_test_"';
},
},
{
type: 'input',
name: 'CLERK_SIGNING_SECRET',
message: 'Enter your Clerk Signing Secret:',
default: process.env.DEBUG ? 'whsec_1234567890' : undefined,
validate: (value) => {
if (value.startsWith('whsec_')) {
return true;
}
return 'Please enter a valid Clerk Signing Secret. Should start with "whsec_"';
},
},
]);
// OS
const cpus = await inquirer.prompt([
{
type: 'input',
name: 'CPUS',
default: os.cpus().length,
message: 'How many CPUs do you have?',
validate: (value) => {
const parsed = parseInt(value, 10);
if (Number.isNaN(parsed)) {
return 'Please enter a valid number';
}
if (parsed < 1) {
return 'Please enter a number greater than 0';
}
return true;
},
},
]);
const basicAuth = await inquirer.prompt<{
password: string;
}>([
{
type: 'input',
name: 'password',
default: generatePassword(12),
message: 'Give a password for basic auth',
validate: (value) => {
if (!value) {
return 'Please enter a valid password';
}
if (value.length < 5) {
return 'Password should be atleast 5 characters';
}
return true;
},
},
]);
console.log('');
console.log('Creating .env file...\n');
const POSTGRES_PASSWORD = generatePassword(20);
const REDIS_PASSWORD = generatePassword(20);
writeEnvFile({
POSTGRES_PASSWORD: envs.DATABASE_URL ? undefined : POSTGRES_PASSWORD,
REDIS_PASSWORD: envs.REDIS_URL ? undefined : REDIS_PASSWORD,
CLICKHOUSE_URL: envs.CLICKHOUSE_URL || 'http://op-ch:8123',
CLICKHOUSE_DB: envs.CLICKHOUSE_DB || 'openpanel',
CLICKHOUSE_USER: envs.CLICKHOUSE_USER || 'openpanel',
CLICKHOUSE_PASSWORD: envs.CLICKHOUSE_PASSWORD || generatePassword(20),
REDIS_URL: envs.REDIS_URL || 'redis://op-kv:6379',
DATABASE_URL:
envs.DATABASE_URL ||
`postgresql://postgres:${POSTGRES_PASSWORD}@op-db:5432/postgres?schema=public`,
DOMAIN_NAME: domainNameResponse.domainName,
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
clerkResponse.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || '',
CLERK_SECRET_KEY: clerkResponse.CLERK_SECRET_KEY || '',
CLERK_SIGNING_SECRET: clerkResponse.CLERK_SIGNING_SECRET || '',
});
console.log('Updating docker-compose.yml file...\n');
fs.copyFileSync(
path.resolve(__dirname, 'docker-compose.template.yml'),
path.resolve(__dirname, 'docker-compose.yml')
);
if (envs.CLICKHOUSE_URL) {
removeServiceFromDockerCompose('op-ch');
removeServiceFromDockerCompose('op-ch-migrator');
}
if (envs.REDIS_URL) {
removeServiceFromDockerCompose('op-kv');
}
if (envs.DATABASE_URL) {
removeServiceFromDockerCompose('op-db');
}
if (proxyResponse.proxy === 'Bring my own') {
removeServiceFromDockerCompose('op-proxy');
} else {
writeCaddyfile(domainNameResponse.domainName, basicAuth.password);
}
searchAndReplaceDockerCompose([['$OP_WORKER_REPLICAS', cpus.CPUS]]);
console.log(
[
'======================================================================',
'Here are some good things to know before you continue:',
'',
`1. Make sure that your webhook is pointing at ${domainNameResponse.domainName}/api/webhook/clerk`,
'',
'2. Commands:',
'\t- ./start (example: ./start)',
'\t- ./stop (example: ./stop)',
'\t- ./logs (example: ./logs)',
'\t- ./rebuild (example: ./rebuild op-dashboard)',
'',
'3. Danger zone!',
'\t- ./danger_wipe_everything (example: ./danger_wipe_everything)',
'',
'4. More about self-hosting: https://docs.openpanel.dev/docs/self-hosting',
'======================================================================',
'',
`Start OpenPanel with "./start" inside the self-hosting directory`,
'',
'',
].join('\n')
);
}
initiateOnboarding();

6
self-hosting/rebuild Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
NAME=$1
docker compose build $NAME
docker compose up -d --no-deps --force-recreate $NAME

96
self-hosting/setup Executable file
View File

@@ -0,0 +1,96 @@
#!/bin/bash
NODE_VERSION=20.15.0
# Function to install Node.js
install_nvm_and_node() {
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
source ~/.bashrc
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
nvm install $NODE_VERSION
nvm use $NODE_VERSION
}
# Function to install pnpm
install_pnpm() {
echo "Installing pnpm..."
npm install -g pnpm
}
# Function to install Docker
install_docker() {
echo "Installing Docker..."
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
# Install Docker packages:
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Add current user to docker group
sudo usermod -aG docker $USER
echo "Docker installed successfully. You may need to log out and back in for group changes to take effect."
}
# Check if Node.js is installed
if ! command -v node >/dev/null 2>&1; then
echo "********************************************************************************"
echo "********************************************************************************"
echo "Do you wish to automatically install Node.js version $NODE_VERSION using NVM? (yes/no)"
echo "********************************************************************************"
echo "********************************************************************************"
read user_choice
case $user_choice in
[Yy]* )
install_nvm_and_node;;
[Nn]* )
echo "Please install Node.js version $NODE_VERSION by yourself as per your preference. Exiting script."
exit 1;;
* )
echo "Invalid input. Please answer yes or no."
exit 1;;
esac
fi
# Check if Docker is installed
if ! command -v docker >/dev/null 2>&1; then
echo "********************************************************************************"
echo "********************************************************************************"
echo "Docker is not installed. Do you wish to install Docker? (yes/no)"
echo "********************************************************************************"
echo "********************************************************************************"
read docker_choice
case $docker_choice in
[Yy]* )
install_docker;;
[Nn]* )
echo "Skipping Docker installation.";;
* )
echo "Invalid input. Skipping Docker installation.";;
esac
else
echo "Docker is already installed."
fi
# Check if pnpm is installed
if ! command -v pnpm >/dev/null 2>&1; then
install_pnpm
fi
pnpm --ignore-workspace install
./node_modules/.bin/jiti quiz.ts

3
self-hosting/start Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker compose up -d

3
self-hosting/stop Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker compose down

View File

@@ -0,0 +1,28 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"exclude": ["node_modules", "build", "dist"],
"include": ["."]
}

54
sh/docker-build Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
APP=$1
VERSION=$2
if [ -z "$APP" ]; then
echo "Please provide an app name as an argument."
echo "Usage: $0 <app_name> <version>"
exit 1
fi
# Check if version is provided
if [ -z "$VERSION" ]; then
echo "Please provide a version number as an argument."
echo "Usage: $0 $APP <version>"
exit 1
fi
# Ensure Docker Buildx is available and set up a builder
docker buildx create --use --name multi-arch-builder || true
# Function to build a multi-architecture image
build_image() {
local app=$1
local image_name="lindesvard/openpanel-$app"
echo "Building multi-architecture image for $image_name:$VERSION"
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t "$image_name:$VERSION" \
-t "$image_name:latest" \
--build-arg DATABASE_URL="postgresql://p@p:5432/p" \
-f "apps/$app/Dockerfile" \
--push \
.
if [ $? -ne 0 ]; then
echo "Failed to build $image_name:$VERSION"
exit 1
fi
echo "Successfully built and pushed multi-architecture image for $image_name:$VERSION"
}
if [ "$APP" == "all" ]; then
build_image "dashboard"
build_image "worker"
build_image "api"
echo "All multi-architecture images have been built and pushed successfully."
else
build_image $APP
echo "Multi-architecture image for $APP has been built and pushed successfully."
fi

49
sh/docker-publish Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
APP=$1
VERSION=$2
if [ -z "$APP" ]; then
echo "Please provide an app name as an argument."
echo "Usage: $0 <app_name> <version>"
exit 1
fi
# Check if version is provided
if [ -z "$VERSION" ]; then
echo "Please provide a version number as an argument."
echo "Usage: $0 $APP <version>"
exit 1
fi
# Function to push a multi-architecture image
push_image() {
local app=$1
local image_name="lindesvard/openpanel-$app"
echo "Pushing multi-architecture image for $image_name:$VERSION"
# Push the versioned tag
docker buildx imagetools create -t "$image_name:$VERSION" "$image_name:$VERSION"
# Push the latest tag
docker buildx imagetools create -t "$image_name:latest" "$image_name:$VERSION"
if [ $? -ne 0 ]; then
echo "Failed to push $image_name:$VERSION"
exit 1
fi
echo "Successfully pushed multi-architecture image for $image_name:$VERSION and latest"
}
# Push each image
if [ "$APP" == "all" ]; then
push_image "dashboard"
push_image "worker"
push_image "api"
echo "All multi-architecture images have been pushed successfully."
else
push_image $APP
echo "Multi-architecture image for $APP has been pushed successfully."
fi