feat: prepare supporter self-hosting

This commit is contained in:
Carl-Gerhard Lindesvärd
2025-10-22 09:36:53 +02:00
parent f958230a66
commit 9790ba8937
19 changed files with 2647 additions and 115 deletions

View File

@@ -23,6 +23,7 @@ jobs:
api: ${{ steps.filter.outputs.api }} api: ${{ steps.filter.outputs.api }}
worker: ${{ steps.filter.outputs.worker }} worker: ${{ steps.filter.outputs.worker }}
public: ${{ steps.filter.outputs.public }} public: ${{ steps.filter.outputs.public }}
dashboard: ${{ steps.filter.outputs.dashboard }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dorny/paths-filter@v2 - uses: dorny/paths-filter@v2
@@ -42,10 +43,14 @@ jobs:
- 'apps/public/**' - 'apps/public/**'
- 'packages/**' - 'packages/**'
- '.github/workflows/**' - '.github/workflows/**'
dashboard:
- 'apps/start/**'
- 'packages/**'
- '.github/workflows/**'
lint-and-test: lint-and-test:
needs: changes needs: changes
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.worker == 'true' || needs.changes.outputs.public == 'true' }} if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.worker == 'true' || needs.changes.outputs.public == 'true' || needs.changes.outputs.dashboard == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
redis: redis:
@@ -181,3 +186,46 @@ jobs:
ghcr.io/${{ env.repo_owner }}/worker:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }} ghcr.io/${{ env.repo_owner }}/worker:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }}
build-args: | build-args: |
DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy
build-and-push-dashboard:
permissions:
packages: write
needs: [changes, lint-and-test]
if: ${{ needs.changes.outputs.dashboard == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Generate tags
id: tags
run: |
# Sanitize branch name by replacing / with -
BRANCH_NAME=$(echo "${{ github.ref_name }}" | sed 's/\//-/g')
# Get first 4 characters of commit SHA
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-4)
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: apps/start/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
ghcr.io/${{ env.repo_owner }}/dashboard:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }}
build-args: |
NO_CLOUDFLARE=1

View File

@@ -10,7 +10,7 @@ export async function healthcheck(
) { ) {
try { try {
const redisRes = await getRedisCache().ping(); const redisRes = await getRedisCache().ping();
const dbRes = await db.project.findFirst(); const dbRes = await db.$executeRaw`SELECT 1`;
const chRes = await chQuery('SELECT 1'); const chRes = await chQuery('SELECT 1');
const status = redisRes && dbRes && chRes ? 200 : 503; const status = redisRes && dbRes && chRes ? 200 : 503;

View File

@@ -0,0 +1,134 @@
---
title: Supporter Access - Latest Docker Images
description: Get access to OpenPanel's latest private Docker images with bleeding-edge features built on every commit.
---
## Thank You for Your Support! 🙏
First and foremost, **thank you** for supporting OpenPanel! Your contribution means the world to us and helps keep this project alive, maintained, and constantly improving. Every dollar you contribute goes directly into development, infrastructure, and making OpenPanel better for everyone.
As a supporter, you get exclusive access to our private Docker images that are built with every commit, giving you the absolute latest features and fixes before they're publicly released.
## Why Latest Images Matter
### Bleeding-Edge Features
- **Instant access**: Get new features the moment they're merged
- **Early bug fixes**: Patches and fixes deployed immediately
- **Continuous improvements**: Performance enhancements and optimizations in real-time
- **Stay ahead**: Run the most advanced version of OpenPanel available
### Built on Every Commit
We maintain a continuous integration pipeline that builds new Docker images with every single commit to our repository. This means:
- Zero delay between development and deployment
- Production-ready images tested and validated automatically
- Access to features weeks or months before stable releases
## How It Works
Our private Docker images are hosted on GitHub's container registry and protected from public access. As a supporter, you get an API key that grants you access to our private Docker proxy at `docker.openpanel.dev`, which seamlessly pulls these images for you.
## Getting Started
### Step 1: Become a Supporter
Support starts at just **$20/month** and includes:
- Access to all private Docker images
- Priority support in our Discord community
- Direct impact on OpenPanel's development
- Our eternal gratitude ❤️
### Step 2: Get Your API Key
Once you become a supporter, you'll receive a unique API key that grants access to our Docker proxy.
### Step 3: Login to Docker Registry
On your server, authenticate with our Docker proxy using your API key:
```bash
echo "your_api_key" | docker login docker.openpanel.dev -u user --password-stdin
```
Replace `your_api_key` with the actual API key provided to you.
<Callout>
Make sure to keep your API key secure and never commit it to version control!
</Callout>
### Step 4: Update to Latest Images
We've created a convenient script to make updating your images effortless. Navigate to your self-hosting folder and run:
```bash
./get_latest_images apply
```
This script will:
- Check that you're authenticated with our Docker registry
- Fetch the latest Git tags from our repository
- Update your `docker-compose.yml` with the new image tags pointing to the latest builds
<Callout type="info">
The script will automatically check if you're logged in. If not, it will provide you with instructions to authenticate first.
</Callout>
You can also check what tags are available without applying them:
```bash
./get_latest_images # Show latest tags
./get_latest_images --list # List all available tags
```
### Step 5: Restart Your Services
After pulling the latest images, simply restart your services:
```bash
./restart
```
That's it! Your OpenPanel instance is now running the latest and greatest version.
## Quick Update Workflow
Once you're set up, updating to the latest version is as simple as:
```bash
cd /path/to/self-hosting
./get_latest_images apply
./restart
```
This entire process takes less than a minute, giving you instant access to new features and fixes.
The `get_latest_images` script will:
1. Verify you're logged into the Docker registry
2. Fetch the latest tags from GitHub
3. Update your `docker-compose.yml` with images pointing to the latest commit SHAs
4. Create a backup of your docker-compose file before making changes
## Important Notes
- **Stability**: While these images are tested, they may occasionally contain bugs that haven't been discovered yet. We recommend having a backup strategy.
- **Breaking changes**: We strive to maintain backwards compatibility, but occasionally breaking changes may occur. Always check the [changelog](/docs/self-hosting/changelog) before updating.
- **Support**: As a supporter, you have priority access to support. If you encounter any issues, reach out to us on Discord or via email.
## Need Help?
If you have any questions or run into issues:
- Join our [Discord community](https://go.openpanel.dev/discord) (supporters get a special role!)
- Email us at [hello@openpanel.dev](mailto:hello@openpanel.dev)
- Check our [GitHub repository](https://github.com/Openpanel-dev/openpanel)
---
## Your Impact
Every contribution helps us:
- Dedicate more time to development
- Maintain and improve infrastructure
- Provide better documentation and support
- Keep OpenPanel free and open-source for everyone
Thank you for being an essential part of OpenPanel's journey. We couldn't do this without supporters like you! 💙

140
apps/start/Dockerfile Normal file
View File

@@ -0,0 +1,140 @@
ARG NODE_VERSION=22.20.0
FROM node:${NODE_VERSION}-slim AS base
# FIX: Bad workaround (https://github.com/nodejs/corepack/issues/612)
ENV COREPACK_INTEGRITY_KEYS=0
RUN corepack enable && apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
openssl \
libssl3 \
curl \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*
ENV NITRO=1
ENV SELF_HOSTED=1
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
WORKDIR /app
# Workspace - Copy package.json files for caching
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
# Apps
COPY apps/start/package.json ./apps/start/
# Packages - Only copy what start app needs
COPY packages/trpc/package.json packages/trpc/
COPY packages/json/package.json packages/json/
COPY packages/common/package.json packages/common/
COPY packages/payments/package.json packages/payments/
COPY packages/constants/package.json packages/constants/
COPY packages/validation/package.json packages/validation/
COPY packages/integrations/package.json packages/integrations/
COPY packages/sdks/sdk/package.json packages/sdks/sdk/
COPY packages/sdks/_info/package.json packages/sdks/_info/
COPY patches ./patches
# BUILD
FROM base AS build
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
make \
g++ && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install all dependencies (including dev dependencies for build)
RUN pnpm install --frozen-lockfile && \
pnpm store prune
# Copy source code
COPY apps/start ./apps/start
COPY packages ./packages
COPY tooling ./tooling
# Generate Prisma client and build the app
RUN pnpm --filter start run build
# PROD - Install only production dependencies
FROM base AS prod
ENV npm_config_build_from_source=true
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
make \
g++ && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app/package.json ./
COPY --from=build /app/pnpm-lock.yaml ./
COPY --from=build /app/pnpm-workspace.yaml ./
# Copy package.json files for production install
COPY --from=build /app/apps/start/package.json ./apps/start/
COPY --from=build /app/packages/db/package.json ./packages/db/
COPY --from=build /app/packages/trpc/package.json ./packages/trpc/
COPY --from=build /app/packages/auth/package.json ./packages/auth/
COPY --from=build /app/packages/json/package.json ./packages/json/
COPY --from=build /app/packages/common/package.json ./packages/common/
COPY --from=build /app/packages/payments/package.json ./packages/payments/
COPY --from=build /app/packages/constants/package.json ./packages/constants/
COPY --from=build /app/packages/validation/package.json ./packages/validation/
COPY --from=build /app/packages/integrations/package.json ./packages/integrations/
COPY --from=build /app/packages/sdks/sdk/package.json ./packages/sdks/sdk/
COPY --from=build /app/packages/sdks/_info/package.json ./packages/sdks/_info/
COPY --from=build /app/patches ./patches
# Install production dependencies only
RUN pnpm install --frozen-lockfile --prod && \
pnpm rebuild && \
pnpm store prune
# FINAL - Minimal runtime image
FROM base AS runner
ENV NODE_ENV=production
ENV npm_config_build_from_source=true
WORKDIR /app
# Copy workspace files
COPY --from=build /app/package.json ./
COPY --from=build /app/pnpm-workspace.yaml ./
# Copy production node_modules
COPY --from=prod /app/node_modules ./node_modules
# Copy built app with .output directory
COPY --from=build /app/apps/start/.output ./apps/start/.output
COPY --from=build /app/apps/start/dist ./apps/start/dist
COPY --from=build /app/apps/start/package.json ./apps/start/
# Copy necessary packages
COPY --from=build /app/packages/db ./packages/db
COPY --from=build /app/packages/trpc ./packages/trpc
COPY --from=build /app/packages/auth ./packages/auth
COPY --from=build /app/packages/json ./packages/json
COPY --from=build /app/packages/common ./packages/common
COPY --from=build /app/packages/payments ./packages/payments
COPY --from=build /app/packages/constants ./packages/constants
COPY --from=build /app/packages/validation ./packages/validation
COPY --from=build /app/packages/integrations ./packages/integrations
COPY --from=build /app/packages/sdks/sdk ./packages/sdks/sdk
COPY --from=build /app/packages/sdks/_info ./packages/sdks/_info
COPY --from=build /app/tooling/typescript ./tooling/typescript
WORKDIR /app/apps/start
EXPOSE 3000
# Start the Tanstack Start server
CMD ["node", ".output/server/index.mjs"]

View File

@@ -65,6 +65,7 @@
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/nitro-v2-vite-plugin": "^1.133.19",
"@tanstack/react-devtools": "^0.7.6", "@tanstack/react-devtools": "^0.7.6",
"@tanstack/react-query": "^5.90.2", "@tanstack/react-query": "^5.90.2",
"@tanstack/react-query-devtools": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2",

View File

@@ -12,21 +12,12 @@ import { ThemeProvider } from './theme-provider';
export function Providers({ children }: { children: React.ReactNode }) { export function Providers({ children }: { children: React.ReactNode }) {
const storeRef = useRef<AppStore>(undefined); const storeRef = useRef<AppStore>(undefined);
if (!storeRef.current) { if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore(); storeRef.current = makeStore();
} }
return ( return (
<NuqsAdapter> <NuqsAdapter>
<ThemeProvider> <ThemeProvider>
{/* {import.meta.env.VITE_OP_CLIENT_ID && (
<OpenPanelComponent
clientId={import.meta.env.VITE_OP_CLIENT_ID}
trackScreenViews
trackOutgoingLinks
trackAttributes
/>
)} */}
<ReduxProvider store={storeRef.current}> <ReduxProvider store={storeRef.current}>
<TooltipProvider delayDuration={200}> <TooltipProvider delayDuration={200}>
{children} {children}

View File

@@ -1,13 +1,9 @@
import { useAppContext } from '@/hooks/use-app-context';
import { useTRPC } from '@/integrations/trpc/react'; import { useTRPC } from '@/integrations/trpc/react';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import type { IServiceOrganization } from '@openpanel/db'; import type { IServiceOrganization } from '@openpanel/db';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { import { Link, useLocation, useParams } from '@tanstack/react-router';
Link,
useLocation,
useParams,
useRouteContext,
} from '@tanstack/react-router';
import { MenuIcon, XIcon } from 'lucide-react'; import { MenuIcon, XIcon } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FeedbackButton } from './feedback-button'; import { FeedbackButton } from './feedback-button';
@@ -81,6 +77,7 @@ export function SidebarContainer({
}: SidebarContainerProps) { }: SidebarContainerProps) {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const location = useLocation(); const location = useLocation();
const { isSelfHosted } = useAppContext();
useEffect(() => { useEffect(() => {
setActive(false); setActive(false);
@@ -135,7 +132,7 @@ export function SidebarContainer({
<div className="mt-auto w-full "> <div className="mt-auto w-full ">
<FeedbackButton /> <FeedbackButton />
{import.meta.env.VITE_SELF_HOSTED === 'true' && ( {isSelfHosted && (
<div className={cn('text-sm w-full text-center')}> <div className={cn('text-sm w-full text-center')}>
Self-hosted instance Self-hosted instance
</div> </div>

View File

@@ -5,12 +5,13 @@ export function useAppContext() {
strict: false, strict: false,
}); });
if (!params.apiUrl || !params.dashboardUrl) { if (!params.apiUrl || !params.dashboardUrl || !params.isSelfHosted) {
throw new Error('API URL or dashboard URL is not set'); throw new Error('API URL or dashboard URL is not set');
} }
return { return {
apiUrl: params.apiUrl, apiUrl: params.apiUrl,
dashboardUrl: params.dashboardUrl, dashboardUrl: params.dashboardUrl,
isSelfHosted: params.isSelfHosted,
}; };
} }

View File

@@ -16,6 +16,8 @@ import { Route as PublicRouteImport } from './routes/_public'
import { Route as LoginRouteImport } from './routes/_login' import { Route as LoginRouteImport } from './routes/_login'
import { Route as AppRouteImport } from './routes/_app' import { Route as AppRouteImport } from './routes/_app'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiHealthcheckRouteImport } from './routes/api/healthcheck'
import { Route as ApiConfigRouteImport } from './routes/api/config'
import { Route as PublicOnboardingRouteImport } from './routes/_public.onboarding' import { Route as PublicOnboardingRouteImport } from './routes/_public.onboarding'
import { Route as LoginResetPasswordRouteImport } from './routes/_login.reset-password' import { Route as LoginResetPasswordRouteImport } from './routes/_login.reset-password'
import { Route as LoginLoginRouteImport } from './routes/_login.login' import { Route as LoginLoginRouteImport } from './routes/_login.login'
@@ -112,6 +114,16 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ApiHealthcheckRoute = ApiHealthcheckRouteImport.update({
id: '/api/healthcheck',
path: '/api/healthcheck',
getParentRoute: () => rootRouteImport,
} as any)
const ApiConfigRoute = ApiConfigRouteImport.update({
id: '/api/config',
path: '/api/config',
getParentRoute: () => rootRouteImport,
} as any)
const PublicOnboardingRoute = PublicOnboardingRouteImport.update({ const PublicOnboardingRoute = PublicOnboardingRouteImport.update({
id: '/onboarding', id: '/onboarding',
path: '/onboarding', path: '/onboarding',
@@ -459,6 +471,8 @@ export interface FileRoutesByFullPath {
'/login': typeof LoginLoginRoute '/login': typeof LoginLoginRoute
'/reset-password': typeof LoginResetPasswordRoute '/reset-password': typeof LoginResetPasswordRoute
'/onboarding': typeof PublicOnboardingRoute '/onboarding': typeof PublicOnboardingRoute
'/api/config': typeof ApiConfigRoute
'/api/healthcheck': typeof ApiHealthcheckRoute
'/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute '/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute '/$organizationId/billing': typeof AppOrganizationIdBillingRoute
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute '/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
@@ -513,6 +527,8 @@ export interface FileRoutesByTo {
'/login': typeof LoginLoginRoute '/login': typeof LoginLoginRoute
'/reset-password': typeof LoginResetPasswordRoute '/reset-password': typeof LoginResetPasswordRoute
'/onboarding': typeof PublicOnboardingRoute '/onboarding': typeof PublicOnboardingRoute
'/api/config': typeof ApiConfigRoute
'/api/healthcheck': typeof ApiHealthcheckRoute
'/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute '/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute '/$organizationId/billing': typeof AppOrganizationIdBillingRoute
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute '/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
@@ -566,6 +582,8 @@ export interface FileRoutesById {
'/_login/login': typeof LoginLoginRoute '/_login/login': typeof LoginLoginRoute
'/_login/reset-password': typeof LoginResetPasswordRoute '/_login/reset-password': typeof LoginResetPasswordRoute
'/_public/onboarding': typeof PublicOnboardingRoute '/_public/onboarding': typeof PublicOnboardingRoute
'/api/config': typeof ApiConfigRoute
'/api/healthcheck': typeof ApiHealthcheckRoute
'/_app/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute '/_app/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute
'/_app/$organizationId/billing': typeof AppOrganizationIdBillingRoute '/_app/$organizationId/billing': typeof AppOrganizationIdBillingRoute
'/_app/$organizationId/settings': typeof AppOrganizationIdSettingsRoute '/_app/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
@@ -630,6 +648,8 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/reset-password' | '/reset-password'
| '/onboarding' | '/onboarding'
| '/api/config'
| '/api/healthcheck'
| '/$organizationId/$projectId' | '/$organizationId/$projectId'
| '/$organizationId/billing' | '/$organizationId/billing'
| '/$organizationId/settings' | '/$organizationId/settings'
@@ -684,6 +704,8 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/reset-password' | '/reset-password'
| '/onboarding' | '/onboarding'
| '/api/config'
| '/api/healthcheck'
| '/$organizationId/$projectId' | '/$organizationId/$projectId'
| '/$organizationId/billing' | '/$organizationId/billing'
| '/$organizationId/settings' | '/$organizationId/settings'
@@ -736,6 +758,8 @@ export interface FileRouteTypes {
| '/_login/login' | '/_login/login'
| '/_login/reset-password' | '/_login/reset-password'
| '/_public/onboarding' | '/_public/onboarding'
| '/api/config'
| '/api/healthcheck'
| '/_app/$organizationId/$projectId' | '/_app/$organizationId/$projectId'
| '/_app/$organizationId/billing' | '/_app/$organizationId/billing'
| '/_app/$organizationId/settings' | '/_app/$organizationId/settings'
@@ -799,6 +823,8 @@ export interface RootRouteChildren {
LoginRoute: typeof LoginRouteWithChildren LoginRoute: typeof LoginRouteWithChildren
PublicRoute: typeof PublicRouteWithChildren PublicRoute: typeof PublicRouteWithChildren
StepsRoute: typeof StepsRouteWithChildren StepsRoute: typeof StepsRouteWithChildren
ApiConfigRoute: typeof ApiConfigRoute
ApiHealthcheckRoute: typeof ApiHealthcheckRoute
ShareOverviewShareIdRoute: typeof ShareOverviewShareIdRoute ShareOverviewShareIdRoute: typeof ShareOverviewShareIdRoute
} }
@@ -839,6 +865,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/api/healthcheck': {
id: '/api/healthcheck'
path: '/api/healthcheck'
fullPath: '/api/healthcheck'
preLoaderRoute: typeof ApiHealthcheckRouteImport
parentRoute: typeof rootRouteImport
}
'/api/config': {
id: '/api/config'
path: '/api/config'
fullPath: '/api/config'
preLoaderRoute: typeof ApiConfigRouteImport
parentRoute: typeof rootRouteImport
}
'/_public/onboarding': { '/_public/onboarding': {
id: '/_public/onboarding' id: '/_public/onboarding'
path: '/onboarding' path: '/onboarding'
@@ -1631,6 +1671,8 @@ const rootRouteChildren: RootRouteChildren = {
LoginRoute: LoginRouteWithChildren, LoginRoute: LoginRouteWithChildren,
PublicRoute: PublicRouteWithChildren, PublicRoute: PublicRouteWithChildren,
StepsRoute: StepsRouteWithChildren, StepsRoute: StepsRouteWithChildren,
ApiConfigRoute: ApiConfigRoute,
ApiHealthcheckRoute: ApiHealthcheckRoute,
ShareOverviewShareIdRoute: ShareOverviewShareIdRoute, ShareOverviewShareIdRoute: ShareOverviewShareIdRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@@ -30,6 +30,7 @@ interface MyRouterContext {
trpc: TRPCOptionsProxy<AppRouter>; trpc: TRPCOptionsProxy<AppRouter>;
apiUrl: string; apiUrl: string;
dashboardUrl: string; dashboardUrl: string;
isSelfHosted: boolean;
} }
export const Route = createRootRouteWithContext<MyRouterContext>()({ export const Route = createRootRouteWithContext<MyRouterContext>()({

View File

@@ -0,0 +1,19 @@
import { Sidebar } from '@/components/sidebar';
import { getServerEnvs } from '@/server/get-envs';
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router';
// Nothing sensitive here, its client environment variables which is good for debugging
export const Route = createFileRoute('/api/config')({
server: {
handlers: {
GET: async () => {
const envs = await getServerEnvs();
return new Response(JSON.stringify(envs), {
headers: {
'Content-Type': 'application/json',
},
});
},
},
},
});

View File

@@ -0,0 +1,11 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/api/healthcheck')({
server: {
handlers: {
GET: async () => {
return new Response('OK');
},
},
},
});

View File

@@ -7,6 +7,7 @@ export const getServerEnvs = createServerFn().handler(async () => {
dashboardUrl: String( dashboardUrl: String(
process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL, process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL,
), ),
isSelfHosted: process.env.SELF_HOSTED !== undefined,
}; };
return envs; return envs;

View File

@@ -1,21 +1,34 @@
import { cloudflare } from '@cloudflare/vite-plugin'; import { cloudflare } from '@cloudflare/vite-plugin';
import { wrapVinxiConfigWithSentry } from '@sentry/tanstackstart-react'; import { wrapVinxiConfigWithSentry } from '@sentry/tanstackstart-react';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin';
import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import viteReact from '@vitejs/plugin-react'; import viteReact from '@vitejs/plugin-react';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import viteTsConfigPaths from 'vite-tsconfig-paths'; import viteTsConfigPaths from 'vite-tsconfig-paths';
const config = defineConfig({ const plugins = [
plugins: [ viteTsConfigPaths({
cloudflare({ viteEnvironment: { name: 'ssr' } }), projects: ['./tsconfig.json'],
viteTsConfigPaths({ }),
projects: ['./tsconfig.json'], tailwindcss(),
tanstackStart(),
viteReact(),
];
if (process.env.NITRO) {
plugins.unshift(
nitroV2Plugin({
preset: 'node-server',
compatibilityDate: '2025-10-21',
}), }),
tailwindcss(), );
tanstackStart(), } else {
viteReact(), plugins.unshift(cloudflare({ viteEnvironment: { name: 'ssr' } }));
], }
const config = defineConfig({
plugins,
}); });
export default wrapVinxiConfigWithSentry(config, { export default wrapVinxiConfigWithSentry(config, {

View File

@@ -59,6 +59,9 @@
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"nuqs": "patches/nuqs.patch" "nuqs": "patches/nuqs.patch"
},
"overrides": {
"rolldown": "1.0.0-beta.43"
} }
} }
} }

View File

@@ -110,6 +110,7 @@ export const eventsGroupQueue = new GroupQueue<
>({ >({
logger: queueLogger, logger: queueLogger,
namespace: 'group_events', namespace: 'group_events',
// @ts-expect-error - TODO: Fix this in groupmq
redis: getRedisGroupQueue(), redis: getRedisGroupQueue(),
orderingMethod: 'in-memory', orderingMethod: 'in-memory',
orderingWindowMs, orderingWindowMs,

2030
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

270
self-hosting/get_latest_images Executable file
View File

@@ -0,0 +1,270 @@
#!/bin/bash
# GitHub repository (update if needed)
REPO="${GITHUB_REPO:-Openpanel-dev/openpanel}"
# Components to find tags for
COMPONENTS=("worker" "api" "dashboard")
# Docker compose file path
DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-./docker-compose.yml}"
# Color codes for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
GRAY='\033[0;90m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Show usage
show_usage() {
echo "Usage: $0 [COMMAND] [OPTIONS]"
echo ""
echo "Fetches the latest Git tags for worker, api, and dashboard components"
echo ""
echo "Commands:"
echo " apply Apply the latest tags to docker-compose.yml"
echo " (none) Show the latest tags (default)"
echo ""
echo "Options:"
echo " --list, -l List all available tags"
echo " --repo REPO Specify GitHub repository (default: $REPO)"
echo " --file FILE Specify docker-compose file (default: $DOCKER_COMPOSE_FILE)"
echo " --help, -h Show this help message"
echo ""
echo "Environment variables:"
echo " GITHUB_REPO Set the GitHub repository"
echo " DOCKER_COMPOSE_FILE Set the docker-compose file path"
echo ""
echo "Examples:"
echo " $0 # Show latest tags"
echo " $0 apply # Update docker-compose.yml with latest tags"
echo " $0 --list # List all available tags"
echo ""
exit 0
}
# Parse arguments
LIST_ALL=false
APPLY_MODE=false
while [[ $# -gt 0 ]]; do
case $1 in
apply)
APPLY_MODE=true
shift
;;
--list|-l)
LIST_ALL=true
shift
;;
--repo)
REPO="$2"
shift 2
;;
--file)
DOCKER_COMPOSE_FILE="$2"
shift 2
;;
--help|-h)
show_usage
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
show_usage
;;
esac
done
# Check if user needs to be logged in (for apply mode)
if [ "$APPLY_MODE" = true ]; then
# Check if Docker is available
if ! command -v docker &> /dev/null; then
echo -e "${RED}Error: Docker is not installed or not in PATH${NC}"
exit 1
fi
# Check if logged into docker.openpanel.dev
echo -e "${BLUE}Checking Docker registry authentication...${NC}\n"
if ! docker info 2>/dev/null | grep -q "docker.openpanel.dev" && ! grep -q "docker.openpanel.dev" ~/.docker/config.json 2>/dev/null; then
echo -e "${YELLOW}⚠ You need to login to the OpenPanel Docker registry first!${NC}\n"
echo -e "${CYAN}To access the latest Docker images, you need:${NC}"
echo -e " 1. Be a supporter (starts at \$20/month)"
echo -e " 2. Get your API key from your supporter dashboard"
echo -e " 3. Login to the registry with:\n"
echo -e "${GREEN} echo \"your_api_key\" | docker login docker.openpanel.dev -u user --password-stdin${NC}\n"
echo -e "${GRAY}For more info: https://openpanel.dev/docs/self-hosting/supporter-access-latest-docker-images${NC}\n"
read -p "$(echo -e ${YELLOW}Have you already logged in? [y/N]:${NC} )" -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${RED}Please login first and try again.${NC}"
exit 1
fi
else
echo -e "${GREEN}✓ Docker registry authentication OK${NC}\n"
fi
fi
echo -e "${BLUE}Fetching tags from ${REPO}...${NC}\n"
# Fetch all tags from GitHub API
TAGS_JSON=$(curl -s "https://api.github.com/repos/${REPO}/tags")
# Check if we got valid JSON response
if [ -z "$TAGS_JSON" ]; then
echo -e "${RED}Failed to fetch tags from GitHub API${NC}"
exit 1
fi
# Check if repository has any tags
TAGS_CLEAN=$(echo "$TAGS_JSON" | tr -d '[:space:]')
if [ "$TAGS_CLEAN" == "[]" ]; then
echo -e "${RED}No tags found in repository${NC}"
echo -e "${YELLOW}Create tags using: git tag <tag-name> && git push origin <tag-name>${NC}"
echo ""
echo -e "${GRAY}Example tag naming patterns:${NC}"
echo -e " ${GRAY}- worker-v1.0.0${NC}"
echo -e " ${GRAY}- api-v1.0.0${NC}"
echo -e " ${GRAY}- dashboard-v1.0.0${NC}"
exit 1
fi
# List all tags if requested
if [ "$LIST_ALL" = true ]; then
echo -e "${GREEN}All available tags:${NC}\n"
if command -v jq &> /dev/null; then
echo "$TAGS_JSON" | jq -r '.[] | " \(.name) (\(.commit.sha[0:7]))"'
else
echo "$TAGS_JSON" | grep "\"name\":" | sed 's/.*"name": "\([^"]*\)".*/ \1/'
fi
echo ""
exit 0
fi
# Function to find latest tag matching a component
get_latest_tag() {
local component=$1
local output_var_tag=$2
local output_var_sha=$3
if command -v jq &> /dev/null; then
# Use jq for better JSON parsing
local tag=$(echo "$TAGS_JSON" | jq -r "[.[] | select(.name | contains(\"${component}\"))] | .[0] | .name" 2>/dev/null)
local sha=$(echo "$TAGS_JSON" | jq -r "[.[] | select(.name | contains(\"${component}\"))] | .[0] | .commit.sha" 2>/dev/null)
else
# Fallback to grep/sed
local tag=$(echo "$TAGS_JSON" | grep -o "\"name\": \"[^\"]*${component}[^\"]*\"" | head -1 | cut -d'"' -f4)
local sha=$(echo "$TAGS_JSON" | grep -B5 "\"name\": \"${tag}\"" | grep "\"sha\"" | head -1 | cut -d'"' -f4)
fi
if [ -z "$tag" ] || [ "$tag" == "null" ]; then
echo -e "${RED}✗${NC} ${component}: No matching tag found"
echo
return 1
fi
echo -e "${GREEN}✓${NC} ${component}:"
echo -e " Tag: ${YELLOW}${tag}${NC}"
echo -e " SHA: ${sha}"
echo
# Return values via eval (for compatibility with older bash)
if [ -n "$output_var_tag" ]; then
eval "$output_var_tag='$tag'"
fi
if [ -n "$output_var_sha" ]; then
eval "$output_var_sha='$sha'"
fi
return 0
}
# Function to apply tags to docker-compose.yml
apply_tags() {
echo -e "${CYAN}Applying tags to ${DOCKER_COMPOSE_FILE}...${NC}\n"
# Check if docker-compose file exists
if [ ! -f "$DOCKER_COMPOSE_FILE" ]; then
echo -e "${RED}Error: Docker compose file not found: ${DOCKER_COMPOSE_FILE}${NC}"
exit 1
fi
# Create a backup
local backup_file="${DOCKER_COMPOSE_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
cp "$DOCKER_COMPOSE_FILE" "$backup_file"
echo -e "${GRAY}Created backup: ${backup_file}${NC}\n"
local updated=0
local failed=0
for component in "${COMPONENTS[@]}"; do
# Get tag and SHA for this component
local component_tag=""
local component_sha=""
get_latest_tag "$component" component_tag component_sha >/dev/null 2>&1
if [ -z "$component_sha" ] || [ "$component_sha" == "null" ]; then
echo -e "${RED}✗${NC} ${component}: Skipping (no tag found)"
((failed++))
continue
fi
# Get first 4 characters of SHA
local short_sha="${component_sha:0:4}"
# New image tag format
local new_image="docker.openpanel.dev/openpanel-dev/${component}:main-${short_sha}"
# Find and replace the image line in docker-compose.yml
# Look for lines like: image: something{component}something
if grep -q "image:.*${component}" "$DOCKER_COMPOSE_FILE"; then
# Use sed to replace the entire image line
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS sed syntax
sed -i '' "s|image:.*${component}.*|image: ${new_image}|g" "$DOCKER_COMPOSE_FILE"
else
# Linux sed syntax
sed -i "s|image:.*${component}.*|image: ${new_image}|g" "$DOCKER_COMPOSE_FILE"
fi
echo -e "${GREEN}✓${NC} Updated ${component}: ${CYAN}${new_image}${NC}"
((updated++))
else
echo -e "${YELLOW}⚠${NC} ${component}: No matching image line found in docker-compose.yml"
((failed++))
fi
done
echo ""
echo -e "${BLUE}Summary:${NC}"
echo -e " Updated: ${GREEN}${updated}${NC}"
echo -e " Failed: ${RED}${failed}${NC}"
echo -e " Backup: ${GRAY}${backup_file}${NC}"
echo ""
if [ $updated -gt 0 ]; then
echo -e "${GREEN}Successfully updated docker-compose.yml!${NC}"
fi
}
# Main execution
if [ "$APPLY_MODE" = true ]; then
# Apply mode: update docker-compose.yml
# First, show all tags
for component in "${COMPONENTS[@]}"; do
get_latest_tag "$component"
done
# Then apply them
apply_tags
else
# Default mode: just show the tags
for component in "${COMPONENTS[@]}"; do
get_latest_tag "$component"
done
echo -e "${GRAY}Tip: Use --list to see all available tags, or 'apply' to update docker-compose.yml${NC}"
echo -e "${BLUE}Done!${NC}"
fi

3
self-hosting/restart Executable file
View File

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