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
logs logs
!self-hosting/logs
_.log _.log
npm-debug.log_ npm-debug.log_
yarn-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 - tRPC - will probably migrate this to server actions
- Clerk - for authentication - 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 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 ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL ENV DATABASE_URL=$DATABASE_URL
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" 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 WORKDIR /app
COPY package.json package.json # Workspace
COPY pnpm-lock.yaml pnpm-lock.yaml COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY pnpm-workspace.yaml pnpm-workspace.yaml # Apps
COPY apps/api/package.json apps/api/package.json COPY apps/api/package.json ./apps/api/
COPY packages/db/package.json packages/db/package.json # Packages
COPY packages/redis/package.json packages/redis/package.json COPY packages/db/package.json packages/db/
COPY packages/trpc/package.json packages/trpc/package.json COPY packages/trpc/package.json packages/trpc/
COPY packages/queue/package.json packages/queue/package.json COPY packages/queue/package.json packages/queue/
COPY packages/common/package.json packages/common/package.json COPY packages/redis/package.json packages/redis/
COPY packages/constants/package.json packages/constants/package.json COPY packages/common/package.json packages/common/
COPY packages/validation/package.json packages/validation/package.json COPY packages/sdks/sdk/package.json packages/sdks/sdk/
COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json 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 COPY patches patches
# BUILD # BUILD
FROM base AS build FROM base AS build
WORKDIR /app/apps/api RUN apt-get update && apt-get install -y --no-install-recommends \
RUN pnpm install --frozen-lockfile python3 \
make \
g++
WORKDIR /app WORKDIR /app
COPY apps/api apps/api RUN pnpm install --frozen-lockfile && \
COPY packages packages pnpm store prune
COPY tooling tooling
RUN pnpm db:codegen
WORKDIR /app/apps/api COPY apps/api ./apps/api
RUN pnpm run build COPY packages ./packages
COPY tooling ./tooling
RUN pnpm db:codegen && \
pnpm --filter api run build
# PROD # PROD
FROM base AS prod FROM base AS prod
WORKDIR /app/apps/api RUN apt-get update && apt-get install -y --no-install-recommends \
RUN pnpm install --frozen-lockfile --prod 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 # FINAL
FROM base AS runner FROM base AS runner
COPY --from=build /app/package.json /app/package.json ENV NODE_ENV=production
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 # Apps
COPY --from=build /app/apps/api /app/apps/api COPY --from=build /app/apps/api ./apps/api
# Apps node_modules
COPY --from=prod /app/apps/api/node_modules /app/apps/api/node_modules
# Packages # Packages
COPY --from=build /app/packages/db /app/packages/db COPY --from=build /app/packages/db ./packages/db
COPY --from=build /app/packages/redis /app/packages/redis COPY --from=build /app/packages/trpc ./packages/trpc
COPY --from=build /app/packages/trpc /app/packages/trpc COPY --from=build /app/packages/queue ./packages/queue
COPY --from=build /app/packages/queue /app/packages/queue COPY --from=build /app/packages/redis ./packages/redis
COPY --from=build /app/packages/common /app/packages/common COPY --from=build /app/packages/common ./packages/common
COPY --from=build /app/packages/sdks/sdk ./packages/sdks/sdk
# Packages node_modules COPY --from=build /app/packages/constants ./packages/constants
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules COPY --from=build /app/packages/validation ./packages/validation
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
RUN pnpm db:codegen RUN pnpm db:codegen

View File

@@ -5,6 +5,7 @@ import icoToPng from 'ico-to-png';
import sharp from 'sharp'; import sharp from 'sharp';
import { createHash } from '@openpanel/common'; import { createHash } from '@openpanel/common';
import { ch, TABLE_NAMES } from '@openpanel/db';
import { getRedisCache } from '@openpanel/redis'; import { getRedisCache } from '@openpanel/redis';
interface GetFaviconParams { interface GetFaviconParams {
@@ -110,3 +111,37 @@ export async function clearFavicons(
} }
return reply.status(404).send('OK'); 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') { if (payload.type === 'user.created') {
const email = payload.data.email_addresses[0]?.email_address; const email = payload.data.email_addresses[0]?.email_address;
const emails = payload.data.email_addresses.map((e) => e.email_address);
if (!email) { if (!email) {
return Response.json( return Response.json(
@@ -63,14 +64,16 @@ export async function clerkWebhook(
const memberships = await db.member.findMany({ const memberships = await db.member.findMany({
where: { where: {
email, email: {
in: emails,
},
userId: null, userId: null,
}, },
}); });
for (const membership of memberships) { for (const membership of memberships) {
const access = pathOr<string[]>([], ['meta', 'access'], membership); const access = pathOr<string[]>([], ['meta', 'access'], membership);
db.$transaction([ await db.$transaction([
// Update the member to link it to the user // Update the member to link it to the user
// This will remove the item from invitations // This will remove the item from invitations
db.member.update({ db.member.update({
@@ -123,7 +126,6 @@ export async function clerkWebhook(
deletedAt: new Date(), deletedAt: new Date(),
firstName: null, firstName: null,
lastName: null, lastName: null,
email: `deleted+${payload.data.id}@openpanel.dev`,
}, },
}), }),
db.projectAccess.deleteMany({ db.projectAccess.deleteMany({

View File

@@ -2,6 +2,12 @@ import * as controller from '@/controllers/misc.controller';
import type { FastifyPluginCallback } from 'fastify'; import type { FastifyPluginCallback } from 'fastify';
const miscRouter: FastifyPluginCallback = (fastify, opts, done) => { const miscRouter: FastifyPluginCallback = (fastify, opts, done) => {
fastify.route({
method: 'POST',
url: '/ping',
handler: controller.ping,
});
fastify.route({ fastify.route({
method: 'GET', method: 'GET',
url: '/favicon', url: '/favicon',

View File

@@ -1,6 +1,6 @@
ARG NODE_VERSION=20.15.1 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" ENV SKIP_ENV_VALIDATION="1"
@@ -11,17 +11,15 @@ ARG ENABLE_INSTRUMENTATION_HOOK
ENV ENABLE_INSTRUMENTATION_HOOK=$ENABLE_INSTRUMENTATION_HOOK ENV ENABLE_INSTRUMENTATION_HOOK=$ENABLE_INSTRUMENTATION_HOOK
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" 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 \ RUN corepack enable
&& 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
WORKDIR /app 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/constants/package.json packages/constants/package.json
COPY packages/validation/package.json packages/validation/package.json COPY packages/validation/package.json packages/validation/package.json
COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json
COPY patches patches
# BUILD # BUILD
FROM base AS build FROM base AS build
WORKDIR /app/apps/dashboard WORKDIR /app
RUN pnpm install --frozen-lockfile --ignore-scripts RUN pnpm install --frozen-lockfile --ignore-scripts
WORKDIR /app
COPY apps/dashboard apps/dashboard COPY apps/dashboard apps/dashboard
COPY packages packages COPY packages packages
COPY tooling tooling COPY tooling tooling
@@ -57,48 +55,44 @@ WORKDIR /app/apps/dashboard
# Will be replaced on runtime # Will be replaced on runtime
ENV NEXT_PUBLIC_DASHBOARD_URL="__NEXT_PUBLIC_DASHBOARD_URL__" ENV NEXT_PUBLIC_DASHBOARD_URL="__NEXT_PUBLIC_DASHBOARD_URL__"
ENV NEXT_PUBLIC_API_URL="__NEXT_PUBLIC_API_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" 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 RUN pnpm run build
# PROD # RUNNER
FROM base AS prod
WORKDIR /app/apps/dashboard
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
# FINAL
FROM base AS runner FROM base AS runner
COPY --from=build /app/package.json /app/package.json WORKDIR /app
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
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 EXPOSE 3000
# CMD ["pnpm", "start"] ENV PORT=3000
COPY --from=build /app/apps/dashboard/entrypoint.sh /usr/bin/ ENV HOSTNAME=0.0.0.0
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh", "pnpm", "start"] ENTRYPOINT [ "/app/entrypoint.sh", "node", "/app/apps/dashboard/server.js"]

View File

@@ -1,32 +1,39 @@
#!/bin/bash #!/bin/sh
set -e set -e
echo "> Replace env variable placeholders with runtime values..." echo "> Replace env variable placeholders with runtime values..."
# Define an array of environment variables to check
variables_to_replace=( # Define environment variables to check (space-separated string)
"NEXT_PUBLIC_DASHBOARD_URL" variables_to_replace="NEXT_PUBLIC_DASHBOARD_URL NEXT_PUBLIC_API_URL NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
"NEXT_PUBLIC_API_URL"
"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
)
# Replace env variable placeholders with real values # Replace env variable placeholders with real values
for key in "${variables_to_replace[@]}"; do for key in $variables_to_replace; do
value=$(printenv $key) value=$(eval echo \$"$key")
if [ ! -z "$value" ]; then if [ -n "$value" ]; then
echo " - Searching for $key with value $value..." echo " - Searching for $key with value $value..."
# Use a custom placeholder for 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY' or use the actual key otherwise # Use a custom placeholder for 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY' or use the actual key otherwise
if [ "$key" = "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY" ]; then case "$key" in
placeholder="pk_test_eW9sby5jb20k" 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 else
placeholder="__${key}__" echo " - Skipping $key as it has no value set."
fi 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 done
echo "> Done!"
echo "> Running $@"
# Execute the container's main process (CMD in Dockerfile) # Execute the container's main process (CMD in Dockerfile)
exec "$@" exec "$@"

View File

@@ -9,6 +9,7 @@ await import('./src/env.mjs');
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
output: 'standalone',
webpack: (config, { isServer }) => { webpack: (config, { isServer }) => {
if (isServer) { if (isServer) {
config.plugins = [...config.plugins, new PrismaPlugin()]; config.plugins = [...config.plugins, new PrismaPlugin()];

View File

@@ -5,7 +5,7 @@
"scripts": { "scripts": {
"dev": "rm -rf .next && pnpm with-env next dev", "dev": "rm -rf .next && pnpm with-env next dev",
"testing": "pnpm dev", "testing": "pnpm dev",
"build": "next build", "build": "pnpm with-env next build",
"start": "next start", "start": "next start",
"lint": "eslint .", "lint": "eslint .",
"format": "prettier --check \"**/*.{tsx,mjs,ts,md,json}\"", "format": "prettier --check \"**/*.{tsx,mjs,ts,md,json}\"",

View File

@@ -16,9 +16,8 @@ const SkipOnboarding = () => {
res.refetch(); res.refetch();
}, [pathname]); }, [pathname]);
console.log(res.data);
if (!pathname.startsWith('/onboarding')) return null; if (!pathname.startsWith('/onboarding')) return null;
return ( return (
<button <button
onClick={() => { 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.', text: 'Are you sure you want to skip onboarding? Since you do not have any projects, you will be logged out.',
onConfirm() { onConfirm() {
auth.signOut(); 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 { export default function manifest(): MetadataRoute.Manifest {
return { return {
id: process.env.NEXT_PUBLIC_DASHBOARD_URL,
name: 'Openpanel.dev', name: 'Openpanel.dev',
short_name: 'Openpanel.dev', short_name: 'Openpanel.dev',
description: '', description: '',
@@ -9,12 +10,5 @@ export default function manifest(): MetadataRoute.Manifest {
display: 'standalone', display: 'standalone',
background_color: '#fff', background_color: '#fff',
theme_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> <strong>Usage</strong>
<p>Create a custom event called &quot;my_event&quot;.</p> <p>Create a custom event called &quot;my_event&quot;.</p>
<Syntax <Syntax
code={`curl 'https://api.openpanel.dev/track' \\ code={`curl '${process.env.NEXT_PUBLIC_API_URL}/track' \\
-H 'content-type: application/json' \\ -H 'content-type: application/json' \\
-H 'openpanel-client-id: ${clientId}' \\ -H 'openpanel-client-id: ${clientId}' \\
-H 'openpanel-client-secret: ${clientSecret}' \\ -H 'openpanel-client-secret: ${clientSecret}' \\

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 424 KiB

View File

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

View File

@@ -1,25 +1,6 @@
{ {
"index": "Get Started", "index": "Get Started",
"-- Frameworks": { "sdks": "SDKs",
"type": "separator", "migration": "Migrations",
"title": "Frameworks" "self-hosting": "Self-hosting"
},
"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"
} }

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" /> <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" title="HTML / Script"
href="/docs/script" href="/docs/sdks/script"
> >
{' '} {' '}
</Card> </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" /> <BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png" />
} }
title="React" title="React"
href="/docs/react" href="/docs/sdks/react"
> >
{' '} {' '}
</Card> </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" /> <BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png" />
} }
title="React-Native" title="React-Native"
href="/docs/react-native" href="/docs/sdks/react-native"
> >
{' '} {' '}
</Card> </Card>
@@ -94,11 +94,11 @@ Clears the current user identifier and ends the session.
icon={ icon={
<BrandLogo <BrandLogo
isDark 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" title="Next.js"
href="/docs/nextjs" href="/docs/sdks/nextjs"
> >
{' '} {' '}
</Card> </Card>
@@ -110,7 +110,7 @@ Clears the current user identifier and ends the session.
/> />
} }
title="Remix" title="Remix"
href="/docs/remix" href="/docs/sdks/remix"
> >
{' '} {' '}
</Card> </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" /> <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" title="Vue"
href="/docs/vue" href="/docs/sdks/vue"
> >
{' '} {' '}
</Card> </Card>
@@ -131,7 +131,7 @@ Clears the current user identifier and ends the session.
/> />
} }
title="Astro" title="Astro"
href="/docs/astro" href="/docs/sdks/astro"
> >
{' '} {' '}
</Card> </Card>
@@ -140,3 +140,28 @@ Clears the current user identifier and ends the session.
## Unofficial SDKs ## Unofficial SDKs
While not officially supported, the following community-contributed SDKs are available. 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 # 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 ## Installation

View File

@@ -121,7 +121,7 @@ export const op = new Openpanel({
op.track('my_event', { foo: 'bar' }); 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 ### 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.Tab>
</Tabs> </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 ## 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 # 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. 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" height="32"
width="32" width="32"
/> />
<strong style={{ marginLeft: '8px' }}>openpanel</strong> <strong style={{ marginLeft: '8px' }}>OpenPanel</strong>
</> </>
), ),
head: () => { 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 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 ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL ENV DATABASE_URL=$DATABASE_URL
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN corepack enable && \
apt-get update && \
RUN apt update \ apt-get install -y --no-install-recommends \
&& apt install -y curl \ ca-certificates \
&& curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \ openssl \
&& bash n $NODE_VERSION \ libssl3 && \
&& rm n \ apt-get clean && \
&& npm install -g n rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY package.json package.json # Workspace
COPY pnpm-lock.yaml pnpm-lock.yaml COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY pnpm-workspace.yaml pnpm-workspace.yaml # Apps
COPY apps/worker/package.json apps/worker/package.json COPY apps/worker/package.json ./apps/worker/
COPY packages/db/package.json packages/db/package.json # Packages
COPY packages/redis/package.json packages/redis/package.json COPY packages/db/package.json ./packages/db/
COPY packages/logger/package.json packages/logger/package.json COPY packages/redis/package.json ./packages/redis/
COPY packages/queue/package.json packages/queue/package.json COPY packages/queue/package.json ./packages/queue/
COPY packages/common/package.json packages/common/package.json COPY packages/logger/package.json ./packages/logger/
COPY packages/constants/package.json packages/constants/package.json COPY packages/common/package.json ./packages/common/
COPY packages/validation/package.json packages/validation/package.json COPY packages/constants/package.json ./packages/constants/
COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json # Patches
COPY patches patches COPY patches ./patches
# BUILD # BUILD
FROM base AS build FROM base AS build
WORKDIR /app/apps/worker WORKDIR /app
RUN pnpm install --frozen-lockfile --ignore-scripts RUN pnpm install --frozen-lockfile && \
pnpm store prune
WORKDIR /app COPY apps/worker ./apps/worker
COPY apps/worker apps/worker COPY packages ./packages
COPY packages packages COPY tooling ./tooling
COPY tooling tooling
RUN pnpm db:codegen
WORKDIR /app/apps/worker RUN pnpm db:codegen && \
RUN pnpm run build pnpm --filter worker run build
# PROD # PROD
FROM base AS prod FROM base AS prod
WORKDIR /app/apps/worker WORKDIR /app
RUN pnpm install --frozen-lockfile --prod --ignore-scripts COPY --from=build /app/package.json ./
COPY --from=build /app/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod && \
pnpm store prune
# FINAL # FINAL
FROM base AS runner FROM base AS runner
COPY --from=build /app/package.json /app/package.json WORKDIR /app
COPY --from=prod /app/node_modules /app/node_modules
COPY --from=build /app/package.json ./
COPY --from=prod /app/node_modules ./node_modules
# Apps # Apps
COPY --from=build /app/apps/worker /app/apps/worker COPY --from=build /app/apps/worker ./apps/worker
# Apps node_modules
COPY --from=prod /app/apps/worker/node_modules /app/apps/worker/node_modules
# Packages # Packages
COPY --from=build /app/packages/db /app/packages/db COPY --from=build /app/packages/db ./packages/db
COPY --from=build /app/packages/redis /app/packages/redis COPY --from=build /app/packages/redis ./packages/redis
COPY --from=build /app/packages/logger /app/packages/logger COPY --from=build /app/packages/logger ./packages/logger
COPY --from=build /app/packages/queue /app/packages/queue COPY --from=build /app/packages/queue ./packages/queue
COPY --from=build /app/packages/common /app/packages/common COPY --from=build /app/packages/common ./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
RUN pnpm db:codegen RUN pnpm db:codegen

View File

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

View File

@@ -5,6 +5,7 @@ import type { WorkerOptions } from 'bullmq';
import { Worker } from 'bullmq'; import { Worker } from 'bullmq';
import express from 'express'; import express from 'express';
import { createInitialSalts } from '@openpanel/db';
import { cronQueue, eventsQueue, sessionsQueue } from '@openpanel/queue'; import { cronQueue, eventsQueue, sessionsQueue } from '@openpanel/queue';
import { getRedisQueue } from '@openpanel/redis'; import { getRedisQueue } from '@openpanel/redis';
@@ -15,7 +16,7 @@ import { register } from './metrics';
const PORT = parseInt(process.env.WORKER_PORT || '3000', 10); const PORT = parseInt(process.env.WORKER_PORT || '3000', 10);
const serverAdapter = new ExpressAdapter(); const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath(process.env.SELF_HOSTED ? '/worker' : '/'); serverAdapter.setBasePath('/');
const app = express(); const app = express();
const workerOptions: WorkerOptions = { 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(); const repeatableJobs = await cronQueue.getRepeatableJobs();
console.log('Repeatable jobs:'); console.log('Repeatable jobs:');
console.log(repeatableJobs); console.log(repeatableJobs);
await createInitialSalts();
} }
start(); 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 { eventBuffer, profileBuffer } from '@openpanel/db';
import type { CronQueuePayload } from '@openpanel/queue'; import type { CronQueuePayload } from '@openpanel/queue';
import { ping } from './cron.ping';
import { salt } from './cron.salt'; import { salt } from './cron.salt';
export async function cronJob(job: Job<CronQueuePayload>) { export async function cronJob(job: Job<CronQueuePayload>) {
@@ -16,5 +17,8 @@ export async function cronJob(job: Job<CronQueuePayload>) {
case 'flushProfiles': { case 'flushProfiles': {
return await profileBuffer.flush(); return await profileBuffer.flush();
} }
case 'ping': {
return await ping();
}
} }
} }

View File

@@ -1,6 +1,17 @@
CREATE DATABASE IF NOT EXISTS openpanel; 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(), `id` UUID DEFAULT generateUUIDv4(),
`name` String, `name` String,
`sdk_name` String, `sdk_name` String,
@@ -80,7 +91,7 @@ SELECT
uniqState(profile_id) as profile_id, uniqState(profile_id) as profile_id,
project_id project_id
FROM FROM
events events_v2
GROUP BY GROUP BY
date, date,
project_id; project_id;

View File

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

View File

@@ -1,3 +1,5 @@
import { generateSalt } from '@openpanel/common';
import { db } from '../prisma-client'; import { db } from '../prisma-client';
export async function getCurrentSalt() { export async function getCurrentSalt() {
@@ -27,7 +29,7 @@ export async function getSalts() {
} }
if (!prev) { if (!prev) {
throw new Error('No previous salt found'); throw new Error('No salt found');
} }
return { return {
@@ -35,3 +37,44 @@ export async function getSalts() {
previous: prev.salt, 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'; type: 'flushProfiles';
payload: undefined; payload: undefined;
}; };
export type CronQueuePayloadPing = {
type: 'ping';
payload: undefined;
};
export type CronQueuePayload = export type CronQueuePayload =
| CronQueuePayloadSalt | CronQueuePayloadSalt
| CronQueuePayloadFlushEvents | CronQueuePayloadFlushEvents
| CronQueuePayloadFlushProfiles; | CronQueuePayloadFlushProfiles
| CronQueuePayloadPing;
export const eventsQueue = new Queue<EventsQueuePayload>('events', { export const eventsQueue = new Queue<EventsQueuePayload>('events', {
connection: getRedisQueue(), connection: getRedisQueue(),

View File

@@ -1,7 +1,7 @@
const api = { const api = {
logo: 'https://cdn-icons-png.flaticon.com/512/10169/10169724.png', logo: 'https://cdn-icons-png.flaticon.com/512/10169/10169724.png',
name: 'Rest API', name: 'Rest API',
href: 'https://docs.openpanel.dev/docs/api', href: 'https://docs.openpanel.dev/docs/sdks/api',
} as const; } as const;
export const frameworks = { 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', 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', 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', logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png',
name: 'React', 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', logo: 'https://static-00.iconduck.com/assets.00/nextjs-icon-512x512-y563b8iq.png',
name: 'Next.js', 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', logo: 'https://www.datocms-assets.com/205/1642515307-square-logo.svg',
name: 'Remix', 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', logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1024px-Vue.js_Logo_2.svg.png',
name: 'Vue', 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', logo: 'https://astro.build/assets/press/astro-icon-dark.png',
name: 'Astro', name: 'Astro',
href: 'https://docs.openpanel.dev/docs/astro', href: 'https://docs.openpanel.dev/docs/sdks/astro',
}, },
api, 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', logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png',
name: 'React-Native', name: 'React-Native',
href: 'https://docs.openpanel.dev/docs/react-native', href: 'https://docs.openpanel.dev/docs/sdks/react-native',
}, },
api, api,
], ],
@@ -50,12 +50,12 @@ export const frameworks = {
{ {
logo: 'https://static-00.iconduck.com/assets.00/node-js-icon-454x512-nztofx17.png', logo: 'https://static-00.iconduck.com/assets.00/node-js-icon-454x512-nztofx17.png',
name: 'Node', name: 'Node',
href: 'https://docs.openpanel.dev/docs/node', href: 'https://docs.openpanel.dev/docs/sdks/node',
}, },
{ {
logo: 'https://expressjs.com/images/favicon.png', logo: 'https://expressjs.com/images/favicon.png',
name: 'Express', 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', 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 specifier: ^0.1.5
version: 0.1.5 version: 0.1.5
'@bull-board/api': '@bull-board/api':
specifier: ^5.21.0 specifier: 5.21.0
version: 5.21.0(patch_hash=25udjn3ygs6h4rrgl46tnrqrn4)(@bull-board/ui@5.21.0) version: 5.21.0(patch_hash=25udjn3ygs6h4rrgl46tnrqrn4)(@bull-board/ui@5.21.0)
'@bull-board/express': '@bull-board/express':
specifier: ^5.21.0 specifier: 5.21.0
version: 5.21.0 version: 5.21.0
'@openpanel/common': '@openpanel/common':
specifier: workspace:* specifier: workspace:*
@@ -8194,6 +8194,7 @@ packages:
/are-we-there-yet@2.0.0: /are-we-there-yet@2.0.0:
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
engines: {node: '>=10'} engines: {node: '>=10'}
deprecated: This package is no longer supported.
dependencies: dependencies:
delegates: 1.0.0 delegates: 1.0.0
readable-stream: 3.6.2 readable-stream: 3.6.2
@@ -11674,6 +11675,7 @@ packages:
/gauge@3.0.2: /gauge@3.0.2:
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
deprecated: This package is no longer supported.
dependencies: dependencies:
aproba: 2.0.0 aproba: 2.0.0
color-support: 1.1.3 color-support: 1.1.3
@@ -12307,6 +12309,7 @@ packages:
/inflight@1.0.6: /inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 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: dependencies:
once: 1.4.0 once: 1.4.0
wrappy: 1.0.2 wrappy: 1.0.2
@@ -15036,6 +15039,7 @@ packages:
/npmlog@5.0.1: /npmlog@5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
deprecated: This package is no longer supported.
dependencies: dependencies:
are-we-there-yet: 2.0.0 are-we-there-yet: 2.0.0
console-control-strings: 1.1.0 console-control-strings: 1.1.0
@@ -16924,6 +16928,7 @@ packages:
/rimraf@3.0.2: /rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true hasBin: true
dependencies: dependencies:
glob: 7.2.3 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