Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fb632006a | ||
|
|
63b0eef948 | ||
|
|
e429bff0c2 | ||
|
|
6425052f98 | ||
|
|
62c2f0ed73 | ||
|
|
830d6bbc8d | ||
|
|
b87d9a9a8a | ||
|
|
a9aa64ee51 | ||
|
|
8ac27a0f89 | ||
|
|
614e4fef12 | ||
|
|
c784f9b9c8 | ||
|
|
698ab85f6d | ||
|
|
6284bbc74f | ||
|
|
7b1cf99803 | ||
|
|
1aa2d74f4e | ||
|
|
b35349963e | ||
|
|
127b607d76 | ||
|
|
c974384957 | ||
|
|
b05ce42a12 | ||
|
|
f29a0b7340 | ||
|
|
b5b37264a4 | ||
|
|
bf8322317c | ||
|
|
a9d21b63b7 | ||
|
|
c322999417 | ||
|
|
8deda67f4c | ||
|
|
0bb292dad3 | ||
|
|
63635b1ff6 | ||
|
|
6621a29d54 | ||
|
|
e4cd30c15a | ||
|
|
73d257ec11 | ||
|
|
b456667931 | ||
|
|
fee40dba4c | ||
|
|
aa60cb637a | ||
|
|
f343ceb15f | ||
|
|
72491787d4 | ||
|
|
9c784b723e | ||
|
|
a307ed4720 | ||
|
|
39ce5301b4 | ||
|
|
b7ca428d92 | ||
|
|
d5d374ad72 | ||
|
|
bf8ca3fb35 | ||
|
|
350e44ff95 | ||
|
|
a0d6ed1527 | ||
|
|
7a343aa1b8 | ||
|
|
cd446e9f25 | ||
|
|
f4bf271183 | ||
|
|
443976641c | ||
|
|
64073b5ec2 | ||
|
|
fcac3882ca | ||
|
|
979eadc06a | ||
|
|
a7339677c9 | ||
|
|
bbd0817b9d | ||
|
|
c4398d281f | ||
|
|
7b4af4c234 | ||
|
|
62a418ee4f | ||
|
|
740981d044 | ||
|
+4 |
a14bab01ad |
@@ -109,20 +109,22 @@ const Search = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search templates..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
className="w-full p-6"
|
||||
/>
|
||||
{searchQuery.length > 0 ? (
|
||||
<div className="cursor-pointer" onClick={handleClearSearch}>
|
||||
<XIcon className="absolute end-3 translate-y-3.5 top-1/2 h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
) : (
|
||||
<SearchIcon className="absolute end-3 translate-y-2 top-1/2 lg:size-5 size-4 text-gray-400" />
|
||||
)}
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search templates..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
className="w-full"
|
||||
/>
|
||||
{searchQuery.length > 0 ? (
|
||||
<div className="cursor-pointer" onClick={handleClearSearch}>
|
||||
<XIcon className="absolute end-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
) : (
|
||||
<SearchIcon className="absolute end-3 top-1/2 -translate-y-1/2 lg:size-5 size-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 justify-between">
|
||||
@@ -154,7 +156,7 @@ const Search = () => {
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<PopoverContent className="w-50 p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Search tags...."
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "./components/ui/dropdown-menu"
|
||||
import { useTheme } from "./theme-provider"
|
||||
|
||||
@@ -13,7 +13,7 @@ export function ModeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
|
||||
@@ -15,7 +15,10 @@ env = [
|
||||
"_APP_POOL_ADAPTER=stack",
|
||||
"_APP_WORKER_PER_CORE=6",
|
||||
"_APP_OPTIONS_ABUSE=enabled",
|
||||
"_APP_OPTIONS_FORCE_HTTPS=disabled",
|
||||
# Requires HTTPS to be enabled on the main domain in Dokploy (Domains tab).
|
||||
# Makes Appwrite generate https:// URLs (e.g. APPWRITE_FUNCTION_API_ENDPOINT for
|
||||
# Functions) and reject plain-HTTP API calls; over plain HTTP the console cannot log in.
|
||||
"_APP_OPTIONS_FORCE_HTTPS=enabled",
|
||||
"_APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled",
|
||||
"_APP_OPTIONS_ROUTER_PROTECTION=disabled",
|
||||
"_APP_OPENSSL_KEY_V1=${openssl_key}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
autobase-console:
|
||||
image: autobase/console:2.7.2
|
||||
image: autobase/console:2.10.0
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "80"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "autobase",
|
||||
"name": "Autobase",
|
||||
"version": "2.7.2",
|
||||
"version": "2.10.0",
|
||||
"description": "Autobase for PostgreSQL® is an open-source alternative to cloud-managed databases (self-hosted DBaaS).",
|
||||
"links": {
|
||||
"github": "https://github.com/vitabaks/autobase",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# BlogFactory on Dokploy
|
||||
|
||||
Dokploy creates independent database, storage, JWT, encryption, and scheduler secrets during import. Only the `web` service receives a public domain; the API, scheduler, PostgreSQL, and MinIO remain private.
|
||||
|
||||
## First administrator
|
||||
|
||||
1. Open the Compose service's environment settings and copy the generated `ADMIN_EMAILS` value.
|
||||
2. Open the generated BlogFactory domain and register with that exact email address.
|
||||
3. Return to the Compose environment settings, change `BLOGFACTORY_ALLOW_SIGNUP` to `false`, and redeploy.
|
||||
4. Add OpenRouter, CMS, or Google Search Console credentials from BlogFactory only when you need those optional integrations.
|
||||
|
||||
BlogFactory sends reviewed content to connected CMS providers as drafts. The template does not grant live-publish or delete authority.
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="8" fill="#fff"/>
|
||||
<rect x="1" y="1" width="62" height="62" rx="7" fill="none" stroke="#d7d8d4" stroke-width="2"/>
|
||||
<circle cx="50" cy="14" r="5" fill="#ff5a1f"/>
|
||||
<rect x="13" y="31" width="10" height="18" fill="#202224"/>
|
||||
<rect x="27" y="20" width="10" height="29" fill="#202224"/>
|
||||
<rect x="41" y="26" width="10" height="23" fill="#202224"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 453 B |
@@ -0,0 +1,102 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17.6-alpine3.22
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: blogfactory
|
||||
POSTGRES_USER: blogfactory
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U blogfactory -d blogfactory"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address :9001
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
api:
|
||||
image: ghcr.io/blogfactoryhq/blogfactory-api:v0.1.0
|
||||
restart: unless-stopped
|
||||
command: >-
|
||||
sh -c "bun run src/init-s3-bucket.ts && bun run src/db/migrate.ts && exec bun run src/index.ts"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
expose:
|
||||
- 3000
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
BLOGFACTORY_SELF_HOSTED: "true"
|
||||
DATABASE_URL: postgresql://blogfactory:${POSTGRES_PASSWORD}@postgres:5432/blogfactory
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
API_KEY_ENCRYPTION_SECRET: ${API_KEY_ENCRYPTION_SECRET}
|
||||
ADMIN_EMAILS: ${ADMIN_EMAILS}
|
||||
BLOGFACTORY_ALLOW_SIGNUP: ${BLOGFACTORY_ALLOW_SIGNUP}
|
||||
CRON_SECRET: ${CRON_SECRET}
|
||||
WEB_APP_URL: ${BLOGFACTORY_URL}
|
||||
MCP_APP_URL: http://web/mcp-review.html
|
||||
MCP_ALLOWED_ORIGINS: ${BLOGFACTORY_URL}
|
||||
S3_ENDPOINT: http://minio:9000
|
||||
S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
||||
S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
S3_BUCKET: blogfactory
|
||||
S3_REGION: us-east-1
|
||||
healthcheck:
|
||||
test: ["CMD", "bun", "-e", "fetch('http://localhost:3000/api/ready').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
web:
|
||||
image: ghcr.io/blogfactoryhq/blogfactory-web:v0.1.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
expose:
|
||||
- 80
|
||||
environment:
|
||||
API_UPSTREAM: http://api:3000
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "nginx -t >/dev/null 2>&1 && wget -q -O /dev/null http://api:3000/api/ready"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
scheduler:
|
||||
image: curlimages/curl:8.15.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
CRON_SECRET: ${CRON_SECRET}
|
||||
CRON_INTERVAL_SECONDS: 21600
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
- >-
|
||||
while true; do curl --fail --silent --show-error --output /dev/null -H "Authorization: Bearer $${CRON_SECRET}" http://api:3000/api/cron/drain && echo "Scheduler drain completed"; sleep "$${CRON_INTERVAL_SECONDS}"; done
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
minio-data:
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "blogfactory",
|
||||
"name": "BlogFactory",
|
||||
"version": "v0.1.0",
|
||||
"description": "Self-hosted control plane for AI-assisted content operations and human-approved CMS draft delivery.",
|
||||
"logo": "blogfactory.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/BlogFactoryHQ/blogfactory",
|
||||
"website": "https://blogfactory.io/",
|
||||
"docs": "https://github.com/BlogFactoryHQ/blogfactory/blob/main/docs/self-hosting.md"
|
||||
},
|
||||
"tags": ["ai", "automation", "content-management"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
postgres_password = "${password:48}"
|
||||
minio_root_user = "blogfactory"
|
||||
minio_root_password = "${password:48}"
|
||||
jwt_secret = "${base64:48}"
|
||||
encryption_secret = "${base64:48}"
|
||||
cron_secret = "${base64:48}"
|
||||
admin_email = "${email}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"POSTGRES_PASSWORD=${postgres_password}",
|
||||
"MINIO_ROOT_USER=${minio_root_user}",
|
||||
"MINIO_ROOT_PASSWORD=${minio_root_password}",
|
||||
"JWT_SECRET=${jwt_secret}",
|
||||
"API_KEY_ENCRYPTION_SECRET=${encryption_secret}",
|
||||
"CRON_SECRET=${cron_secret}",
|
||||
"ADMIN_EMAILS=${admin_email}",
|
||||
"BLOGFACTORY_ALLOW_SIGNUP=true",
|
||||
"BLOGFACTORY_URL=https://${main_domain}",
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "web"
|
||||
port = 80
|
||||
host = "${main_domain}"
|
||||
@@ -1,3 +1,8 @@
|
||||
# If you are on a machine with ARM architecture, please add a "-arm" suffix to the calcom image on line 19.
|
||||
# For example, `calcom/cal.com:v6.2.0` will become `calcom/cal.com:v6.2.0-arm`
|
||||
|
||||
# If serving the instance on https, change `NEXT_PUBLIC_WEBAPP_URL` (Line 28) and `NEXTAUTH_URL` (Line 30) to https
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
@@ -11,15 +16,25 @@ services:
|
||||
- DATABASE_URL=postgres://postgres:password@postgres:5432/db
|
||||
|
||||
calcom:
|
||||
image: calcom/cal.com:v2.7.6
|
||||
image: calcom/cal.com:v6.2.0
|
||||
depends_on:
|
||||
- postgres
|
||||
environment:
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY}
|
||||
- DATABASE_URL=postgres://postgres:password@postgres:5432/db
|
||||
- DATABASE_DIRECT_URL=postgres://postgres:password@postgres:5432/db
|
||||
- DATABASE_HOST=postgres:5432
|
||||
- NEXT_PUBLIC_WEBAPP_URL=http://${CALCOM_HOST}
|
||||
- ALLOWED_HOSTNAMES="${CALCOM_HOST}"
|
||||
- NEXTAUTH_URL=http://${CALCOM_HOST}/api/auth
|
||||
- NEXT_PUBLIC_DISABLE_SIGNUP=${DISABLE_SIGNUP}
|
||||
- EMAIL_SERVER_HOST=${SMTP_SERVER}
|
||||
- EMAIL_SERVER_PORT=${SMTP_PORT}
|
||||
- EMAIL_SERVER_USER=${EMAIL_SERVER_USER}
|
||||
- EMAIL_SERVER_PASSWORD=${SMTP_PASSWORD}
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
|
||||
|
||||
volumes:
|
||||
calcom-data:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "calcom",
|
||||
"name": "Calcom",
|
||||
"version": "v2.7.6",
|
||||
"version": "v6.2.0",
|
||||
"description": "Calcom is a open source alternative to Calendly that allows to create scheduling and booking services.",
|
||||
"links": {
|
||||
"github": "https://github.com/calcom/cal.com",
|
||||
|
||||
@@ -8,6 +8,12 @@ env = [
|
||||
"CALCOM_HOST=${main_domain}",
|
||||
"NEXTAUTH_SECRET=${nextauth_secret}",
|
||||
"CALENDSO_ENCRYPTION_KEY=${calcom_encryption_key}",
|
||||
"DISABLE_SIGNUP=true",
|
||||
"SMTP_SERVER=",
|
||||
"SMTP_PORT=",
|
||||
"SMTP_PASSWORD=",
|
||||
"EMAIL_SERVER_USER=",
|
||||
"EMAIL_FROM=",
|
||||
]
|
||||
mounts = []
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"logo": "logo.png",
|
||||
"links": {
|
||||
"github": "https://github.com/chiefonboarding/chiefonboarding",
|
||||
"website": "https://demo.chiefonboarding.com/",
|
||||
"website": "https://chiefonboarding.com/",
|
||||
"docs": "https://docs.chiefonboarding.com/"
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "Convex is an open-source reactive database designed to make life easy for web app developers.",
|
||||
"logo": "convex.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/get-convex/convex",
|
||||
"github": "https://github.com/get-convex/convex-backend",
|
||||
"website": "https://www.convex.dev/",
|
||||
"docs": "https://www.convex.dev/docs"
|
||||
},
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"description": "One personal context file every AI agent reads before answering and proposes updates to as it learns about you. Requires an external Supabase project (see instructions).",
|
||||
"logo": "creed.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/connorhpbrn/creed",
|
||||
"github": "https://github.com/hpbrn/creed",
|
||||
"website": "https://creed.md",
|
||||
"docs": "https://creed.md/docs"
|
||||
"docs": "https://docs.creed.md/"
|
||||
},
|
||||
"tags": [
|
||||
"ai",
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"logo": "image.png",
|
||||
"links": {
|
||||
"github": "https://github.com/sergi0g/cup",
|
||||
"website": "https://cup.sh",
|
||||
"docs": "https://github.com/sergi0g/cup"
|
||||
"website": "https://cup.sergi0g.dev/",
|
||||
"docs": "https://cup.sergi0g.dev/docs"
|
||||
},
|
||||
"tags": [
|
||||
"docker",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# ============================================================================
|
||||
# Didacta Community — blueprint de Dokploy
|
||||
# ----------------------------------------------------------------------------
|
||||
# Formato de `blueprints/<id>/docker-compose.yml` del repositorio
|
||||
# Dokploy/templates. Reglas de esa casa que este fichero cumple:
|
||||
# - Sin `ports:` (el proxy de Dokploy entra por la red interna).
|
||||
# - Sin `container_name:` ni `networks:` (los gestiona Dokploy).
|
||||
# - `restart: unless-stopped` en todos los servicios.
|
||||
# - El nombre del servicio principal coincide con el id del blueprint.
|
||||
#
|
||||
# Las variables las inyecta Dokploy en un `.env` junto a este fichero a partir
|
||||
# de `[config.env]` de template.toml — de ahí el `env_file` de cada servicio.
|
||||
#
|
||||
# UN SOLO DOMINIO: el contenedor corre API (:4000) y web (:3000) juntos, y
|
||||
# Next.js reescribe `/api/*`, `/healthz` y `/readyz` al 4000 interno. El
|
||||
# dominio de template.toml apunta al 3000 y no hace falta nada más.
|
||||
# ============================================================================
|
||||
|
||||
services:
|
||||
didacta:
|
||||
image: ghcr.io/va360labs/didacta-community:0.0.1-alpha.107
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
expose:
|
||||
- 3000
|
||||
volumes:
|
||||
# Uploads de cursos, certificados y evidencias + la clave de cifrado
|
||||
# autogenerada. Es el volumen que hay que respaldar.
|
||||
- didacta-data:/app/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-fsS', 'http://127.0.0.1:4000/healthz']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
# El primer arranque aplica migraciones, RLS y grants antes de servir.
|
||||
start_period: 120s
|
||||
|
||||
postgres:
|
||||
# pgvector NO es opcional: la migración baseline hace
|
||||
# `CREATE EXTENSION "vector"` y falla contra un postgres:16 estándar.
|
||||
image: pgvector/pgvector:pg16
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- didacta-postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- didacta-redis-data:/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
didacta-data:
|
||||
driver: local
|
||||
didacta-postgres-data:
|
||||
driver: local
|
||||
didacta-redis-data:
|
||||
driver: local
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "didacta",
|
||||
"name": "Didacta",
|
||||
"version": "0.0.1-alpha.107",
|
||||
"description": "Didacta is a fair-code, self-hosted learning platform (LMS) with multi-tenant isolation, a public course catalog, enrollment flows, gamification and a module marketplace.",
|
||||
"logo": "logo.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/va360labs/didacta-io",
|
||||
"website": "https://didacta.io",
|
||||
"docs": "https://docs.didacta.io"
|
||||
},
|
||||
"tags": ["education", "lms", "elearning", "courses"]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# ============================================================================
|
||||
# Didacta Community — configuración del blueprint de Dokploy
|
||||
# ----------------------------------------------------------------------------
|
||||
# Dokploy resuelve los helpers de [variables], escribe [config.env] en un .env
|
||||
# junto al docker-compose.yml y publica el dominio de [[config.domains]].
|
||||
#
|
||||
# Helpers disponibles: ${domain}, ${password:N}, ${base64:N}, ${hash:N},
|
||||
# ${uuid}, ${randomPort}, ${email}, ${username}, ${timestamp}.
|
||||
# ============================================================================
|
||||
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
# 64 caracteres sin símbolos: firma las sesiones. Rotarlo echa a todo el mundo.
|
||||
auth_secret = "${password:64}"
|
||||
# Token de un solo uso del alta inicial. Va en una URL, así que sin símbolos.
|
||||
setup_token = "${password:32}"
|
||||
# Va dentro de la cadena de conexión de Postgres: sin símbolos a propósito.
|
||||
postgres_password = "${password:32}"
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "didacta"
|
||||
port = 3000
|
||||
host = "${main_domain}"
|
||||
|
||||
[config.env]
|
||||
NODE_ENV = "production"
|
||||
# Debe coincidir SIEMPRE con el tag de la imagen del docker-compose.yml.
|
||||
DIDACTA_CORE_VERSION = "0.0.1-alpha.107"
|
||||
|
||||
# Base absoluta de los enlaces que salen por correo (verificación de email,
|
||||
# invitaciones, recordatorios). Sin esto los correos llevan enlaces rotos.
|
||||
WEB_PUBLIC_URL = "https://${main_domain}"
|
||||
WEB_PUBLIC_ALLOWED_HOSTS = "${main_domain}"
|
||||
|
||||
# Credenciales del Postgres del propio blueprint. El servicio `postgres` las
|
||||
# lee del mismo .env para inicializar el cluster.
|
||||
POSTGRES_USER = "didacta"
|
||||
POSTGRES_PASSWORD = "${postgres_password}"
|
||||
POSTGRES_DB = "didacta"
|
||||
|
||||
# Conexión de BOOTSTRAP (superusuario). Se usa solo para migraciones, RLS y
|
||||
# grants — nunca para servir tráfico. El entrypoint deriva de aquí la conexión
|
||||
# de runtime hacia el rol `didacta_app` (NOBYPASSRLS), que es lo que hace real
|
||||
# el aislamiento entre tenants. Por eso NO se define DATABASE_URL.
|
||||
ADMIN_DATABASE_URL = "postgresql://didacta:${postgres_password}@postgres:5432/didacta?schema=public"
|
||||
REDIS_URL = "redis://redis:6379"
|
||||
|
||||
AUTH_SECRET = "${auth_secret}"
|
||||
# Fijarlo evita tener que buscar el token en los logs: el alta se hace en
|
||||
# https://<dominio>/setup?token=<este valor>.
|
||||
DIDACTA_SETUP_TOKEN = "${setup_token}"
|
||||
|
||||
# Puertos INTERNOS fijos. No tocar: el dominio apunta al 3000 y el healthcheck
|
||||
# al 4000.
|
||||
API_PORT = "4000"
|
||||
WEB_PORT = "3000"
|
||||
|
||||
# Almacenamiento local sobre el volumen persistente `didacta-data`. Para S3
|
||||
# (AWS, Hetzner, MinIO…): STORAGE_DRIVER=s3 y rellenar las S3_*.
|
||||
STORAGE_DRIVER = "local"
|
||||
STORAGE_ROOT = "/app/data/storage"
|
||||
|
||||
# SMTP: sin esto la instalación arranca igual, avisa por la interfaz y no puede
|
||||
# mandar correos. Rellenar desde el panel de Dokploy después del despliegue.
|
||||
SMTP_HOST = ""
|
||||
SMTP_PORT = "587"
|
||||
SMTP_USER = ""
|
||||
SMTP_PASS = ""
|
||||
SMTP_SECURE = "false"
|
||||
SMTP_FROM = ""
|
||||
|
||||
# Licencia Enterprise (opcional). Vacío = edición Community.
|
||||
DIDACTA_LICENSE_KEY = ""
|
||||
|
||||
# TENANT_SETTINGS_ENC_KEY se deja SIN definir a propósito: la app genera una
|
||||
# clave hex de 32 bytes en el primer arranque y la persiste en el volumen
|
||||
# `didacta-data`. Definirla con un helper que no produzca exactamente 64
|
||||
# caracteres hexadecimales rompería el cifrado de secretos (SSO, SMTP por
|
||||
# tenant, Stripe) de forma silenciosa.
|
||||
@@ -0,0 +1,16 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
dockroute:
|
||||
image: ghcr.io/dockroute/dockroute:0.2.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DOCKROUTE_PROVIDER=${DOCKROUTE_PROVIDER}
|
||||
- DOCKROUTE_OWNER_ID=${DOCKROUTE_OWNER_ID}
|
||||
- DOCKROUTE_POLICY=${DOCKROUTE_POLICY}
|
||||
- DOCKROUTE_DEFAULT_TARGET=${DOCKROUTE_DEFAULT_TARGET}
|
||||
- DOCKROUTE_DOMAIN_FILTER=${DOCKROUTE_DOMAIN_FILTER}
|
||||
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
|
||||
- CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID}
|
||||
- CLOUDFLARE_TUNNEL_ID=${CLOUDFLARE_TUNNEL_ID}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
After Width: | Height: | Size: 691 KiB |
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "dockroute",
|
||||
"name": "DockRoute",
|
||||
"version": "0.2.0",
|
||||
"description": "External-DNS for plain Docker hosts: watches container labels and reconciles DNS records and Cloudflare Tunnel routes, never touching records it cannot prove it manages.",
|
||||
"logo": "dockroute.png",
|
||||
"links": {
|
||||
"github": "https://github.com/Dockroute/Dockroute",
|
||||
"website": "https://www.dockroute.dev",
|
||||
"docs": "https://www.dockroute.dev"
|
||||
},
|
||||
"tags": [
|
||||
"dns",
|
||||
"networking",
|
||||
"automation"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
variables = {}
|
||||
|
||||
[config]
|
||||
domains = []
|
||||
mounts = []
|
||||
|
||||
[config.env]
|
||||
DOCKROUTE_PROVIDER = "log"
|
||||
DOCKROUTE_OWNER_ID = "default"
|
||||
DOCKROUTE_POLICY = "sync"
|
||||
DOCKROUTE_DEFAULT_TARGET = ""
|
||||
DOCKROUTE_DOMAIN_FILTER = ""
|
||||
CLOUDFLARE_API_TOKEN = ""
|
||||
CLOUDFLARE_ACCOUNT_ID = ""
|
||||
CLOUDFLARE_TUNNEL_ID = ""
|
||||
@@ -6,8 +6,8 @@
|
||||
"logo": "docling-serve.png",
|
||||
"links": {
|
||||
"github": "https://github.com/docling-project/docling-serve",
|
||||
"website": "https://docling-project.github.io/docling-serve/",
|
||||
"docs": "https://docling-project.github.io/docling-serve/",
|
||||
"website": "",
|
||||
"docs": "https://github.com/docling-project/docling-serve/blob/main/docs/README.md",
|
||||
"dockerhub": "https://quay.io/repository/docling-project/docling-serve"
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"logo": "doublezero.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/technomancy-dev/00",
|
||||
"website": "https://www.double-zero.cloud/",
|
||||
"website": "",
|
||||
"docs": "https://github.com/technomancy-dev/00"
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"links": {
|
||||
"github": "https://github.com/amir20/dozzle",
|
||||
"website": "https://dozzle.dev",
|
||||
"docs": "https://dozzle.dev/docs"
|
||||
"docs": "https://dozzle.dev/guide/what-is-dozzle"
|
||||
},
|
||||
"tags": [
|
||||
"monitoring",
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"logo": "drawio.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/jgraph/drawio",
|
||||
"website": "https://draw.io/",
|
||||
"docs": "https://www.drawio.com/doc/"
|
||||
"website": "https://www.drawio.com/",
|
||||
"docs": "https://www.drawio.com/docs/"
|
||||
},
|
||||
"tags": [
|
||||
"drawing",
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
"description": "Drawnix is an application for generating and managing visual content, powered by pubuzhixing/drawnix Docker image.",
|
||||
"logo": "image.png",
|
||||
"links": {
|
||||
"github": "https://github.com/pubuzhixing/drawnix",
|
||||
"website": "https://hub.docker.com/r/pubuzhixing/drawnix",
|
||||
"docs": "https://hub.docker.com/r/pubuzhixing/drawnix"
|
||||
"github": "https://github.com/plait-board/drawnix/",
|
||||
"website": "https://drawnix.com/",
|
||||
"docs": "https://github.com/plait-board/drawnix/blob/develop/README_en.md#docker",
|
||||
"dockerhub": "https://hub.docker.com/r/pubuzhixing/drawnix"
|
||||
},
|
||||
"tags": [
|
||||
"visualization",
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"description": "Drizzle Gateway is a self-hosted database gateway that allows you to connect to your databases from anywhere.",
|
||||
"logo": "drizzle-gateway.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/drizzle-team/drizzle-gateway",
|
||||
"website": "https://drizzle-team.github.io/",
|
||||
"docs": "https://drizzle-team.github.io/docs"
|
||||
"github": "",
|
||||
"website": "https://gateway.drizzle.team/",
|
||||
"docs": "https://gateway.drizzle.team/docs/railway"
|
||||
},
|
||||
"tags": [
|
||||
"database",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"logo": "logo.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/dumbwareio/dumbassets",
|
||||
"website": "https://www.dumbware.io/software/DumbAssets/",
|
||||
"website": "https://dumbware.io/DumbAssets/",
|
||||
"docs": "https://github.com/dumbwareio/dumbassets"
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"description": "DumbBudget is a simple, self-hosted budget tracking service with PIN protection and no database required.",
|
||||
"logo": "logo.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/dumbwareio/dumbbudget",
|
||||
"website": "https://www.dumbware.io/software/DumbBudget/",
|
||||
"docs": "https://github.com/dumbwareio/dumbbudget"
|
||||
"github": "https://github.com/DumbWareio/DumbBudget",
|
||||
"website": "https://dumbware.io/DumbBudget",
|
||||
"docs": "https://github.com/DumbWareio/DumbBudget/blob/main/README.md"
|
||||
},
|
||||
"tags": [
|
||||
"budget",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"logo": "logo.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/dumbwareio/dumbdrop",
|
||||
"website": "https://www.dumbware.io/software/DumbDrop/",
|
||||
"website": "https://dumbware.io/DumbDrop/",
|
||||
"docs": "https://github.com/dumbwareio/dumbdrop"
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"logo": "logo.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/dumbwareio/dumbpad",
|
||||
"website": "https://www.dumbware.io/software/DumbPad/",
|
||||
"website": "https://dumbware.io/DumbPad/",
|
||||
"docs": "https://github.com/dumbwareio/dumbpad"
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"links": {
|
||||
"github": "https://github.com/elastic/elasticsearch",
|
||||
"website": "https://www.elastic.co/elasticsearch/",
|
||||
"docs": "https://docs.elastic.co/elasticsearch/"
|
||||
"docs": "https://www.elastic.co/docs/solutions/search"
|
||||
},
|
||||
"tags": [
|
||||
"search",
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"description": "Evolution API is a robust platform dedicated to empowering small businesses with limited resources, going beyond a simple messaging solution via WhatsApp.",
|
||||
"logo": "evolutionapi.png",
|
||||
"links": {
|
||||
"github": "https://github.com/EvolutionAPI/evolution-api",
|
||||
"docs": "https://doc.evolution-api.com/v2/en/get-started/introduction",
|
||||
"website": "https://evolution-api.com/opensource-whatsapp-api/"
|
||||
"github": "https://github.com/evolution-foundation/evolution-api",
|
||||
"docs": "https://docs.evolutionfoundation.com.br/en",
|
||||
"website": "https://evolutionfoundation.com.br/"
|
||||
},
|
||||
"tags": [
|
||||
"api",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
falkordb:
|
||||
image: falkordb/falkordb:v4.20.4-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- REDIS_ARGS=--requirepass ${FALKORDB_PASSWORD} --appendonly yes
|
||||
- FALKORDB_ARGS=MAX_QUEUED_QUERIES 25 TIMEOUT 1000 RESULTSET_SIZE 10000 IMPORT_FOLDER /app/.data/import
|
||||
- FALKORDB_PASSWORD=${FALKORDB_PASSWORD}
|
||||
- AUTH_URL=${AUTH_URL}
|
||||
- AUTH_SECRET=${AUTH_SECRET}
|
||||
- NEXTAUTH_URL=${AUTH_URL}
|
||||
- NEXTAUTH_SECRET=${AUTH_SECRET}
|
||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||
- ALLOWED_ORIGINS=${AUTH_URL}
|
||||
- TRUST_PROXY_HEADERS=true
|
||||
- CSV_STORAGE=local
|
||||
- CSV_LOCAL_TEMP_DIR=/app/.data/import
|
||||
- CSV_LOCAL_LOAD_URI_MODE=file
|
||||
- API_TOKEN_STORAGE_PATH=/app/.data/api_tokens.json
|
||||
volumes:
|
||||
- falkordb_data:/var/lib/falkordb/data
|
||||
- falkordb_browser_data:/app/.data
|
||||
expose:
|
||||
- 3000
|
||||
- 6379
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "REDISCLI_AUTH=$$FALKORDB_PASSWORD redis-cli ping | grep PONG"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
falkordb_data:
|
||||
falkordb_browser_data:
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg width="508" height="508" viewBox="0 0 508 508" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1764_80)">
|
||||
<rect x="2" width="508" height="508" rx="63.5" fill="#191919"/>
|
||||
<path d="M459.448 247.263C459.06 249.273 458.295 251.265 457.465 253.036C456.174 255.775 454.477 258.357 452.439 260.598C447.809 265.671 441.271 268.253 434.576 269.608C427.88 270.964 407.573 270.088 399.089 268.041C377.481 262.821 355.182 262.941 341.966 268.05C333.583 271.296 330.651 273.15 326.224 276.968C308.711 292.027 300.872 328.455 293.919 349.897C289.188 364.56 284.549 379.823 273.925 391.378C257.565 409.149 230.332 414 205.635 414H97.1546C95.7989 414 94.7752 412.773 95.0427 411.436L97.588 398.35C99.6538 387.689 109.005 379.998 119.869 379.998H192.706C213.732 379.998 236.234 377.056 247.891 360.963C254.43 351.916 256.311 340.711 258.985 330.051C268.346 292.728 271.454 258.219 308.822 243.5C297.811 236.621 289.806 225.499 287.021 213.27C281.902 190.694 294.011 167.962 295.136 144.897C295.634 135.075 293.319 123.999 284.697 118.549C278.656 114.722 270.91 114.611 263.652 114.611C236.409 114.639 219.182 114.657 187.504 114.676H180.145C165.897 114.685 155.199 101.691 157.919 87.7101L158.685 83.7446C158.879 82.7209 159.745 82.0016 160.778 82.0016C177.637 82.0016 241.602 82.103 273.695 82.0016C292.222 81.9555 313.036 82.8224 325.579 95.7242C329.738 100.013 332.394 104.992 333.962 110.387C334.035 110.6 334.091 110.83 334.155 111.051C337.752 124.267 334.921 139.852 331.296 153.621C326.63 171.531 321.632 192.345 331.628 209.461C335.603 216.258 341.828 221.293 349.224 223.959C353.356 225.453 355.126 226.2 359.516 226.163C361.711 226.144 363.897 225.923 366.064 225.536C370.38 224.779 375.074 223.728 378.966 222.963C380.829 222.594 382.701 222.262 384.591 221.994C389.719 221.275 395.206 221.137 400.426 222.179C404.253 222.944 407.988 224.355 411.188 226.605C414.591 229.003 416.602 231.982 418.391 235.671C419.396 237.737 420.641 239.83 422.67 240.909C426.303 242.846 431.938 241.545 435.848 241.232C439.915 240.909 443.955 240.273 448.012 239.802C449.986 239.572 451.959 239.369 453.942 239.277C455.501 239.203 457.198 239.406 458.369 240.559C459.208 241.388 459.605 242.587 459.697 243.768C459.78 244.911 459.678 246.083 459.457 247.254L459.448 247.263Z" fill="url(#paint0_linear_1764_80)"/>
|
||||
<path d="M182.763 183.28L172.627 235.164C172.36 236.556 173.421 237.848 174.841 237.848H229.159C230.58 237.848 231.64 239.139 231.373 240.531L228.956 253.055C226.909 263.66 217.622 271.333 206.823 271.333H167.509C166.43 271.333 165.499 272.099 165.296 273.159L154.663 327.847C152.606 338.443 143.319 346.088 132.529 346.088H110.525C109.105 346.088 108.045 344.797 108.312 343.405L145.902 149.794C146.104 148.734 147.036 147.968 148.115 147.968H254.096C255.516 147.968 256.577 149.259 256.309 150.652L253.893 163.176C251.846 173.781 242.559 181.454 231.76 181.454H184.985C183.906 181.454 182.975 182.219 182.772 183.28H182.763Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1764_80" x1="459.787" y1="82.0147" x2="70.7574" y2="381.599" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#7466FF"/>
|
||||
<stop offset="0.5" stop-color="#FF66B3"/>
|
||||
<stop offset="1" stop-color="#FF804D"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1764_80">
|
||||
<rect width="508" height="508" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,4 @@
|
||||
1. Open the generated domain to access FalkorDB Browser.
|
||||
2. Create a connection using `localhost` as the host and `6379` as the port.
|
||||
3. Leave the username empty and use the generated `FALKORDB_PASSWORD` from the Dokploy environment settings.
|
||||
4. Keep port `6379` private unless remote database clients need direct access. The browser interface can access it inside the container.
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "falkordb",
|
||||
"name": "FalkorDB",
|
||||
"version": "4.20.4",
|
||||
"description": "A high-performance graph database for knowledge graphs, GraphRAG, agent memory, fraud detection and other relationship-heavy workloads. Includes the FalkorDB Browser interface.",
|
||||
"logo": "falkordb.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/FalkorDB/FalkorDB",
|
||||
"website": "https://www.falkordb.com/",
|
||||
"docs": "https://docs.falkordb.com/"
|
||||
},
|
||||
"tags": [
|
||||
"database",
|
||||
"graph-database",
|
||||
"knowledge-graph",
|
||||
"graphrag",
|
||||
"ai",
|
||||
"redis"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
falkordb_url = "https://${main_domain}"
|
||||
falkordb_password = "${hash:32}"
|
||||
auth_secret = "${base64:48}"
|
||||
encryption_key = "${hash:32}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"FALKORDB_PASSWORD=${falkordb_password}",
|
||||
"AUTH_URL=${falkordb_url}",
|
||||
"AUTH_SECRET=${auth_secret}",
|
||||
"ENCRYPTION_KEY=${encryption_key}",
|
||||
]
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "falkordb"
|
||||
port = 3000
|
||||
host = "${main_domain}"
|
||||
@@ -0,0 +1,101 @@
|
||||
# Firecrawl
|
||||
|
||||
This template deploys Firecrawl `2.11.230` with authenticated API access, persistent infrastructure, browser rendering, and interactive Swagger documentation.
|
||||
|
||||
## First deployment
|
||||
|
||||
1. Create the template and keep the generated environment values unchanged.
|
||||
2. Deploy the Compose service.
|
||||
3. Wait for `firecrawl-db-seed` to finish and for `gateway` and `firecrawl` to become healthy. The first start can take several minutes.
|
||||
4. Open `https://<your-domain>/docs/` for the interactive API documentation.
|
||||
|
||||
Dokploy generates the client credential in the Environment tab:
|
||||
|
||||
```text
|
||||
FIRECRAWL_API_KEY=fc-...
|
||||
```
|
||||
|
||||
Treat this value as a secret. The template provisions it in Firecrawl's PostgreSQL authentication database automatically.
|
||||
|
||||
## Test the API
|
||||
|
||||
```bash
|
||||
curl -X POST "https://<your-domain>/v2/scrape" \
|
||||
-H "Authorization: Bearer <FIRECRAWL_API_KEY>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url":"https://docs.dokploy.com","formats":[{"type":"markdown"}]}'
|
||||
```
|
||||
|
||||
The OpenAPI document is available at:
|
||||
|
||||
```text
|
||||
https://<your-domain>/openapi.json
|
||||
```
|
||||
|
||||
## Included services
|
||||
|
||||
- `gateway`: public same-origin API and documentation gateway.
|
||||
- `firecrawl`: API plus queue, extract, indexing, NuQ, and reconciliation workers.
|
||||
- `playwright-service`: browser-rendered scraping.
|
||||
- `firecrawl-db`: authentication, request, crawl, monitoring, and application data.
|
||||
- `nuq-postgres`: durable NuQ job storage.
|
||||
- `redis`: queues, caching, and rate-limit state.
|
||||
- `rabbitmq`: extract-job transport.
|
||||
- `swagger-ui`: interactive API documentation.
|
||||
|
||||
Only the gateway is assigned a public domain. PostgreSQL, Redis, RabbitMQ, Playwright, and worker ports remain private.
|
||||
|
||||
## Capabilities
|
||||
|
||||
Scrape, batch scrape, crawl, map, parsing, browser sessions, monitoring, change tracking, and the normal non-AI API paths work without an external model provider.
|
||||
|
||||
AI-backed extraction, JSON generation, agent features, and summaries require one of these configurations in the Environment tab:
|
||||
|
||||
- `OPENAI_API_KEY`, optionally with `OPENAI_BASE_URL`
|
||||
- `OPENROUTER_API_KEY`
|
||||
- `OLLAMA_BASE_URL`
|
||||
|
||||
Set `MODEL_NAME` and `MODEL_EMBEDDING_NAME` when the provider requires explicit model names.
|
||||
|
||||
Search integrations can be configured with `SEARCHAPI_API_KEY` or `SEARXNG_ENDPOINT`. Proxy variables and LlamaParse are optional.
|
||||
|
||||
## MCP clients
|
||||
|
||||
The Firecrawl API is not itself an MCP transport. Connect the official Firecrawl MCP adapter to this deployment:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"firecrawl-selfhost": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "firecrawl-mcp"],
|
||||
"env": {
|
||||
"FIRECRAWL_API_URL": "https://<your-domain>",
|
||||
"FIRECRAWL_API_KEY": "<FIRECRAWL_API_KEY>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Persistence and backups
|
||||
|
||||
The following named volumes contain persistent state:
|
||||
|
||||
- `firecrawl-db-data`
|
||||
- `nuq-postgres-data`
|
||||
- `redis-data`
|
||||
- `rabbitmq-data`
|
||||
|
||||
Back up both PostgreSQL volumes before upgrading. Redis and RabbitMQ backups are also recommended when queued jobs must survive disaster recovery.
|
||||
|
||||
Database initialization scripts run only when `firecrawl-db-data` is empty. A newer Firecrawl image may require schema or RPC changes, so do not replace the pinned image with `latest` without reviewing migrations and testing a restored backup.
|
||||
|
||||
## Security notes
|
||||
|
||||
- Keep `ALLOW_LOCAL_WEBHOOKS=false` unless private-network webhook access is intentional.
|
||||
- Keep the generated database, Redis, RabbitMQ, and Bull secrets private.
|
||||
- Use HTTPS for the public domain.
|
||||
- The generated API key receives unrestricted self-host rate-limit overrides; protect it like an administrator credential.
|
||||
- Changing `FIRECRAWL_API_KEY` after deployment provisions another key and does not automatically revoke the old one.
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"id": "firecrawl",
|
||||
"name": "Firecrawl",
|
||||
"version": "latest",
|
||||
"description": "Firecrawl is an API service that takes a URL, crawls it, and converts it into clean markdown or structured data. It can crawl all accessible subpages and provide clean data for each.",
|
||||
"version": "2.11.230",
|
||||
"description": "Production-ready, authenticated Firecrawl with persistent queues, browser rendering, a generated API key, and interactive OpenAPI documentation.",
|
||||
"logo": "firecrawl.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/firecrawl/firecrawl",
|
||||
"website": "https://firecrawl.dev",
|
||||
"docs": "https://github.com/firecrawl/firecrawl"
|
||||
"docs": "https://docs.firecrawl.dev/contributing/self-host"
|
||||
},
|
||||
"tags": [
|
||||
"api",
|
||||
"crawler",
|
||||
"scraping",
|
||||
"data-extraction",
|
||||
"llm"
|
||||
"llm",
|
||||
"automation",
|
||||
"self-hosted"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,65 +1,61 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
openai_api_key = "${OPENAI_API_KEY}"
|
||||
openai_base_url = "${OPENAI_BASE_URL}"
|
||||
ollama_base_url = "${OLLAMA_BASE_URL}"
|
||||
model_name = "${MODEL_NAME}"
|
||||
model_embedding_name = "${MODEL_EMBEDDING_NAME}"
|
||||
proxy_server = "${PROXY_SERVER}"
|
||||
proxy_username = "${PROXY_USERNAME}"
|
||||
proxy_password = "${PROXY_PASSWORD}"
|
||||
searxng_endpoint = "${SEARXNG_ENDPOINT}"
|
||||
searxng_engines = "${SEARXNG_ENGINES}"
|
||||
searxng_categories = "${SEARXNG_CATEGORIES}"
|
||||
supabase_anon_token = "${SUPABASE_ANON_TOKEN}"
|
||||
supabase_url = "${SUPABASE_URL}"
|
||||
supabase_service_token = "${SUPABASE_SERVICE_TOKEN}"
|
||||
test_api_key = "${TEST_API_KEY}"
|
||||
bull_auth_key = "${password:32}"
|
||||
llamaparse_api_key = "${LLAMAPARSE_API_KEY}"
|
||||
slack_webhook_url = "${SLACK_WEBHOOK_URL}"
|
||||
posthog_api_key = "${POSTHOG_API_KEY}"
|
||||
posthog_host = "${POSTHOG_HOST}"
|
||||
max_cpu = "${MAX_CPU}"
|
||||
max_ram = "${MAX_RAM}"
|
||||
app_postgres_password = "${hash:32}"
|
||||
nuq_postgres_password = "${hash:32}"
|
||||
redis_password = "${hash:32}"
|
||||
rabbitmq_password = "${hash:32}"
|
||||
bull_auth_key = "${hash:32}"
|
||||
firecrawl_org_id = "${uuid}"
|
||||
firecrawl_team_id = "${uuid}"
|
||||
# Dokploy's real hash helper returns exactly N hex characters. Force the UUID
|
||||
# version and variant nibbles so Firecrawl's strict API-key parser accepts it.
|
||||
firecrawl_api_key_a = "${hash:12}"
|
||||
firecrawl_api_key_b = "${hash:3}"
|
||||
firecrawl_api_key_c = "${hash:15}"
|
||||
firecrawl_api_key = "fc-${firecrawl_api_key_a}4${firecrawl_api_key_b}8${firecrawl_api_key_c}"
|
||||
self_hosted_webhook_hmac_secret = "${hash:32}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"PORT=3002",
|
||||
"HOST=0.0.0.0",
|
||||
"USE_DB_AUTHENTICATION=false",
|
||||
"APP_POSTGRES_PASSWORD=${app_postgres_password}",
|
||||
"NUQ_POSTGRES_PASSWORD=${nuq_postgres_password}",
|
||||
"REDIS_PASSWORD=${redis_password}",
|
||||
"RABBITMQ_USER=firecrawl",
|
||||
"RABBITMQ_PASSWORD=${rabbitmq_password}",
|
||||
"BULL_AUTH_KEY=${bull_auth_key}",
|
||||
"PLAYWRIGHT_MICROSERVICE_URL=http://playwright-service:3000/scrape",
|
||||
"REDIS_URL=redis://redis:6379",
|
||||
"REDIS_RATE_LIMIT_URL=redis://redis:6379",
|
||||
"OPENAI_API_KEY=${openai_api_key}",
|
||||
"OPENAI_BASE_URL=${openai_base_url}",
|
||||
"OLLAMA_BASE_URL=${ollama_base_url}",
|
||||
"MODEL_NAME=${model_name}",
|
||||
"MODEL_EMBEDDING_NAME=${model_embedding_name}",
|
||||
"PROXY_SERVER=${proxy_server}",
|
||||
"PROXY_USERNAME=${proxy_username}",
|
||||
"PROXY_PASSWORD=${proxy_password}",
|
||||
"SEARXNG_ENDPOINT=${searxng_endpoint}",
|
||||
"SEARXNG_ENGINES=${searxng_engines}",
|
||||
"SEARXNG_CATEGORIES=${searxng_categories}",
|
||||
"SUPABASE_ANON_TOKEN=${supabase_anon_token}",
|
||||
"SUPABASE_URL=${supabase_url}",
|
||||
"SUPABASE_SERVICE_TOKEN=${supabase_service_token}",
|
||||
"TEST_API_KEY=${test_api_key}",
|
||||
"LLAMAPARSE_API_KEY=${llamaparse_api_key}",
|
||||
"SLACK_WEBHOOK_URL=${slack_webhook_url}",
|
||||
"POSTHOG_API_KEY=${posthog_api_key}",
|
||||
"POSTHOG_HOST=${posthog_host}",
|
||||
"MAX_CPU=0.8",
|
||||
"MAX_RAM=0.8"
|
||||
"FIRECRAWL_ORG_ID=${firecrawl_org_id}",
|
||||
"FIRECRAWL_TEAM_ID=${firecrawl_team_id}",
|
||||
"FIRECRAWL_API_KEY=${firecrawl_api_key}",
|
||||
"NUM_WORKERS_PER_QUEUE=8",
|
||||
"CRAWL_CONCURRENT_REQUESTS=10",
|
||||
"MAX_CONCURRENT_JOBS=5",
|
||||
"BROWSER_POOL_SIZE=5",
|
||||
"LOGGING_LEVEL=INFO",
|
||||
"DISABLE_ENGPICKER=true",
|
||||
"ALLOW_LOCAL_WEBHOOKS=false",
|
||||
"BLOCK_MEDIA=true",
|
||||
"OPENAI_API_KEY=",
|
||||
"OPENAI_BASE_URL=",
|
||||
"OPENROUTER_API_KEY=",
|
||||
"OLLAMA_BASE_URL=",
|
||||
"MODEL_NAME=",
|
||||
"MODEL_EMBEDDING_NAME=",
|
||||
"SEARCHAPI_API_KEY=",
|
||||
"SEARXNG_ENDPOINT=",
|
||||
"SEARXNG_ENGINES=",
|
||||
"SEARXNG_CATEGORIES=",
|
||||
"LLAMAPARSE_API_KEY=",
|
||||
"SLACK_WEBHOOK_URL=",
|
||||
"PROXY_SERVER=",
|
||||
"PROXY_USERNAME=",
|
||||
"PROXY_PASSWORD=",
|
||||
"SELF_HOSTED_WEBHOOK_URL=",
|
||||
"SELF_HOSTED_WEBHOOK_HMAC_SECRET=${self_hosted_webhook_hmac_secret}"
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "api"
|
||||
port = 3002
|
||||
serviceName = "gateway"
|
||||
port = 8080
|
||||
host = "${main_domain}"
|
||||
path = "/"
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
freellmapi:
|
||||
image: ghcr.io/tashfeenahmed/freellmapi:v0.8.4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: ${PORT}
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||
expose:
|
||||
- 3001
|
||||
volumes:
|
||||
- freellmapi-data:/app/server/data
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://127.0.0.1:3001/api/ping').then((res) => { if (!res.ok) process.exit(1); }).catch(() => process.exit(1));"
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 15s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
freellmapi-data:
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<circle cx="16" cy="16" r="11" fill="#ffffff"></circle>
|
||||
<circle cx="16" cy="16" r="4" fill="#10b981"></circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 201 B |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "freellmapi",
|
||||
"name": "FreeLLMAPI",
|
||||
"version": "v0.8.4",
|
||||
"description": "FreeLLMAPI aggregates free tiers from 29 LLM providers into a single OpenAI-compatible endpoint with smart routing, automatic failover, per-key rate tracking, and encrypted key storage.",
|
||||
"logo": "freellmapi.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/tashfeenahmed/freellmapi",
|
||||
"website": "https://freellmapi.co",
|
||||
"docs": "https://github.com/tashfeenahmed/freellmapi/blob/main/docs/install.md"
|
||||
},
|
||||
"tags": ["ai", "proxy", "llm", "openai-compatible", "developer-tools"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
encryption_key = "${hash:64}"
|
||||
|
||||
[config]
|
||||
[[config.domains]]
|
||||
serviceName = "freellmapi"
|
||||
port = 3001
|
||||
host = "${main_domain}"
|
||||
|
||||
[config.env]
|
||||
NODE_ENV = "production"
|
||||
PORT = "3001"
|
||||
ENCRYPTION_KEY = "${encryption_key}"
|
||||
@@ -0,0 +1,33 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: goatcounter
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
|
||||
POSTGRES_DB: goatcounter
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U goatcounter -d goatcounter"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
goatcounter:
|
||||
image: arp242/goatcounter:2.7
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
GOATCOUNTER_DB: "postgresql+postgresql://goatcounter:${DB_PASSWORD:?DB_PASSWORD is required}@postgres:5432/goatcounter?sslmode=disable"
|
||||
GOATCOUNTER_LISTEN: "0.0.0.0:8080"
|
||||
command:
|
||||
- serve
|
||||
- -automigrate
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 417 429" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><rect id="Artboard4" x="0.586" y="0" width="415.356" height="428.505" style="fill:none;"/><clipPath id="_clip1"><rect x="0.586" y="0" width="415.356" height="428.505"/></clipPath><g clip-path="url(#_clip1)"><path d="M25.399,235.075l118.517,-135.285c0,0 -124.734,-57.004 -120.995,-58.98c182.412,-96.381 370.769,214.033 370.769,214.033l-24.839,65.501c0,0 -169.954,-0.509 -192.464,-75.727" style="fill:none;stroke:#9a15a4;stroke-width:44.5px;"/><path d="M179.11,406.252c-0.044,-36.273 38.389,-117.225 38.389,-117.225" style="fill:none;stroke:#9a15a4;stroke-width:44.5px;"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "goatcounter",
|
||||
"name": "GoatCounter",
|
||||
"version": "2.7.0",
|
||||
"description": "GoatCounter is an open source web analytics platform available as a free donation-supported hosted service or self-hosted app. It aims to offer easy to use and meaningful privacy-friendly web analytics as an alternative to Google Analytics or Matomo.",
|
||||
"logo": "logo.svg",
|
||||
"links": {
|
||||
"website": "https://www.goatcounter.com/",
|
||||
"github": "https://github.com/arp242/goatcounter",
|
||||
"docs": "https://www.goatcounter.com/help/"
|
||||
},
|
||||
"tags": ["analytics", "privacy"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[variables]
|
||||
db_password = "${password:32}"
|
||||
goatcounter_domain = "${domain}"
|
||||
|
||||
[config]
|
||||
env = ["DB_PASSWORD=${db_password}"]
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "goatcounter"
|
||||
port = 8080
|
||||
host = "${goatcounter_domain}"
|
||||
https = true
|
||||
@@ -0,0 +1,32 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
instatic-init:
|
||||
image: ghcr.io/corebunch/instatic:0.0.16
|
||||
user: "0:0"
|
||||
entrypoint: ["chown", "-R", "bun:bun", "/app/storage"]
|
||||
volumes:
|
||||
- instatic-storage:/app/storage
|
||||
|
||||
instatic:
|
||||
image: ghcr.io/corebunch/instatic:0.0.16
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
instatic-init:
|
||||
condition: service_completed_successfully
|
||||
expose:
|
||||
- 3001
|
||||
volumes:
|
||||
- instatic-storage:/app/storage
|
||||
healthcheck:
|
||||
test: ["CMD", "bun", "run", "server/healthcheck.ts"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
volumes:
|
||||
instatic-storage:
|
||||
driver: local
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_2)">
|
||||
<rect width="256" height="256" fill="white"/>
|
||||
<path d="M167.667 207.333V227.167H88.3333V207.333H167.667ZM88.3333 207.333H48.6667V187.5H88.3333V207.333ZM207.333 207.333H167.667V187.5H207.333V207.333ZM48.6667 187.5H28.8333V167.667H48.6667V187.5ZM167.667 187.5H88.3333V167.667H167.667V187.5ZM227.167 187.5H207.333V167.667H227.167V187.5ZM88.3333 167.667H48.6667V147.833H88.3333V167.667ZM207.333 167.667H167.667V147.833H207.333V167.667ZM48.6667 147.833H28.8333V128H48.6667V147.833ZM167.667 48.6667H207.333V68.5H227.167V108.167H207.333V128H167.667V147.833H88.3333V128H48.6667V108.167H28.8333V68.5H48.6667V48.6667H88.3333V28.8333H167.667V48.6667ZM227.167 147.833H207.333V128H227.167V147.833Z" fill="black"/>
|
||||
<rect width="20" height="20" fill="black"/>
|
||||
<rect x="236" width="20" height="20" fill="black"/>
|
||||
<rect x="236" y="236" width="20" height="20" fill="black"/>
|
||||
<rect y="236" width="20" height="20" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_2">
|
||||
<rect width="256" height="256" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "instatic",
|
||||
"name": "Instatic",
|
||||
"version": "0.0.16",
|
||||
"description": "Instatic is a self-hosted, open-source visual CMS combining a canvas editor, content engine, and static site publisher, producing clean semantic HTML with no framework runtime shipped to visitors.",
|
||||
"logo": "instatic.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/corebunch/instatic",
|
||||
"website": "https://instatic.com",
|
||||
"docs": "https://github.com/corebunch/instatic/tree/main/docs"
|
||||
},
|
||||
"tags": ["cms", "website-builder", "static-site"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
instatic_secret_key = "${base64:32}"
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "instatic"
|
||||
port = 3001
|
||||
host = "${main_domain}"
|
||||
|
||||
[config.env]
|
||||
PORT = "3001"
|
||||
DATABASE_URL = "sqlite:/app/storage/data/cms.db"
|
||||
UPLOADS_DIR = "/app/storage/uploads"
|
||||
STATIC_DIR = "/app/dist"
|
||||
INSTATIC_SECRET_KEY = "${instatic_secret_key}"
|
||||
PUBLIC_ORIGIN = "https://${main_domain},http://${main_domain}"
|
||||
@@ -0,0 +1,41 @@
|
||||
services:
|
||||
kanbn:
|
||||
image: ghcr.io/kanbn/kan:0.6.0
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3000
|
||||
environment:
|
||||
NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}
|
||||
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
|
||||
POSTGRES_URL: ${POSTGRES_URL}
|
||||
NEXT_PUBLIC_ALLOW_CREDENTIALS: "true"
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
|
||||
migrate:
|
||||
image: ghcr.io/kanbn/kan-migrate:0.6.0
|
||||
restart: "no"
|
||||
environment:
|
||||
POSTGRES_URL: ${POSTGRES_URL}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:15
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: kan_db
|
||||
POSTGRES_USER: kan
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- kan_postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U kan -d kan_db"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
kan_postgres_data:
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="2400" height="900" viewBox="0 0 2400 900" xmlns="http://www.w3.org/2000/svg">
|
||||
<text id="kanbn" xml:space="preserve" x="239" y="681" font-family="Plus Jakarta Sans" font-size="598" font-weight="800" fill="#ededed" letter-spacing="-29.9">kan.bn</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "kanbn",
|
||||
"name": "Kanbn",
|
||||
"version": "0.6.0",
|
||||
"description": "The open-source project management alternative to Trello.",
|
||||
"logo": "logo.svg",
|
||||
"links": {
|
||||
"website": "https://kan.bn/",
|
||||
"github": "https://github.com/kanbn/kan",
|
||||
"docs": "https://docs.kan.bn/"
|
||||
},
|
||||
"tags": ["collaboration", "project-management", "kanban-board", "task-management"]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
postgres_password = "${password:32}"
|
||||
better_auth_secret = "${password:32}"
|
||||
|
||||
[config]
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "kanbn"
|
||||
port = 3000
|
||||
host = "${main_domain}"
|
||||
https = true
|
||||
|
||||
[config.env]
|
||||
NEXT_PUBLIC_BASE_URL = "https://${main_domain}"
|
||||
BETTER_AUTH_SECRET = "${better_auth_secret}"
|
||||
POSTGRES_PASSWORD = "${postgres_password}"
|
||||
POSTGRES_URL = "postgresql://kan:${postgres_password}@postgres:5432/kan_db"
|
||||
NEXT_PUBLIC_ALLOW_CREDENTIALS = "true"
|
||||
@@ -0,0 +1,19 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
kotonoba:
|
||||
image: ghcr.io/trixtylab/kotonoba:1.0.19
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DB_PATH: ${DB_PATH}
|
||||
UPLOAD_DIR: ${UPLOAD_DIR}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
SITE_URL: ${SITE_URL}
|
||||
ADMIN_DOMAIN: ${ADMIN_DOMAIN}
|
||||
volumes:
|
||||
- blog_data:/app/data
|
||||
|
||||
volumes:
|
||||
blog_data: {}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="100%" height="100%">
|
||||
<defs>
|
||||
<!-- Primary Accent Gradient (Electric Indigo to Violet) -->
|
||||
<linearGradient id="iconAccent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#818CF8" />
|
||||
<stop offset="100%" stop-color="#4F46E5" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Cyan Highlight Gradient -->
|
||||
<linearGradient id="iconCyan" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#38BDF8" />
|
||||
<stop offset="100%" stop-color="#0284C7" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Pure Standalone Vector Symbol without Background Container -->
|
||||
<g transform="translate(14, 10)">
|
||||
<!-- Top Spark Dot (Kanji 言 Radical Spark) -->
|
||||
<circle cx="80" cy="36" r="10" fill="url(#iconCyan)" />
|
||||
|
||||
<!-- Vertical Spine Pillar (CMS Core Column) -->
|
||||
<rect x="68" y="58" width="24" height="144" rx="12" fill="url(#iconAccent)" />
|
||||
|
||||
<!-- Upper Speech Bar (Horizontal Japanese Radical Stroke) -->
|
||||
<rect x="110" y="68" width="44" height="12" rx="6" fill="url(#iconCyan)" opacity="0.95" />
|
||||
|
||||
<!-- Geometric Dynamic Chevrons forming 'K' -->
|
||||
<path
|
||||
d="M 110 120
|
||||
C 110 112, 116 105, 124 101
|
||||
L 168 76
|
||||
C 178 70, 191 78, 191 89
|
||||
C 191 95, 187 101, 182 104
|
||||
L 142 126
|
||||
C 135 130, 135 136, 142 140
|
||||
L 183 162
|
||||
C 188 165, 191 171, 191 177
|
||||
C 191 188, 178 196, 168 190
|
||||
L 124 165
|
||||
C 116 161, 110 154, 110 146
|
||||
Z"
|
||||
fill="url(#iconAccent)"
|
||||
/>
|
||||
|
||||
<!-- Central Dialogue Focal Point Node -->
|
||||
<circle cx="130" cy="133" r="9" fill="url(#iconCyan)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "kotonoba",
|
||||
"name": "Kotonoba",
|
||||
"version": "1.0.19",
|
||||
"description": "A high-performance, self-hosted, multi-tenant blog CMS built with Next.js 16, SQLite, Tailwind CSS v4, and Docker.",
|
||||
"logo": "icon.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/trixtylab/kotonoba",
|
||||
"website": "https://github.com/trixtylab/kotonoba",
|
||||
"docs": "https://github.com/TrixtyLab/kotonoba/tree/master/docs"
|
||||
},
|
||||
"tags": [
|
||||
"blog",
|
||||
"i18n",
|
||||
"nextjs",
|
||||
"web"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
jwt_secret = "${password:32}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"NODE_ENV=production",
|
||||
"DB_PATH=/app/data/kotonoba.db",
|
||||
"UPLOAD_DIR=/app/data/uploads",
|
||||
"JWT_SECRET=${jwt_secret}",
|
||||
"SITE_URL=https://${main_domain}",
|
||||
"ADMIN_DOMAIN=${main_domain}"
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "kotonoba"
|
||||
port = 3000
|
||||
host = "${main_domain}"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# Langfuse self-hosted, adapted from the official docker-compose.yml:
|
||||
# https://github.com/langfuse/langfuse/blob/main/docker-compose.yml
|
||||
#
|
||||
# MinIO is exposed on its own domain because Langfuse's web UI generates
|
||||
# presigned S3 URLs that the browser fetches directly (e.g. media in
|
||||
# traces) - it cannot resolve the internal "minio" hostname.
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: docker.io/postgres:17
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: postgres
|
||||
TZ: UTC
|
||||
PGTZ: UTC
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
volumes:
|
||||
- langfuse-postgres-data:/var/lib/postgresql/data
|
||||
|
||||
clickhouse:
|
||||
image: docker.io/clickhouse/clickhouse-server:25.12
|
||||
restart: unless-stopped
|
||||
user: "101:101"
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: clickhouse
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
|
||||
healthcheck:
|
||||
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 1s
|
||||
volumes:
|
||||
- langfuse-clickhouse-data:/var/lib/clickhouse
|
||||
- langfuse-clickhouse-logs:/var/log/clickhouse-server
|
||||
|
||||
minio:
|
||||
image: cgr.dev/chainguard/minio
|
||||
restart: unless-stopped
|
||||
entrypoint: sh
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
volumes:
|
||||
- langfuse-minio-data:/data
|
||||
|
||||
redis:
|
||||
image: docker.io/redis:7
|
||||
restart: unless-stopped
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH}
|
||||
--maxmemory-policy noeviction
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
volumes:
|
||||
- langfuse-redis-data:/data
|
||||
|
||||
langfuse-worker:
|
||||
image: docker.io/langfuse/langfuse-worker:4
|
||||
restart: unless-stopped
|
||||
depends_on: &langfuse-depends-on
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
environment: &langfuse-worker-env
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL}
|
||||
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
|
||||
SALT: ${SALT}
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||
TELEMETRY_ENABLED: "true"
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: "false"
|
||||
CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
|
||||
CLICKHOUSE_URL: http://clickhouse:8123
|
||||
CLICKHOUSE_USER: clickhouse
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
|
||||
CLICKHOUSE_CLUSTER_ENABLED: "false"
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000
|
||||
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: events/
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio:9000
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: media/
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENABLED: "false"
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: "6379"
|
||||
REDIS_AUTH: ${REDIS_AUTH}
|
||||
REDIS_TLS_ENABLED: "false"
|
||||
|
||||
langfuse-web:
|
||||
image: docker.io/langfuse/langfuse:4
|
||||
restart: unless-stopped
|
||||
depends_on: *langfuse-depends-on
|
||||
environment:
|
||||
<<: *langfuse-worker-env
|
||||
# Bind Next.js to all interfaces: Docker sets HOSTNAME to the container id,
|
||||
# which resolves to a single network's IP - with Dokploy's extra proxy
|
||||
# network attached, Traefik would get connection refused (502) otherwise.
|
||||
HOSTNAME: "0.0.0.0"
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
|
||||
# Overrides the worker's internal endpoint above: the browser resolves
|
||||
# presigned media URLs itself, so this one must be publicly reachable.
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_PUBLIC_ENDPOINT}
|
||||
|
||||
volumes:
|
||||
langfuse-postgres-data:
|
||||
langfuse-clickhouse-data:
|
||||
langfuse-clickhouse-logs:
|
||||
langfuse-minio-data:
|
||||
langfuse-redis-data:
|
||||
@@ -0,0 +1,26 @@
|
||||
# Langfuse
|
||||
|
||||
Self-hosted Langfuse: open-source LLM tracing, prompt management, evaluation and cost/usage analytics, based on the official `docker-compose.yml`.
|
||||
|
||||
## Two domains
|
||||
|
||||
This template exposes two domains:
|
||||
|
||||
- The **main domain** serves the Langfuse web app/API (`langfuse-web`, port 3000).
|
||||
- The **second domain** exposes MinIO's S3 API (port 9000) directly. Langfuse's web UI generates presigned URLs for media (e.g. images attached to traces) that the *browser* fetches straight from MinIO, so this endpoint has to be publicly reachable — it isn't just an internal implementation detail.
|
||||
|
||||
## First boot
|
||||
|
||||
`langfuse-web` and `langfuse-worker` wait on Postgres, ClickHouse, Redis and MinIO to report healthy, then run their own migrations on startup. Once `langfuse-web` is up, open its domain and create the first user — they become the owner of the initial organization.
|
||||
|
||||
## After enabling HTTPS
|
||||
|
||||
By default `NEXTAUTH_URL` and `LANGFUSE_S3_MEDIA_PUBLIC_ENDPOINT` are set to `http://`. If you enable HTTPS on either domain (recommended for production), update the matching environment variable to `https://<domain>` and redeploy, otherwise auth callbacks and media URLs will point at the wrong scheme.
|
||||
|
||||
## Licensing
|
||||
|
||||
The core of Langfuse (tracing, prompt management, evaluation, playground) is open-source (MIT) and fully usable self-hosted without a license key. Some enterprise-only features (e.g. SSO/SAML, fine-grained RBAC) live under a separate commercial license — see [self-hosting license docs](https://langfuse.com/self-hosting/license-key). This template does not configure a license key.
|
||||
|
||||
## Versioning
|
||||
|
||||
Images are pinned to the `4` major tag (`langfuse/langfuse:4`, `langfuse/langfuse-worker:4`), matching the upstream compose file. To pin to a specific release instead, replace `4` with a version tag from [Docker Hub](https://hub.docker.com/r/langfuse/langfuse/tags).
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M254.75 302.25L285.25 326.75C285.25 326.75 308.587 309.418 325.75 306.875C343.75 304.208 362.954 314.244 380.75 326.208C407.629 344.279 430.25 367.208 430.25 367.208L456.75 341.208C456.75 341.208 383.686 262.047 325.75 269.208C287.75 273.905 254.75 302.25 254.75 302.25Z" fill="#FF5D5F"/>
|
||||
<path d="M80.25 151.286L55.25 178.786C55.25 178.786 124.902 243.786 179.75 243.786C204.75 243.786 239.419 224.201 269.25 198.757C286.25 184.257 305.25 167.786 324.25 167.786C337.021 167.786 353.866 174.551 369.75 192.316C369.75 192.316 380.003 186.168 386.25 181.75C391.74 177.868 399.896 171.25 399.896 171.25C377.047 146.864 343.998 129.038 324.25 130.786C292.25 130.79 269.25 150.711 240.75 173.75C212.25 196.789 200.25 206.286 179.75 206.286C145.25 206.286 80.25 151.286 80.25 151.286Z" fill="#4E9CFF"/>
|
||||
<path d="M80.25 360.75L55.25 333.25C55.25 333.25 124.902 268.25 179.75 268.25C204.75 268.25 239.419 287.835 269.25 313.279C286.25 327.779 305.25 344.25 324.25 344.25C337.083 344.25 353.799 337.207 369.75 319.25C369.75 319.25 379.339 325.161 385.25 329.25C391.328 333.455 400.25 340.407 400.25 340.407C377.39 364.987 344.1 383.007 324.25 381.25C292.25 381.246 273.25 364.289 244.75 341.25C216.25 318.211 200.25 305.75 179.75 305.75C145.25 305.75 80.25 360.75 80.25 360.75Z" fill="#4E9CFF"/>
|
||||
<path d="M406.25 213.25C399.745 217.746 389.25 224.25 389.25 224.25C389.25 224.25 395.25 237.25 395.25 254.75C395.25 272.25 389.75 287.25 389.75 287.25C389.75 287.25 399.172 293.135 405.25 297.25C411.564 301.525 421.25 308.75 421.25 308.75C421.25 308.75 432.75 284.75 432.75 254.75C432.75 224.75 421.25 202.25 421.25 202.25C421.25 202.25 412.226 209.12 406.25 213.25Z" fill="#4E9CFF"/>
|
||||
<path d="M256.25 209.25L285.25 185.25C285.25 185.25 308.587 202.04 325.75 204.583C343.75 207.25 362.954 197.214 380.75 185.25C407.629 167.179 430.25 144.25 430.25 144.25L456.75 170.25C456.75 170.25 383.686 249.411 325.75 242.25C287.75 237.553 256.25 209.25 256.25 209.25Z" fill="#FF5D5F"/>
|
||||
<path d="M186.255 130.25C223.755 130.25 255.25 162.25 255.25 162.25C255.25 162.25 246.487 169.155 240.75 173.75C234.775 178.536 225.25 186.25 225.25 186.25C225.25 186.25 208.755 168.75 186.255 168.75C177.028 168.75 165.039 174.292 152.255 185.25C142.391 193.705 132.129 204.216 125.255 217.25C119.31 228.52 116.068 241.802 115.755 255.75C115.361 273.269 121.571 291.634 131.755 306.25C138.58 316.046 146.726 323.418 155.255 329.75C166.323 337.968 177.865 343.75 186.255 343.75C195.217 343.75 203.274 340.635 209.255 337.75C218.755 332.25 226.25 325.75 226.25 325.75L255.75 350.25C255.75 350.25 243.75 362.25 227.255 371.25C216.595 376.507 202.895 381.75 186.255 381.75C169.626 381.75 150.315 372.915 132.255 359.25C120.579 350.416 109.135 339.948 100.255 327.25C85.7005 306.438 78.2004 281.118 78.2502 255.75C78.3008 230.065 86.5823 204.625 101.255 183.75C124.255 153.75 158.273 130.25 186.255 130.25Z" fill="#FF5D5F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "langfuse",
|
||||
"name": "Langfuse",
|
||||
"version": "4",
|
||||
"description": "Langfuse is an open-source LLM engineering platform for tracing, prompt management, evaluation, and cost/usage analytics of LLM applications.",
|
||||
"logo": "langfuse.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/langfuse/langfuse",
|
||||
"website": "https://langfuse.com/",
|
||||
"docs": "https://langfuse.com/self-hosting"
|
||||
},
|
||||
"tags": ["ai", "llm", "observability", "analytics", "monitoring"]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
minio_domain = "${domain}"
|
||||
postgres_password = "${password:32}"
|
||||
clickhouse_password = "${password:32}"
|
||||
minio_root_password = "${password:32}"
|
||||
redis_auth = "${password:32}"
|
||||
nextauth_secret = "${password:32}"
|
||||
salt = "${password:32}"
|
||||
encryption_key = "${hash:64}"
|
||||
nextauth_url = "http://${main_domain}"
|
||||
minio_public_endpoint = "http://${minio_domain}"
|
||||
|
||||
[config]
|
||||
[[config.domains]]
|
||||
serviceName = "langfuse-web"
|
||||
port = 3000
|
||||
host = "${main_domain}"
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "minio"
|
||||
port = 9000
|
||||
host = "${minio_domain}"
|
||||
|
||||
[config.env]
|
||||
POSTGRES_PASSWORD = "${postgres_password}"
|
||||
CLICKHOUSE_PASSWORD = "${clickhouse_password}"
|
||||
MINIO_ROOT_PASSWORD = "${minio_root_password}"
|
||||
REDIS_AUTH = "${redis_auth}"
|
||||
NEXTAUTH_SECRET = "${nextauth_secret}"
|
||||
SALT = "${salt}"
|
||||
ENCRYPTION_KEY = "${encryption_key}"
|
||||
# Set to https://<main-domain> once HTTPS is enabled on the domain
|
||||
NEXTAUTH_URL = "${nextauth_url}"
|
||||
# Set to https://<minio-domain> once HTTPS is enabled on the domain
|
||||
LANGFUSE_S3_MEDIA_PUBLIC_ENDPOINT = "${minio_public_endpoint}"
|
||||
@@ -0,0 +1,71 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
louez:
|
||||
image: synapsr/louez:2.1.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LOUEZ_MODE: standalone
|
||||
DATABASE_URL: mysql://louez:${DB_PASSWORD}@mysql:3306/louez
|
||||
AUTH_URL: https://${LOUEZ_HOST}
|
||||
AUTH_TRUST_HOST: "true"
|
||||
NEXT_PUBLIC_APP_URL: https://${LOUEZ_HOST}
|
||||
NEXT_PUBLIC_APP_DOMAIN: ${LOUEZ_HOST}
|
||||
# The bucket stays private: Louez streams images same-origin under /files
|
||||
# with its own credentials, so the store needs no domain of its own.
|
||||
S3_ENDPOINT: http://minio:9000
|
||||
S3_REGION: us-east-1
|
||||
S3_BUCKET: louez
|
||||
S3_ACCESS_KEY_ID: louez
|
||||
S3_SECRET_ACCESS_KEY: ${MINIO_PASSWORD}
|
||||
S3_PUBLIC_URL: /files
|
||||
volumes:
|
||||
# Holds the auth secret generated on first boot, so sessions survive
|
||||
# redeploys.
|
||||
- louez-data:/app/data
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_DATABASE: louez
|
||||
MYSQL_USER: louez
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
|
||||
restart: unless-stopped
|
||||
# A bucket is a top-level directory of the data drive; creating it here
|
||||
# keeps the stack to three services instead of adding a one-shot job. The
|
||||
# entrypoint is replaced because the image's own prepends `minio` to its
|
||||
# arguments, which would turn the shell into an unknown sub-command.
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command: ["mkdir -p /data/louez && exec minio server /data"]
|
||||
environment:
|
||||
MINIO_ROOT_USER: louez
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD}
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
louez-data:
|
||||
mysql-data:
|
||||
minio-data:
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="128" height="128" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#1f54dd"/>
|
||||
<path d="M10 7V25H22V21H14V7H10Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 206 B |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "louez",
|
||||
"name": "Louez",
|
||||
"version": "2.1.0",
|
||||
"description": "Open-source equipment rental platform: a branded booking storefront with live availability, plus a back office for reservations, customers, PDF contracts and revenue statistics. Ships with its database and a private image store, and installs its own schema on first boot.",
|
||||
"links": {
|
||||
"github": "https://github.com/Synapsr/Louez",
|
||||
"website": "https://louez.io",
|
||||
"docs": "https://github.com/Synapsr/Louez#readme"
|
||||
},
|
||||
"logo": "louez.svg",
|
||||
"tags": ["rental", "booking", "reservations", "inventory", "ecommerce", "business"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
|
||||
[config]
|
||||
mounts = []
|
||||
[[config.domains]]
|
||||
serviceName = "louez"
|
||||
port = 3000
|
||||
host = "${main_domain}"
|
||||
|
||||
[config.env]
|
||||
LOUEZ_HOST = "${main_domain}"
|
||||
DB_PASSWORD = "${password:32}" # Password of the database user Louez connects with
|
||||
DB_ROOT_PASSWORD = "${password:32}" # Password for MySQL root user
|
||||
MINIO_PASSWORD = "${password:32}" # Credentials Louez uses to read and write product images
|
||||
@@ -0,0 +1,47 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
my-links:
|
||||
environment:
|
||||
- APP_KEY
|
||||
- APP_URL
|
||||
- DB_PASSWORD
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_USER=mylinks
|
||||
- DB_DATABASE=mylinks
|
||||
- HOST=0.0.0.0
|
||||
- PORT=3333
|
||||
- NODE_ENV=production
|
||||
- LOG_LEVEL=info
|
||||
restart: unless-stopped
|
||||
image: sonny93/my-links:5.3.0
|
||||
expose:
|
||||
- 3333
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:3333/ >/dev/null || exit 1']
|
||||
interval: 30s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
restart: unless-stopped
|
||||
user: postgres
|
||||
environment:
|
||||
POSTGRES_USER: mylinks
|
||||
POSTGRES_DB: mylinks
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "my-links",
|
||||
"name": "MyLinks",
|
||||
"version": "5.3.0",
|
||||
"description": "Self-hostable bookmark manager to organize, search and share your favorite links.",
|
||||
"logo": "my-links.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/my-links/my-links",
|
||||
"website": "https://www.mylinks.app",
|
||||
"docs": "https://docs.mylinks.app"
|
||||
},
|
||||
"tags": ["bookmarks", "link-sharing"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 106.813 73.354 L 86.501 73.355 C 86.501 84.421 95.594 93.398 106.813 93.398 C 124.952 93.398 139.996 108.476 139.996 126.656 L 140 126.656 C 140 144.835 124.956 159.913 106.816 159.913 L 0 159.913 L 62.001 200 L 62.001 189.977 C 62.001 184.441 66.476 179.956 72 179.956 L 106.816 179.956 L 106.816 179.962 C 136.14 179.962 159.999 156.048 159.999 126.656 C 159.999 97.266 136.14 73.354 106.813 73.354 Z" fill="#3f88c5" />
|
||||
<path d="M 93.184 126.646 L 113.498 126.645 C 113.498 115.579 104.402 106.603 93.184 106.603 C 75.042 106.603 60 91.524 60 73.344 C 60 55.165 75.044 40.09 93.184 40.09 L 200 40.09 L 137.999 0 L 137.999 10.023 C 137.999 15.559 133.524 20.044 128 20.044 L 93.184 20.044 L 93.184 20.039 C 63.86 20.039 40.001 43.951 40.001 73.344 L 39.997 73.344 C 39.997 102.735 63.855 126.646 93.184 126.646 Z" fill="#3f88c5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 957 B |
@@ -0,0 +1,17 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
app_key = "${base64:32}"
|
||||
db_password = "${password}"
|
||||
|
||||
[config]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "my-links"
|
||||
port = 3_333
|
||||
host = "${main_domain}"
|
||||
|
||||
[config.env]
|
||||
APP_KEY = "${app_key}"
|
||||
APP_URL = "http://${main_domain}"
|
||||
DB_PASSWORD = "${db_password}"
|
||||
@@ -0,0 +1,31 @@
|
||||
services:
|
||||
navi-music:
|
||||
# 4.1.0 is the newest release that can boot without a YouTube OAuth2 refresh
|
||||
# token; 4.1.1+ throws at startup ("YouTube OAuth token is mandatory") unless
|
||||
# a valid token obtained via an interactive device-code flow is provided, so
|
||||
# newer tags can never boot with template defaults.
|
||||
image: anvian/navi-music:4.1.0
|
||||
restart: unless-stopped
|
||||
# NaviMusic logs (rather than throws) when DISCORD_TOKEN is empty or invalid,
|
||||
# then exits 0 because no JDA threads keep the JVM alive. Keep the container
|
||||
# running in that case so first-time users see the instructions in the logs
|
||||
# instead of a restart loop; real crashes (non-zero exits) still propagate to
|
||||
# the restart policy.
|
||||
entrypoint:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
java $$JAVA_OPTS -jar /app/NaviMusic.jar nogui --spring.profiles.active=prod;
|
||||
code=$$?;
|
||||
if [ "$$code" -eq 0 ]; then
|
||||
echo "NaviMusic is idle. Set DISCORD_TOKEN (and optionally Spotify/YouTube credentials) in the Environment tab, then redeploy.";
|
||||
tail -f /dev/null;
|
||||
fi;
|
||||
exit $$code
|
||||
environment:
|
||||
DISCORD_TOKEN: ${DISCORD_TOKEN}
|
||||
SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID}
|
||||
SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET}
|
||||
YOUTUBE_POTOKEN: ${YOUTUBE_POTOKEN}
|
||||
YOUTUBE_VISITOR: ${YOUTUBE_VISITOR}
|
||||
YOUTUBE_OAUTH2: ${YOUTUBE_OAUTH2}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "navi-music",
|
||||
"name": "NaviMusic",
|
||||
"version": "4.1.0",
|
||||
"description": "A Discord music bot that plays YouTube and Spotify tracks and playlists in voice channels.",
|
||||
"logo": "navi-music.png",
|
||||
"links": {
|
||||
"github": "https://github.com/andre-carbajal/NaviMusic",
|
||||
"website": "https://github.com/andre-carbajal/NaviMusic",
|
||||
"docs": "https://github.com/andre-carbajal/NaviMusic#configuration"
|
||||
},
|
||||
"tags": [
|
||||
"discord",
|
||||
"music",
|
||||
"bot"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,12 @@
|
||||
[config]
|
||||
env = [
|
||||
# Required: create a Discord bot and paste its token.
|
||||
"DISCORD_TOKEN=",
|
||||
# Optional but recommended for Spotify links: https://developer.spotify.com/dashboard
|
||||
"SPOTIFY_CLIENT_ID=",
|
||||
"SPOTIFY_CLIENT_SECRET=",
|
||||
# Optional YouTube tweaks (leave empty to use the default anonymous clients).
|
||||
"YOUTUBE_POTOKEN=",
|
||||
"YOUTUBE_VISITOR=",
|
||||
"YOUTUBE_OAUTH2=",
|
||||
]
|
||||
@@ -3,9 +3,17 @@ main_domain = "${domain}"
|
||||
|
||||
[config]
|
||||
env = {}
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "web"
|
||||
port = 8_069
|
||||
host = "${main_domain}"
|
||||
|
||||
[[config.mounts]]
|
||||
filePath = "config/odoo.conf"
|
||||
content = """
|
||||
[options]
|
||||
addons_path = /mnt/extra-addons
|
||||
data_dir = /var/lib/odoo
|
||||
proxy_mode = True
|
||||
"""
|
||||
|
||||
@@ -3,9 +3,17 @@ main_domain = "${domain}"
|
||||
|
||||
[config]
|
||||
env = {}
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "web"
|
||||
port = 8_069
|
||||
host = "${main_domain}"
|
||||
|
||||
[[config.mounts]]
|
||||
filePath = "config/odoo.conf"
|
||||
content = """
|
||||
[options]
|
||||
addons_path = /mnt/extra-addons
|
||||
data_dir = /var/lib/odoo
|
||||
proxy_mode = True
|
||||
"""
|
||||
|
||||
@@ -3,9 +3,17 @@ main_domain = "${domain}"
|
||||
|
||||
[config]
|
||||
env = {}
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "web"
|
||||
port = 8_069
|
||||
host = "${main_domain}"
|
||||
|
||||
[[config.mounts]]
|
||||
filePath = "config/odoo.conf"
|
||||
content = """
|
||||
[options]
|
||||
addons_path = /mnt/extra-addons
|
||||
data_dir = /var/lib/odoo
|
||||
proxy_mode = True
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
# OpenAnalytics for Dokploy's template catalogue.
|
||||
#
|
||||
# This folder is the blueprint submitted to `Dokploy/templates` as
|
||||
# `blueprints/openanalytics/`; the copy here is the source of truth and the two
|
||||
# must not drift. The operator walkthrough is `../DOKPLOY.md`.
|
||||
#
|
||||
# Derived from `../docker-compose.coolify.yml`, the tested one-click shape.
|
||||
# What changed for this platform, and nothing else did:
|
||||
#
|
||||
# 1. **Generated values arrive through the project `.env`.** Dokploy renders
|
||||
# `template.toml`'s variables into one `.env` file, and this file references
|
||||
# them with `${...}` per service. DO NOT "simplify" this into a shared
|
||||
# `env_file:`: every service has a FORBIDDEN_KEYS list and EXITS AT STARTUP
|
||||
# when handed a secret it must not hold (`packages/domain/src/env.ts`). One
|
||||
# shared env block would hand every service every secret and nothing would
|
||||
# boot. That is the design working, not a bug: the collector is
|
||||
# internet-facing and must not reach ClickHouse; the gateway verifies
|
||||
# signatures and must not hold the key that mints them.
|
||||
#
|
||||
# 2. **A Caddy edge replaces Coolify's Traefik-label middleware.** Dokploy's
|
||||
# Traefik owns TLS and writes `X-Forwarded-For` and `X-Real-Ip` from the
|
||||
# connection, but it passes `CF-Connecting-IP` and its family through,
|
||||
# because they are nobody's standard. Without stripping them, any visitor
|
||||
# picks their own rate-limit bucket or writes their own country into your
|
||||
# analytics. All four domains therefore point at `edge`, which deletes that
|
||||
# family before anything internal sees it. The Caddyfile arrives as a
|
||||
# template mount (`../files/Caddyfile`).
|
||||
#
|
||||
# 3. **`mem_limit` is gone; the ClickHouse ulimits stay.** The valkey-queue
|
||||
# memory bound still holds: `maxmemory` lives in the conf baked into the
|
||||
# image.
|
||||
#
|
||||
# ## Images
|
||||
#
|
||||
# Pinned to `v0.5.0`. The floor is `v0.4.0`, the first release whose
|
||||
# `collector` image serves `oa.js` itself — before it, the one file a customer
|
||||
# pastes into their page answered 404 on installs with no tracker builder.
|
||||
# Nothing here builds: the platform pulls nine of our images plus postgres,
|
||||
# alpine and caddy. `OA_IMAGE_TAG` in the environment tab overrides the pin;
|
||||
# move it only together with this file (the env shape and the migrations are
|
||||
# versioned with the images).
|
||||
|
||||
x-node-healthcheck: &node-healthcheck
|
||||
test:
|
||||
[
|
||||
'CMD',
|
||||
'node',
|
||||
'-e',
|
||||
"fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
||||
]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
services:
|
||||
# --- stores ---------------------------------------------------------------
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: openanalytics
|
||||
POSTGRES_DB: openanalytics
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- pg-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# Upstream's server plus our config drop-in and the entrypoint that renders
|
||||
# the four users, baked into the image (a one-click platform has no checkout
|
||||
# to bind-mount them from).
|
||||
clickhouse:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/clickhouse:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
CLICKHOUSE_INGEST_PASSWORD: ${CLICKHOUSE_INGEST_PASSWORD}
|
||||
CLICKHOUSE_READ_PASSWORD: ${CLICKHOUSE_READ_PASSWORD}
|
||||
CLICKHOUSE_MAINTENANCE_PASSWORD: ${CLICKHOUSE_MAINTENANCE_PASSWORD}
|
||||
CLICKHOUSE_MIGRATION_PASSWORD: ${CLICKHOUSE_MIGRATION_PASSWORD}
|
||||
volumes:
|
||||
- ch-data:/var/lib/clickhouse
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 262144
|
||||
hard: 262144
|
||||
healthcheck:
|
||||
test: ['CMD', 'clickhouse-client', '--query', 'SELECT 1']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 20s
|
||||
|
||||
# The durable event queue. Between the collector's 202 and the worker's
|
||||
# insert this instance holds the only copy of an event.
|
||||
valkey-queue:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/valkey:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
VALKEY_PASSWORD: ${VALKEY_QUEUE_PASSWORD}
|
||||
# One image, two policies. Durable: appendonly, no eviction. Named rather
|
||||
# than defaulted, because a default would be a guess about whether an
|
||||
# acknowledged event may be evicted before the worker inserts it.
|
||||
OA_VALKEY_CONF: /usr/local/etc/valkey/valkey-queue.conf
|
||||
volumes:
|
||||
- valkey-queue-data:/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'REDISCLI_AUTH="$$VALKEY_PASSWORD" valkey-cli ping | grep -q PONG']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# Presence, rate-limit counters, short-lived caches. Losable by design.
|
||||
valkey-realtime:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/valkey:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
VALKEY_PASSWORD: ${VALKEY_REALTIME_PASSWORD}
|
||||
# Losable by design: allkeys-lru inside a memory bound.
|
||||
OA_VALKEY_CONF: /usr/local/etc/valkey/valkey-realtime.conf
|
||||
volumes:
|
||||
- valkey-realtime-data:/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'REDISCLI_AUTH="$$VALKEY_PASSWORD" valkey-cli ping | grep -q PONG']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# --- one-shot: the three signing pairs, and the credential keyring ---------
|
||||
#
|
||||
# No platform generator can make either: a keypair whose halves must match
|
||||
# across two services is not a random string, and the keyring wants exactly
|
||||
# 32 bytes. So both are made inside the stack and handed over as files.
|
||||
#
|
||||
# Four separate key volumes rather than one, so the split between the api's
|
||||
# private halves and each verifier's public half is a filesystem fact and not
|
||||
# a convention: a single shared volume would put every private half one `cat`
|
||||
# away from every service that only verifies.
|
||||
#
|
||||
# Idempotent, and that is load-bearing: platforms re-run the whole stack on
|
||||
# every redeploy, and regenerating would rotate all three pairs behind
|
||||
# everyone's back. Each half is written only when missing, and a missing
|
||||
# public half is derived from the private one, so an interrupted volume
|
||||
# repairs itself.
|
||||
#
|
||||
# EVERY LITERAL DOLLAR IS DOUBLED. Compose substitutes dollar-variables
|
||||
# before the container sees the command, so a single-dollar `$pair` would
|
||||
# arrive as an empty string and this would write `/keys/api/.private.pem`
|
||||
# three times.
|
||||
keygen:
|
||||
image: alpine:3.21
|
||||
restart: 'no'
|
||||
command:
|
||||
- sh
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache openssl >/dev/null
|
||||
mkdir -p /keys/api /keys/gateway /keys/realtime /keyring
|
||||
for entry in "query gateway" "realtime realtime"; do
|
||||
set -- $$entry
|
||||
pair=$$1
|
||||
dest=$$2
|
||||
if [ ! -f "/keys/api/$$pair.private.pem" ]; then
|
||||
openssl genpkey -algorithm ed25519 -out "/keys/api/$$pair.private.pem"
|
||||
echo "$$pair: private half generated"
|
||||
else
|
||||
echo "$$pair: private half exists, left alone"
|
||||
fi
|
||||
if [ ! -f "/keys/$$dest/$$pair.public.pem" ]; then
|
||||
openssl pkey -in "/keys/api/$$pair.private.pem" -pubout \
|
||||
-out "/keys/$$dest/$$pair.public.pem"
|
||||
echo "$$pair: public half derived for $$dest"
|
||||
fi
|
||||
done
|
||||
# 32 bytes, which is what `CREDENTIAL_KEY_BYTES` in
|
||||
# `packages/integrations/src/credential-vault.ts` checks rather than
|
||||
# trusts. Written only when missing: rotating it on a redeploy would
|
||||
# strand every provider credential a customer had already connected.
|
||||
if [ ! -f /keyring/credential-keyring.json ]; then
|
||||
printf '{"active":"k1","keys":{"k1":"%s"}}' "$$(openssl rand -base64 32)" \
|
||||
> /keyring/credential-keyring.json
|
||||
echo "credential keyring: generated"
|
||||
else
|
||||
echo "credential keyring: exists, left alone"
|
||||
fi
|
||||
# uid 1000 is `node` in the runtime image, which is the user every
|
||||
# service runs as.
|
||||
chown -R 1000:1000 /keys /keyring
|
||||
chmod 700 /keys/api /keys/gateway /keys/realtime /keyring
|
||||
chmod 400 /keys/api/*.pem /keyring/credential-keyring.json
|
||||
chmod 444 /keys/gateway/*.pem /keys/realtime/*.pem
|
||||
ls -l /keys/api /keys/gateway /keys/realtime /keyring
|
||||
volumes:
|
||||
- keys-api:/keys/api
|
||||
- keys-gateway:/keys/gateway
|
||||
- keys-realtime:/keys/realtime
|
||||
- keyring:/keyring
|
||||
|
||||
# --- one-shot: the geo database -------------------------------------------
|
||||
#
|
||||
# DB-IP City Lite and not MaxMind, and the licence is the whole reason:
|
||||
# GeoLite2 needs an account and a licence key and forbids redistribution, so
|
||||
# nothing could fetch it on your behalf. DB-IP City Lite is CC BY 4.0, a
|
||||
# direct download. The attribution is a licence condition rather than a
|
||||
# courtesy, and it belongs wherever you show the data:
|
||||
# "IP Geolocation by DB-IP, https://db-ip.com, CC BY 4.0."
|
||||
#
|
||||
# The network is a first-install dependency only: the file is written once
|
||||
# and every later deploy short-circuits on the first line. To refresh it,
|
||||
# delete it from the volume and redeploy. A failed download fails the deploy,
|
||||
# which is the lesser evil: the collector refuses to start on a `GEOIP_DB_PATH`
|
||||
# that names no file, so the alternative is a crash loop one step further
|
||||
# from its cause.
|
||||
geoip:
|
||||
image: alpine:3.21
|
||||
restart: 'no'
|
||||
command:
|
||||
- sh
|
||||
- -ec
|
||||
- |
|
||||
if [ -f /geoip/dbip-city-lite.mmdb ]; then
|
||||
echo "geoip: database present, left alone"
|
||||
exit 0
|
||||
fi
|
||||
apk add --no-cache curl >/dev/null
|
||||
|
||||
# Two candidates. A month's build appears a day or two into it, so on the
|
||||
# first of the month the current one is a 404 rather than an error.
|
||||
# Written out rather than `date -d '1 month ago'`, which busybox does not
|
||||
# take. Leading zeros are stripped because POSIX arithmetic reads `08` as
|
||||
# octal and refuses it.
|
||||
y=$$(date -u +%Y)
|
||||
m=$$(date -u +%m | sed "s/^0*//")
|
||||
pm=$$((m - 1))
|
||||
py=$$y
|
||||
if [ "$$pm" -le 0 ]; then pm=12; py=$$((y - 1)); fi
|
||||
prev=$$(printf "%04d-%02d" "$$py" "$$pm")
|
||||
|
||||
fetched=""
|
||||
for month in "$$(date -u +%Y-%m)" "$$prev"; do
|
||||
echo "geoip: trying $$month"
|
||||
# --fail so a 404 is a non-zero exit rather than an HTML error page
|
||||
# saved as a .gz, which would fail later inside gzip with nothing
|
||||
# pointing here.
|
||||
if curl -fsSL --retry 2 --max-time 300 -o /tmp/dbip.gz \
|
||||
"https://download.db-ip.com/free/dbip-city-lite-$$month.mmdb.gz"; then
|
||||
fetched="$$month"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$$fetched" ]; then
|
||||
echo "geoip: no build available. Check https://db-ip.com/db/download/ip-to-city-lite" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gzip -dc /tmp/dbip.gz > /tmp/dbip.mmdb
|
||||
# Verified before it is moved into place. Every MaxMind-format file ends
|
||||
# with the metadata marker; `strings` and not `grep -a`, because
|
||||
# busybox's `-a` does not match inside this file.
|
||||
if ! tail -c 200000 /tmp/dbip.mmdb | strings | grep -q "MaxMind.com"; then
|
||||
echo "geoip: the download is not a MaxMind-format database" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Moved last, so an interrupted run leaves nothing half-written.
|
||||
mv /tmp/dbip.mmdb /geoip/dbip-city-lite.mmdb
|
||||
chown 1000:1000 /geoip/dbip-city-lite.mmdb
|
||||
chmod 444 /geoip/dbip-city-lite.mmdb
|
||||
echo "geoip: wrote dbip-city-lite.mmdb ($$fetched, $$(du -h /geoip/dbip-city-lite.mmdb | cut -f1))"
|
||||
echo "geoip: IP Geolocation by DB-IP, https://db-ip.com, CC BY 4.0."
|
||||
volumes:
|
||||
- geoip:/geoip
|
||||
|
||||
# --- one-shot: schemas ----------------------------------------------------
|
||||
#
|
||||
# Both stores build from empty. Re-runs are safe and are the upgrade path:
|
||||
# both runners keep a ledger and apply only what is pending.
|
||||
migrate:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/migrate:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: 'no'
|
||||
# Required, not decoration: this is the worker's image under another tag,
|
||||
# so without an entrypoint of its own it boots as the worker and is refused
|
||||
# by the least-privilege boundary. The two CLIs are what a migration is.
|
||||
entrypoint:
|
||||
['sh', '-c', 'node packages/postgres/dist/cli.js && node packages/clickhouse/dist/cli.js']
|
||||
environment:
|
||||
ENVIRONMENT: production
|
||||
POSTGRES_MIGRATION_URL: postgres://openanalytics:${POSTGRES_PASSWORD}@postgres:5432/openanalytics
|
||||
CLICKHOUSE_URL: http://clickhouse:8123
|
||||
# `analytics`, and it cannot be anything else: the ClickHouse entrypoint
|
||||
# writes every grant against `analytics.<table>` by name, so a database
|
||||
# called anything else comes up empty of privileges. Note the spelling
|
||||
# too: the migration CLI reads CLICKHOUSE_DATABASE while the worker and
|
||||
# the gateway read CLICKHOUSE_DB.
|
||||
CLICKHOUSE_DATABASE: analytics
|
||||
CLICKHOUSE_MIGRATION_USER: oa_migration
|
||||
CLICKHOUSE_MIGRATION_PASSWORD: ${CLICKHOUSE_MIGRATION_PASSWORD}
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
clickhouse: { condition: service_healthy }
|
||||
|
||||
# --- application services --------------------------------------------------
|
||||
|
||||
# The only process allowed to read ClickHouse. Holds the PUBLIC half of the
|
||||
# query pair and nothing else: it verifies signed query envelopes and can
|
||||
# never mint a request it would accept.
|
||||
gateway:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/query-gateway:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ENVIRONMENT: production
|
||||
LOG_LEVEL: info
|
||||
PORT: 8081
|
||||
CLICKHOUSE_URL: http://clickhouse:8123
|
||||
CLICKHOUSE_DB: analytics
|
||||
CLICKHOUSE_READ_USER: oa_read
|
||||
CLICKHOUSE_READ_PASSWORD: ${CLICKHOUSE_READ_PASSWORD}
|
||||
REALTIME_CACHE_REDIS_URL: redis://:${VALKEY_REALTIME_PASSWORD}@valkey-realtime:6379
|
||||
QUERY_SIGNING_KEY_ID: oa-selfhost-1
|
||||
QUERY_SIGNING_PUBLIC_KEY_FILE: /keys/query.public.pem
|
||||
volumes:
|
||||
- keys-gateway:/keys:ro
|
||||
healthcheck: *node-healthcheck
|
||||
depends_on:
|
||||
migrate: { condition: service_completed_successfully }
|
||||
keygen: { condition: service_completed_successfully }
|
||||
valkey-realtime: { condition: service_healthy }
|
||||
|
||||
# Control plane, and the only holder of all three private halves. Started
|
||||
# after the gateway: the api sends a field on every gateway query that an
|
||||
# older gateway rejects, so gateway-first has no broken window at all.
|
||||
api:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/api:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ENVIRONMENT: production
|
||||
LOG_LEVEL: info
|
||||
PORT: 8082
|
||||
DATABASE_URL: postgres://openanalytics:${POSTGRES_PASSWORD}@postgres:5432/openanalytics
|
||||
REALTIME_CACHE_REDIS_URL: redis://:${VALKEY_REALTIME_PASSWORD}@valkey-realtime:6379
|
||||
QUERY_GATEWAY_URL: http://gateway:8081
|
||||
AUTH_BASE_URL: https://${API_DOMAIN}
|
||||
APP_BASE_URL: https://${APP_DOMAIN}
|
||||
COLLECTOR_BASE_URL: https://${COLLECTOR_DOMAIN}
|
||||
AUTH_TRUSTED_ORIGINS: https://${APP_DOMAIN}
|
||||
PRODUCT_NAME: Open Analytics
|
||||
AUTH_SECRET: ${AUTH_SECRET}
|
||||
TRIAL_IDENTITY_SECRET: ${TRIAL_IDENTITY_SECRET}
|
||||
CREDENTIAL_SOURCE_SECRET: ${CREDENTIAL_SOURCE_SECRET}
|
||||
# A path rather than the value: no platform generator makes the 32 bytes
|
||||
# this wants. `keygen` writes it; see the note there.
|
||||
OA_CREDENTIAL_KEYRING_FILE: /keyring/credential-keyring.json
|
||||
QUERY_SIGNING_KEY_ID: oa-selfhost-1
|
||||
# The default is `disabled`, written for the hosted product. The
|
||||
# first-run "create the first account" screen depends on it being on.
|
||||
AUTH_PASSWORD_SIGNIN: enabled
|
||||
QUERY_SIGNING_PRIVATE_KEY_FILE: /keys/query.private.pem
|
||||
REALTIME_TOKEN_SIGNING_KEY_FILE: /keys/realtime.private.pem
|
||||
volumes:
|
||||
- keys-api:/keys:ro
|
||||
- keyring:/keyring:ro
|
||||
healthcheck: *node-healthcheck
|
||||
depends_on:
|
||||
migrate: { condition: service_completed_successfully }
|
||||
keygen: { condition: service_completed_successfully }
|
||||
gateway: { condition: service_healthy }
|
||||
valkey-realtime: { condition: service_healthy }
|
||||
|
||||
# Ingest, and the one service that ever sees a raw visitor IP. It also
|
||||
# serves `oa.js` from the copy baked into its own image.
|
||||
collector:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/collector:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ENVIRONMENT: production
|
||||
LOG_LEVEL: info
|
||||
PORT: 8083
|
||||
DATABASE_URL: postgres://openanalytics:${POSTGRES_PASSWORD}@postgres:5432/openanalytics
|
||||
EVENT_STREAM_REDIS_URL: redis://:${VALKEY_QUEUE_PASSWORD}@valkey-queue:6379
|
||||
REALTIME_CACHE_REDIS_URL: redis://:${VALKEY_REALTIME_PASSWORD}@valkey-realtime:6379
|
||||
ANONYMOUS_IDENTITY_SECRET: ${ANONYMOUS_IDENTITY_SECRET}
|
||||
ANONYMOUS_IDENTITY_KEY_VERSION: 1
|
||||
# Set, because the `geoip` one-shot guarantees the file. To use MaxMind
|
||||
# instead, or a newer build: copy your file into the volume and point
|
||||
# this at it.
|
||||
GEOIP_DB_PATH: /geoip/dbip-city-lite.mmdb
|
||||
volumes:
|
||||
# Not read-only: `docker cp` into the running container is the documented
|
||||
# way to refresh the database or swap in MaxMind.
|
||||
- geoip:/geoip
|
||||
healthcheck: *node-healthcheck
|
||||
depends_on:
|
||||
migrate: { condition: service_completed_successfully }
|
||||
keygen: { condition: service_completed_successfully }
|
||||
geoip: { condition: service_completed_successfully }
|
||||
valkey-queue: { condition: service_healthy }
|
||||
valkey-realtime: { condition: service_healthy }
|
||||
|
||||
# The SSE stream behind the live dashboard. Verifies short-lived tokens the
|
||||
# api mints, and holds the PUBLIC half only.
|
||||
realtime:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/realtime:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ENVIRONMENT: production
|
||||
LOG_LEVEL: info
|
||||
PORT: 8084
|
||||
REALTIME_CACHE_REDIS_URL: redis://:${VALKEY_REALTIME_PASSWORD}@valkey-realtime:6379
|
||||
REALTIME_TOKEN_VERIFY_KEY_FILE: /keys/realtime.public.pem
|
||||
volumes:
|
||||
- keys-realtime:/keys:ro
|
||||
healthcheck: *node-healthcheck
|
||||
depends_on:
|
||||
migrate: { condition: service_completed_successfully }
|
||||
keygen: { condition: service_completed_successfully }
|
||||
valkey-realtime: { condition: service_healthy }
|
||||
|
||||
# Drains the queue into ClickHouse; sessions, rollups, exports, mail,
|
||||
# deletions. Never public.
|
||||
worker:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/worker:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ENVIRONMENT: production
|
||||
LOG_LEVEL: info
|
||||
PORT: 8085
|
||||
DATABASE_URL: postgres://openanalytics:${POSTGRES_PASSWORD}@postgres:5432/openanalytics
|
||||
EVENT_STREAM_REDIS_URL: redis://:${VALKEY_QUEUE_PASSWORD}@valkey-queue:6379
|
||||
REALTIME_CACHE_REDIS_URL: redis://:${VALKEY_REALTIME_PASSWORD}@valkey-realtime:6379
|
||||
CLICKHOUSE_URL: http://clickhouse:8123
|
||||
CLICKHOUSE_DB: analytics
|
||||
CLICKHOUSE_INGEST_USER: oa_ingest
|
||||
CLICKHOUSE_INGEST_PASSWORD: ${CLICKHOUSE_INGEST_PASSWORD}
|
||||
CLICKHOUSE_MAINTENANCE_USER: oa_maintenance
|
||||
CLICKHOUSE_MAINTENANCE_PASSWORD: ${CLICKHOUSE_MAINTENANCE_PASSWORD}
|
||||
ANONYMOUS_IDENTITY_SECRET: ${ANONYMOUS_IDENTITY_SECRET}
|
||||
ANONYMOUS_IDENTITY_KEY_VERSION: 1
|
||||
# A path rather than the value: `keygen` writes it; see the note there.
|
||||
OA_CREDENTIAL_KEYRING_FILE: /keyring/credential-keyring.json
|
||||
PRODUCT_NAME: Open Analytics
|
||||
# Only a from-address, and nothing sends until a transport is configured
|
||||
# under Account then Deployment. The schema wants at least three
|
||||
# characters, so an empty value would be a crash loop caused by a
|
||||
# variable nothing told the operator was required.
|
||||
EMAIL_FROM: ${OA_EMAIL_FROM:-Open Analytics <analytics@analytics.example>}
|
||||
# The worker decrypts what the api encrypted, so it holds the same keyring
|
||||
# and reads it from the same file.
|
||||
volumes:
|
||||
- keyring:/keyring:ro
|
||||
healthcheck: *node-healthcheck
|
||||
depends_on:
|
||||
migrate: { condition: service_completed_successfully }
|
||||
keygen: { condition: service_completed_successfully }
|
||||
valkey-queue: { condition: service_healthy }
|
||||
valkey-realtime: { condition: service_healthy }
|
||||
|
||||
# The dashboard. The published image is built against placeholder origins and
|
||||
# its entrypoint substitutes the three `NEXT_PUBLIC_*` values below at every
|
||||
# container start, which is what lets one image serve any deployment.
|
||||
web:
|
||||
image: ${OA_IMAGE_REPO:-ghcr.io/openlabs-so/openanalytics}/web:${OA_IMAGE_TAG:-v0.5.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
NEXT_PUBLIC_API_URL: https://${API_DOMAIN}
|
||||
NEXT_PUBLIC_REALTIME_URL: https://${REALTIME_DOMAIN}
|
||||
NEXT_PUBLIC_COLLECTOR_URL: https://${COLLECTOR_DOMAIN}
|
||||
# Its own healthcheck, not the shared one: the dashboard has no `/health`
|
||||
# route, and `/login` renders without a session, so a 200 means serving.
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
'CMD',
|
||||
'node',
|
||||
'-e',
|
||||
"fetch('http://127.0.0.1:3000/login').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
||||
]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
api: { condition: service_healthy }
|
||||
|
||||
# --- edge ------------------------------------------------------------------
|
||||
|
||||
# All four public hostnames land here (see `template.toml`), and this is the
|
||||
# one job Dokploy's Traefik is not configured for: deleting the
|
||||
# client-settable identity headers before anything internal reads them. The
|
||||
# Caddyfile arrives as a template mount. Do not point the domains at the
|
||||
# services directly: that would ship the stack without its header hygiene.
|
||||
edge:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
OA_APP_HOST: ${APP_DOMAIN}
|
||||
OA_API_HOST: ${API_DOMAIN}
|
||||
OA_COLLECTOR_HOST: ${COLLECTOR_DOMAIN}
|
||||
OA_REALTIME_HOST: ${REALTIME_DOMAIN}
|
||||
volumes:
|
||||
- ../files/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
expose:
|
||||
- '80'
|
||||
depends_on:
|
||||
web: { condition: service_started }
|
||||
api: { condition: service_started }
|
||||
collector: { condition: service_started }
|
||||
realtime: { condition: service_started }
|
||||
|
||||
volumes:
|
||||
pg-data:
|
||||
ch-data:
|
||||
valkey-queue-data:
|
||||
valkey-realtime-data:
|
||||
keys-api:
|
||||
keys-gateway:
|
||||
keys-realtime:
|
||||
keyring:
|
||||
geoip:
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#296FF0">
|
||||
<title>Open Analytics</title>
|
||||
<clipPath id="ring">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M256 4a252 252 0 1 0 0 504 252 252 0 1 0 0-504Zm0 109a141 141 0 1 1 0 282 141 141 0 1 1 0-282Z"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#ring)">
|
||||
<rect x="0" y="4" width="512" height="20"/>
|
||||
<rect x="0" y="36" width="512" height="20"/>
|
||||
<rect x="0" y="68" width="512" height="20"/>
|
||||
<rect x="0" y="100" width="512" height="20"/>
|
||||
<rect x="0" y="132" width="512" height="20"/>
|
||||
<rect x="0" y="164" width="512" height="20"/>
|
||||
<rect x="0" y="196" width="512" height="20"/>
|
||||
<rect x="0" y="228" width="512" height="20"/>
|
||||
<rect x="0" y="260" width="512" height="20"/>
|
||||
<rect x="0" y="292" width="512" height="20"/>
|
||||
<rect x="0" y="324" width="512" height="188"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 888 B |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "openanalytics",
|
||||
"name": "Open Analytics",
|
||||
"version": "v0.5.0",
|
||||
"description": "Open Analytics is an open-source, AI-native web analytics platform: realtime dashboards, funnels, visitor journeys, custom events, revenue analytics, and an MCP server for AI agents. Deploys the complete official self-hosted stack.",
|
||||
"logo": "logo.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/OpenLabs-so/openanalytics",
|
||||
"website": "https://getopen.so",
|
||||
"docs": "https://github.com/OpenLabs-so/openanalytics/blob/main/infra/selfhost/DOKPLOY.md"
|
||||
},
|
||||
"tags": ["analytics"]
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
# OpenAnalytics for Dokploy. Four hostnames, not four paths: the dashboard,
|
||||
# the api, the collector and the realtime stream are separate origins, and all
|
||||
# four route through the `edge` service below, which strips the
|
||||
# client-settable identity headers (see the mounted Caddyfile).
|
||||
#
|
||||
# After deploying, replace the four generated domains with real DNS records in
|
||||
# the Domains tab AND update the matching *_DOMAIN environment variables to
|
||||
# the same values, then redeploy: the api builds auth origins, CORS and every
|
||||
# link it hands out from those variables, not from the Domains tab.
|
||||
|
||||
[variables]
|
||||
app_domain = "${domain}"
|
||||
api_domain = "${domain}"
|
||||
collector_domain = "${domain}"
|
||||
realtime_domain = "${domain}"
|
||||
postgres_password = "${password:32}"
|
||||
clickhouse_ingest_password = "${password:32}"
|
||||
clickhouse_read_password = "${password:32}"
|
||||
clickhouse_maintenance_password = "${password:32}"
|
||||
clickhouse_migration_password = "${password:32}"
|
||||
valkey_queue_password = "${password:32}"
|
||||
valkey_realtime_password = "${password:32}"
|
||||
auth_secret = "${base64:64}"
|
||||
trial_identity_secret = "${base64:64}"
|
||||
credential_source_secret = "${base64:64}"
|
||||
anonymous_identity_secret = "${base64:64}"
|
||||
|
||||
[config]
|
||||
|
||||
# All four domains point at the Caddy edge on purpose. Pointing one at its
|
||||
# service directly would skip the header hygiene the edge exists for.
|
||||
[[config.domains]]
|
||||
serviceName = "edge"
|
||||
port = 80
|
||||
host = "${app_domain}"
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "edge"
|
||||
port = 80
|
||||
host = "${api_domain}"
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "edge"
|
||||
port = 80
|
||||
host = "${collector_domain}"
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "edge"
|
||||
port = 80
|
||||
host = "${realtime_domain}"
|
||||
|
||||
[config.env]
|
||||
# The four public origins. The compose file derives AUTH_BASE_URL,
|
||||
# AUTH_TRUSTED_ORIGINS and the dashboard's NEXT_PUBLIC_* values from these,
|
||||
# so they must always equal the domains served above.
|
||||
APP_DOMAIN = "${app_domain}"
|
||||
API_DOMAIN = "${api_domain}"
|
||||
COLLECTOR_DOMAIN = "${collector_domain}"
|
||||
REALTIME_DOMAIN = "${realtime_domain}"
|
||||
|
||||
# Store credentials. Each service receives only the ones it is allowed to
|
||||
# hold; the compose file interpolates them per service on purpose.
|
||||
POSTGRES_PASSWORD = "${postgres_password}"
|
||||
CLICKHOUSE_INGEST_PASSWORD = "${clickhouse_ingest_password}"
|
||||
CLICKHOUSE_READ_PASSWORD = "${clickhouse_read_password}"
|
||||
CLICKHOUSE_MAINTENANCE_PASSWORD = "${clickhouse_maintenance_password}"
|
||||
CLICKHOUSE_MIGRATION_PASSWORD = "${clickhouse_migration_password}"
|
||||
VALKEY_QUEUE_PASSWORD = "${valkey_queue_password}"
|
||||
VALKEY_REALTIME_PASSWORD = "${valkey_realtime_password}"
|
||||
|
||||
# Application secrets. The two signing key pairs and the credential keyring
|
||||
# are deliberately NOT here: the `keygen` one-shot generates them inside the
|
||||
# stack, because no platform generator can produce a keypair whose halves
|
||||
# must match across two services.
|
||||
AUTH_SECRET = "${auth_secret}"
|
||||
TRIAL_IDENTITY_SECRET = "${trial_identity_secret}"
|
||||
CREDENTIAL_SOURCE_SECRET = "${credential_source_secret}"
|
||||
ANONYMOUS_IDENTITY_SECRET = "${anonymous_identity_secret}"
|
||||
|
||||
# The sender address on outgoing mail. Only a from-address: nothing sends
|
||||
# until a transport is configured in the dashboard under Account then
|
||||
# Deployment. Replace it when you set that up.
|
||||
OA_EMAIL_FROM = "Open Analytics <analytics@analytics.example>"
|
||||
|
||||
# The edge, behind Dokploy's Traefik.
|
||||
#
|
||||
# Traefik terminates TLS, routes all four hostnames here, and writes
|
||||
# X-Forwarded-For and X-Real-Ip from the connection. What it does not do is
|
||||
# touch CF-Connecting-IP and its family, because they are nobody's standard —
|
||||
# and the collector reads the visitor's address and geo from exactly that
|
||||
# family of headers. Without the deletions below, any visitor could pick their
|
||||
# own rate-limit bucket or write their own country into the analytics.
|
||||
#
|
||||
# X-Real-IP is deliberately NOT asserted here: the proxy in front of this one
|
||||
# owns it. Asserting it from this hop would stamp Traefik's address on every
|
||||
# visitor.
|
||||
[[config.mounts]]
|
||||
filePath = "/Caddyfile"
|
||||
content = """
|
||||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
(client_identity_hygiene) {
|
||||
header_up -Fly-Client-IP
|
||||
header_up -CF-Connecting-IP
|
||||
header_up -True-Client-IP
|
||||
header_up -X-Vercel-IP-Country
|
||||
header_up -X-Vercel-IP-City
|
||||
header_up -Fly-Client-Country
|
||||
header_up -CF-IPCountry
|
||||
header_up -CF-IPCity
|
||||
}
|
||||
|
||||
:80 {
|
||||
@web host {$OA_APP_HOST}
|
||||
handle @web {
|
||||
reverse_proxy web:3000
|
||||
}
|
||||
|
||||
@api host {$OA_API_HOST}
|
||||
handle @api {
|
||||
reverse_proxy api:8082 {
|
||||
import client_identity_hygiene
|
||||
}
|
||||
}
|
||||
|
||||
@collector host {$OA_COLLECTOR_HOST}
|
||||
handle @collector {
|
||||
reverse_proxy collector:8083 {
|
||||
import client_identity_hygiene
|
||||
}
|
||||
}
|
||||
|
||||
@realtime host {$OA_REALTIME_HOST}
|
||||
handle @realtime {
|
||||
reverse_proxy realtime:8084 {
|
||||
import client_identity_hygiene
|
||||
}
|
||||
}
|
||||
|
||||
respond 404
|
||||
}
|
||||
"""
|
||||
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
openseo:
|
||||
image: ghcr.io/every-app/open-seo:v0.1.6
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- CLOUDFLARE_INCLUDE_PROCESS_ENV=true
|
||||
- PORT=3001
|
||||
- ALLOWED_HOST=${OPENSEO_HOST}
|
||||
- AUTH_MODE=local_noauth
|
||||
- DATAFORSEO_API_KEY=${DATAFORSEO_API_KEY}
|
||||
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
||||
- OPENROUTER_MODEL=${OPENROUTER_MODEL}
|
||||
- OPENSEO_TELEMETRY_DISABLED=${OPENSEO_TELEMETRY_DISABLED}
|
||||
expose:
|
||||
- 3001
|
||||
volumes:
|
||||
- openseo-data:/app/.wrangler
|
||||
volumes:
|
||||
openseo-data: {}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "openseo",
|
||||
"name": "OpenSEO",
|
||||
"version": "0.1.6",
|
||||
"description": "OpenSEO is an open source, self-hosted alternative to Semrush and Ahrefs for keyword research, rank tracking, backlinks, and site audits.",
|
||||
"logo": "openseo.png",
|
||||
"links": {
|
||||
"github": "https://github.com/every-app/open-seo",
|
||||
"website": "https://openseo.so",
|
||||
"docs": "https://openseo.so/docs/self-hosting"
|
||||
},
|
||||
"tags": [
|
||||
"seo",
|
||||
"analytics",
|
||||
"marketing"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 564 KiB |
@@ -0,0 +1,15 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
|
||||
[config]
|
||||
[[config.domains]]
|
||||
serviceName = "openseo"
|
||||
port = 3001
|
||||
host = "${main_domain}"
|
||||
|
||||
[config.env]
|
||||
OPENSEO_HOST = "${main_domain}"
|
||||
DATAFORSEO_API_KEY = ""
|
||||
OPENROUTER_API_KEY = ""
|
||||
OPENROUTER_MODEL = "openai/gpt-4o-mini"
|
||||
OPENSEO_TELEMETRY_DISABLED = ""
|
||||
@@ -0,0 +1,197 @@
|
||||
services:
|
||||
libsql:
|
||||
image: ghcr.io/tursodatabase/libsql-server:v0.24.33
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8080
|
||||
volumes:
|
||||
- libsql-data:/var/lib/sqld
|
||||
environment:
|
||||
- SQLD_NODE=primary
|
||||
# Increase limits to handle multiple services without connection pooling
|
||||
- SQLD_MAX_CONCURRENT_CONNECTIONS=512
|
||||
- SQLD_MAX_CONCURRENT_REQUESTS=1024
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
'perl -e ''use IO::Socket::INET; exit(IO::Socket::INET->new(PeerAddr=>"127.0.0.1:8080",Timeout=>1) ? 0 : 1);''',
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
# One-shot migration runner (idempotent — drizzle skips applied migrations).
|
||||
# No published image exists for it upstream, so it is built from the pinned
|
||||
# commit the ghcr.io app images were built from.
|
||||
db-migrate:
|
||||
build:
|
||||
context: https://github.com/openstatusHQ/openstatus.git#e1a98255e39c9cc9f6d4853dbce30c3f6c42514a
|
||||
dockerfile: packages/db/Dockerfile
|
||||
restart: on-failure:3
|
||||
environment:
|
||||
- DATABASE_URL=http://libsql:8080
|
||||
- DATABASE_AUTH_TOKEN=
|
||||
depends_on:
|
||||
libsql:
|
||||
condition: service_healthy
|
||||
|
||||
workflows:
|
||||
image: ghcr.io/openstatushq/openstatus-workflows:e1a9825
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3000
|
||||
volumes:
|
||||
- workflows-data:/app/data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DATABASE_URL=http://libsql:8080
|
||||
- DATABASE_AUTH_TOKEN=
|
||||
- CRON_SECRET=${CRON_SECRET}
|
||||
- RESEND_API_KEY=${RESEND_API_KEY:-re_selfhost_placeholder}
|
||||
- SITE_URL=https://${DASHBOARD_HOST}
|
||||
- TINYBIRD_NOOP=true
|
||||
depends_on:
|
||||
db-migrate:
|
||||
condition: service_completed_successfully
|
||||
libsql:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/ping"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
server:
|
||||
image: ghcr.io/openstatushq/openstatus-server:e1a9825
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3000
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DATABASE_URL=http://libsql:8080
|
||||
- DATABASE_AUTH_TOKEN=
|
||||
- CRON_SECRET=${CRON_SECRET}
|
||||
- RESEND_API_KEY=${RESEND_API_KEY:-re_selfhost_placeholder}
|
||||
- SUPER_ADMIN_TOKEN=${SUPER_ADMIN_TOKEN}
|
||||
- SELF_HOST=true
|
||||
- FLY_REGION=self-hosted
|
||||
- TINYBIRD_NOOP=true
|
||||
- UPSTASH_REDIS_REST_URL=http://localhost:6379
|
||||
- UPSTASH_REDIS_REST_TOKEN=placeholder
|
||||
depends_on:
|
||||
db-migrate:
|
||||
condition: service_completed_successfully
|
||||
workflows:
|
||||
condition: service_healthy
|
||||
libsql:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/ping"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
dashboard:
|
||||
image: ghcr.io/openstatushq/openstatus-dashboard:e1a9825
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3000
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- HOSTNAME=0.0.0.0
|
||||
- AUTH_TRUST_HOST=true
|
||||
- DATABASE_URL=http://libsql:8080
|
||||
- DATABASE_AUTH_TOKEN=
|
||||
- AUTH_SECRET=${AUTH_SECRET}
|
||||
- SELF_HOST=true
|
||||
- NEXT_PUBLIC_URL=https://${DASHBOARD_HOST}
|
||||
- CRON_SECRET=${CRON_SECRET}
|
||||
- RESEND_API_KEY=${RESEND_API_KEY:-re_selfhost_placeholder}
|
||||
- TINYBIRD_NOOP=true
|
||||
- UPSTASH_REDIS_REST_URL=http://localhost:6379
|
||||
- UPSTASH_REDIS_REST_TOKEN=placeholder
|
||||
depends_on:
|
||||
db-migrate:
|
||||
condition: service_completed_successfully
|
||||
workflows:
|
||||
condition: service_healthy
|
||||
libsql:
|
||||
condition: service_healthy
|
||||
server:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 45s
|
||||
|
||||
status-page:
|
||||
image: ghcr.io/openstatushq/openstatus-status-page:e1a9825
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3000
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- HOSTNAME=0.0.0.0
|
||||
- AUTH_TRUST_HOST=true
|
||||
- DATABASE_URL=http://libsql:8080
|
||||
- DATABASE_AUTH_TOKEN=
|
||||
- AUTH_SECRET=${AUTH_SECRET}
|
||||
- SELF_HOST=true
|
||||
- NEXT_PUBLIC_URL=https://${DASHBOARD_HOST}
|
||||
- CRON_SECRET=${CRON_SECRET}
|
||||
- RESEND_API_KEY=${RESEND_API_KEY:-re_selfhost_placeholder}
|
||||
- TINYBIRD_NOOP=true
|
||||
depends_on:
|
||||
db-migrate:
|
||||
condition: service_completed_successfully
|
||||
workflows:
|
||||
condition: service_healthy
|
||||
libsql:
|
||||
condition: service_healthy
|
||||
server:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 45s
|
||||
|
||||
# Ingest server for private-location probes (the probe itself,
|
||||
# ghcr.io/openstatushq/private-location, runs wherever you monitor from).
|
||||
private-location:
|
||||
image: ghcr.io/openstatushq/openstatus-private-location:e1a9825
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8080
|
||||
environment:
|
||||
- PORT=8080
|
||||
- GIN_MODE=release
|
||||
- DB_URL=http://libsql:8080
|
||||
- WORKFLOWS_URL=http://workflows:3000
|
||||
- CRON_SECRET=${CRON_SECRET}
|
||||
- TINYBIRD_URL=${TINYBIRD_URL}
|
||||
- TINYBIRD_TOKEN=${TINYBIRD_TOKEN}
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
libsql-data:
|
||||
workflows-data:
|
||||
@@ -0,0 +1,60 @@
|
||||
# OpenStatus
|
||||
|
||||
OpenStatus is deployed here in self-hosted mode following the official Docker
|
||||
setup (`DOCKER.md` / `docker-compose.github-packages.yaml` upstream), with all
|
||||
app images pinned to the same upstream commit (`e1a9825`).
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Purpose |
|
||||
|---|---|
|
||||
| dashboard | Admin UI (your first domain) |
|
||||
| status-page | Public status pages (your second domain, pages served at `/<slug>`) |
|
||||
| server | API backend (tRPC / public API) |
|
||||
| workflows | Background jobs / alerting |
|
||||
| libsql | Local libSQL (Turso) database — no cloud account needed |
|
||||
| db-migrate | One-shot migration runner (built from the pinned upstream commit, exits after applying) |
|
||||
| private-location | Ingest server that receives check results from your monitoring probes |
|
||||
|
||||
Database migrations run automatically on every deploy (the one-shot
|
||||
`db-migrate` service is idempotent).
|
||||
|
||||
Note: `db-migrate` is built from source (upstream publishes no image for it),
|
||||
so the first deployment takes a few extra minutes while it builds.
|
||||
|
||||
## First login
|
||||
|
||||
1. Open the dashboard domain and choose **magic link** login with your e-mail.
|
||||
2. If you did not configure `RESEND_API_KEY`, the magic link is not e-mailed —
|
||||
it is printed to the **dashboard service logs**. Open the dashboard
|
||||
container logs in Dokploy and copy the `>>> Magic Link: ...` URL into your
|
||||
browser. If the printed link starts with `http://0.0.0.0:3000`, replace that
|
||||
host with your dashboard domain before opening it. (Optionally, once HTTPS
|
||||
is enabled on your dashboard domain, you can add
|
||||
`AUTH_URL=https://<your-dashboard-domain>` to the dashboard service in the
|
||||
compose file so links are generated with the right host — do not set it to
|
||||
an empty value or a plain-HTTP domain, as that breaks the auth flow.)
|
||||
3. Create a workspace and a status page with a slug; it becomes available at
|
||||
`https://<status-page-domain>/<slug>`.
|
||||
|
||||
## Running checks (probes)
|
||||
|
||||
Self-hosted OpenStatus runs monitors through **private location probes**
|
||||
(`ghcr.io/openstatushq/private-location`) that you deploy wherever you want to
|
||||
monitor from. Point the probe at this stack's ingest server:
|
||||
|
||||
- Expose the `private-location` service (port 8080) on a domain in Dokploy, and
|
||||
- set `OPENSTATUS_INGEST_URL` on the probe to that URL, together with the
|
||||
`OPENSTATUS_KEY` you create in the dashboard.
|
||||
|
||||
See the upstream guide: https://www.openstatus.dev/docs (Guides → How to
|
||||
Self-Host openstatus).
|
||||
|
||||
## Analytics (optional)
|
||||
|
||||
Tinybird analytics are disabled by default (`TINYBIRD_NOOP=true`), so charts
|
||||
show no data but everything else works. To enable analytics, run a Tinybird
|
||||
Local container or use Tinybird Cloud, deploy the `packages/tinybird` project
|
||||
with the `tb` CLI, and set `TINYBIRD_URL` / `TINYBIRD_TOKEN` in the
|
||||
environment (plus `TINY_BIRD_API_KEY` and remove `TINYBIRD_NOOP`) — see the
|
||||
upstream self-hosting guide.
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "openstatus",
|
||||
"name": "OpenStatus",
|
||||
"version": "e1a9825",
|
||||
"description": "OpenStatus is an open-source synthetic monitoring platform with beautiful status pages, uptime and API monitoring as code, and alerting.",
|
||||
"logo": "openstatus.png",
|
||||
"links": {
|
||||
"github": "https://github.com/openstatusHQ/openstatus",
|
||||
"website": "https://www.openstatus.dev",
|
||||
"docs": "https://www.openstatus.dev/docs"
|
||||
},
|
||||
"tags": [
|
||||
"monitoring",
|
||||
"status-page",
|
||||
"uptime"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,34 @@
|
||||
[variables]
|
||||
dashboard_domain = "${domain}"
|
||||
status_page_domain = "${domain}"
|
||||
auth_secret = "${base64:32}"
|
||||
cron_secret = "${password:32}"
|
||||
super_admin_token = "${password:32}"
|
||||
|
||||
[config]
|
||||
|
||||
# Dashboard (admin UI — log in here)
|
||||
[[config.domains]]
|
||||
serviceName = "dashboard"
|
||||
port = 3000
|
||||
host = "${dashboard_domain}"
|
||||
|
||||
# Public status pages (served at /<page-slug>)
|
||||
[[config.domains]]
|
||||
serviceName = "status-page"
|
||||
port = 3000
|
||||
host = "${status_page_domain}"
|
||||
|
||||
[config.env]
|
||||
DASHBOARD_HOST = "${dashboard_domain}"
|
||||
STATUS_PAGE_HOST = "${status_page_domain}"
|
||||
AUTH_SECRET = "${auth_secret}"
|
||||
CRON_SECRET = "${cron_secret}"
|
||||
SUPER_ADMIN_TOKEN = "${super_admin_token}"
|
||||
# Optional: Resend API key for outgoing e-mails. Without it, magic-link
|
||||
# login URLs are printed to the dashboard container logs instead.
|
||||
RESEND_API_KEY = ""
|
||||
# Optional: Tinybird analytics for the private-location ingest server
|
||||
# (see instructions — analytics are disabled/no-op by default)
|
||||
TINYBIRD_URL = ""
|
||||
TINYBIRD_TOKEN = ""
|
||||
@@ -0,0 +1,230 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine@sha256:9a8afca54e7861fd90fab5fdf4c42477a6b1cb7d293595148e674e0a3181de15
|
||||
environment:
|
||||
POSTGRES_USER: orbit
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required}
|
||||
POSTGRES_DB: orbit
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- pg_isready -U orbit -d orbit
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
redis:
|
||||
image: redis:8-alpine@sha256:978f0e01593e65eed801f2402944efcd936d43b5027e4908a7897baf88ed6241
|
||||
command:
|
||||
- redis-server
|
||||
- --appendonly
|
||||
- 'yes'
|
||||
volumes:
|
||||
- redis:/data
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- redis-cli
|
||||
- ping
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
storage:
|
||||
# SeaweedFS S3 gateway (MinIO images are no longer publicly pullable).
|
||||
image: chrislusf/seaweedfs:4.48
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
command:
|
||||
- |
|
||||
set -e
|
||||
printf '{"identities":[{"name":"orbit","credentials":[{"accessKey":"orbit","secretKey":"%s"}],"actions":["Admin","Read","Write","List","Tagging"]}]}' "$$MINIO_PASSWORD" > /tmp/s3.json
|
||||
exec weed server -dir=/data -ip.bind=0.0.0.0 -s3 -s3.port=9000 -s3.config=/tmp/s3.json
|
||||
environment:
|
||||
MINIO_PASSWORD: ${MINIO_PASSWORD:?required}
|
||||
volumes:
|
||||
- storage:/data
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- wget -q -O /dev/null http://127.0.0.1:9000/healthz || exit 1
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- '9000'
|
||||
bucket:
|
||||
# Reuses the mc client from orbit-bucket to create the bucket and its CORS rule on SeaweedFS.
|
||||
image: ghcr.io/noveum/orbit-bucket:2026.09.24
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
command:
|
||||
- |
|
||||
set -eu
|
||||
until mc alias set orbit http://storage:9000 orbit "$$MINIO_PASSWORD" --api S3v4; do sleep 2; done
|
||||
until mc mb --ignore-existing orbit/orbit-uploads; do sleep 2; done
|
||||
printf '<CORSConfiguration><CORSRule><AllowedOrigin>%s</AllowedOrigin><AllowedMethod>GET</AllowedMethod><AllowedMethod>PUT</AllowedMethod><AllowedMethod>HEAD</AllowedMethod><AllowedHeader>*</AllowedHeader><ExposeHeader>ETag</ExposeHeader><MaxAgeSeconds>3600</MaxAgeSeconds></CORSRule></CORSConfiguration>' "$$ORBIT_APP_URL" > /tmp/cors.xml
|
||||
until mc cors set orbit/orbit-uploads /tmp/cors.xml; do sleep 2; done
|
||||
touch /tmp/initialized
|
||||
exec tail -f /dev/null
|
||||
environment:
|
||||
MINIO_PASSWORD: ${MINIO_PASSWORD:?required}
|
||||
ORBIT_APP_URL: ${ORBIT_APP_URL:?required}
|
||||
depends_on:
|
||||
storage:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- test -f /tmp/initialized
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
web:
|
||||
image: ghcr.io/noveum/orbit-runtime:2026.09.24
|
||||
environment:
|
||||
DATABASE_URL: postgres://orbit:${POSTGRES_PASSWORD:?required}@postgres:5432/orbit
|
||||
DIRECT_URL: postgres://orbit:${POSTGRES_PASSWORD:?required}@postgres:5432/orbit
|
||||
DATABASE_PREPARED_STATEMENTS: 'true'
|
||||
REDIS_URL: redis://redis:6379
|
||||
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?required}
|
||||
BETTER_AUTH_URL: ${ORBIT_APP_URL:?required}
|
||||
NEXT_PUBLIC_APP_URL: ${ORBIT_APP_URL:?required}
|
||||
ORBIT_PASSWORD_AUTH: ${ORBIT_PASSWORD_AUTH:-true}
|
||||
ALLOWED_EMAIL_DOMAINS: ${ALLOWED_EMAIL_DOMAINS:-}
|
||||
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
||||
EMAIL_FROM: ${EMAIL_FROM:-Orbit <auth@orbit.local>}
|
||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
||||
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
|
||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
|
||||
GITHUB_APP_SLUG: ${GITHUB_APP_SLUG:-}
|
||||
GITHUB_APP_ID: ${GITHUB_APP_ID:-}
|
||||
GITHUB_APP_PRIVATE_KEY: ${GITHUB_APP_PRIVATE_KEY:-}
|
||||
GITHUB_APP_CLIENT_ID: ${GITHUB_APP_CLIENT_ID:-}
|
||||
GITHUB_APP_CLIENT_SECRET: ${GITHUB_APP_CLIENT_SECRET:-}
|
||||
GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:-}
|
||||
SLACK_ENABLED: ${SLACK_ENABLED:-false}
|
||||
SLACK_CLIENT_ID: ${SLACK_CLIENT_ID:-}
|
||||
SLACK_CLIENT_SECRET: ${SLACK_CLIENT_SECRET:-}
|
||||
SLACK_SIGNING_SECRET: ${SLACK_SIGNING_SECRET:-}
|
||||
SLACK_APP_ID: ${SLACK_APP_ID:-}
|
||||
CRON_SECRET: ${CRON_SECRET:-}
|
||||
ORBIT_SELF_HOSTED: 'true'
|
||||
S3_ENDPOINT: ${ORBIT_STORAGE_URL:?required}
|
||||
S3_REGION: us-east-1
|
||||
S3_BUCKET: orbit-uploads
|
||||
S3_ACCESS_KEY_ID: orbit
|
||||
S3_SECRET_ACCESS_KEY: ${MINIO_PASSWORD:?required}
|
||||
NEXT_TELEMETRY_DISABLED: '1'
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 5
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
bucket:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
realtime:
|
||||
image: ghcr.io/noveum/orbit-runtime:2026.09.24
|
||||
command:
|
||||
- node
|
||||
- apps/web/realtime.mjs
|
||||
environment:
|
||||
DATABASE_URL: postgres://orbit:${POSTGRES_PASSWORD:?required}@postgres:5432/orbit
|
||||
DIRECT_URL: postgres://orbit:${POSTGRES_PASSWORD:?required}@postgres:5432/orbit
|
||||
DATABASE_PREPARED_STATEMENTS: 'true'
|
||||
REDIS_URL: redis://redis:6379
|
||||
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?required}
|
||||
BETTER_AUTH_URL: ${ORBIT_APP_URL:?required}
|
||||
NEXT_PUBLIC_APP_URL: ${ORBIT_APP_URL:?required}
|
||||
ORBIT_PASSWORD_AUTH: ${ORBIT_PASSWORD_AUTH:-true}
|
||||
ALLOWED_EMAIL_DOMAINS: ${ALLOWED_EMAIL_DOMAINS:-}
|
||||
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
||||
EMAIL_FROM: ${EMAIL_FROM:-Orbit <auth@orbit.local>}
|
||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
||||
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
|
||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
|
||||
GITHUB_APP_SLUG: ${GITHUB_APP_SLUG:-}
|
||||
GITHUB_APP_ID: ${GITHUB_APP_ID:-}
|
||||
GITHUB_APP_PRIVATE_KEY: ${GITHUB_APP_PRIVATE_KEY:-}
|
||||
GITHUB_APP_CLIENT_ID: ${GITHUB_APP_CLIENT_ID:-}
|
||||
GITHUB_APP_CLIENT_SECRET: ${GITHUB_APP_CLIENT_SECRET:-}
|
||||
GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:-}
|
||||
SLACK_ENABLED: ${SLACK_ENABLED:-false}
|
||||
SLACK_CLIENT_ID: ${SLACK_CLIENT_ID:-}
|
||||
SLACK_CLIENT_SECRET: ${SLACK_CLIENT_SECRET:-}
|
||||
SLACK_SIGNING_SECRET: ${SLACK_SIGNING_SECRET:-}
|
||||
SLACK_APP_ID: ${SLACK_APP_ID:-}
|
||||
CRON_SECRET: ${CRON_SECRET:-}
|
||||
ORBIT_SELF_HOSTED: 'true'
|
||||
S3_ENDPOINT: ${ORBIT_STORAGE_URL:?required}
|
||||
S3_REGION: us-east-1
|
||||
S3_BUCKET: orbit-uploads
|
||||
S3_ACCESS_KEY_ID: orbit
|
||||
S3_SECRET_ACCESS_KEY: ${MINIO_PASSWORD:?required}
|
||||
NEXT_TELEMETRY_DISABLED: '1'
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- fetch('http://127.0.0.1:3100/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
depends_on:
|
||||
web:
|
||||
condition: service_healthy
|
||||
bucket:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
scheduler:
|
||||
image: ghcr.io/noveum/orbit-runtime:2026.09.24
|
||||
command:
|
||||
- node
|
||||
- apps/web/scheduler.mjs
|
||||
environment:
|
||||
CRON_SECRET: ${CRON_SECRET:?required}
|
||||
ORBIT_INTERNAL_URL: http://web:3000
|
||||
stop_grace_period: 310s
|
||||
depends_on:
|
||||
web:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
orbit:
|
||||
image: ghcr.io/noveum/orbit-gateway:2026.09.24
|
||||
environment:
|
||||
ORBIT_TRUSTED_PROXIES: ${ORBIT_TRUSTED_PROXIES:-private_ranges}
|
||||
depends_on:
|
||||
web:
|
||||
condition: service_healthy
|
||||
realtime:
|
||||
condition: service_healthy
|
||||
scheduler:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- '3000'
|
||||
volumes:
|
||||
postgres: null
|
||||
redis: null
|
||||
storage: null
|
||||
version: '3.8'
|
||||