Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ Use o hostname central em modo **DNS only**, apontando diretamente ao servidor,

Na instância, informe `api.cliente.com` e `studio.cliente.com`. O painel mostra os registros DNS e permite copiar o destino. Crie esses registros no provedor DNS e clique em **Salvar e implantar**. O Traefik identifica o domínio solicitado e encaminha para a instância correta; o CNAME sozinho não configura o roteamento nem os certificados.

Subdomínios automáticos: defina `PANEL_BASE_DOMAIN=supa.seudominio.com` e crie um registro curinga `*.supa.seudominio.com` apontando para o servidor. Cada nova instância recebe `api-<slug>.supa.seudominio.com` (API) e `<slug>.supa.seudominio.com` (Studio) já na criação; basta implantar. Sem a variável, nada muda e os domínios continuam manuais.

Se o IP do servidor mudar, atualize apenas o A/AAAA do hostname central. Se mudar o hostname central, os CNAMEs existentes precisam ser atualizados. Use subdomínios: ALIAS/ANAME no domínio raiz depende do provedor. Não crie CNAME apontando para si mesmo nem registros A/AAAA conflitantes no mesmo nome. O painel não altera automaticamente seu provedor DNS.

<img src="public/domains.png" alt="Configuração de API e Studio com registros CNAME prontos para copiar" width="100%" />
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
SUPAPANEL_MODE: production
PROXY_MODE: dokploy
INSTANCE_DNS_TARGET: ${INSTANCE_DNS_TARGET:-}
PANEL_BASE_DOMAIN: ${PANEL_BASE_DOMAIN:-}
PROXY_NETWORK: ${PROXY_NETWORK:-dokploy-network}
PROXY_ENTRYPOINT: ${PROXY_ENTRYPOINT:-websecure}
PROXY_CERT_RESOLVER: ${PROXY_CERT_RESOLVER:-letsencrypt}
Expand Down
1 change: 1 addition & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ services:
- SUPABASE_CORE_REPO_URL=${SUPABASE_CORE_REPO_URL}
- APP_NAME=${APP_NAME:-SupaPanel}
- APP_URL=${APP_URL:-http://localhost:3000}
- PANEL_BASE_DOMAIN=${PANEL_BASE_DOMAIN:-}
volumes:
- ${DATA_PATH}:${DATA_PATH}
- /var/run/docker.sock:/var/run/docker.sock
Expand Down
14 changes: 14 additions & 0 deletions src/lib/dns-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,17 @@ export async function getDnsTarget() {
const value = (saved?.value || fallback).toLowerCase();
return validDnsTarget(value) ? value : null;
}
// With a wildcard record (*.base) pointing at the proxy, new instances get
// api-<slug>.<base> and <slug>.<base> without manual DNS setup.
export function autoProjectDomains(
slug: string,
baseDomain = process.env.PANEL_BASE_DOMAIN,
) {
const base = baseDomain?.trim().toLowerCase();
if (!base) return null;
const domain = `api-${slug}.${base}`;
const studioDomain = `${slug}.${base}`;
return validDnsTarget(domain) && validDnsTarget(studioDomain)
? { domain, studioDomain }
: null;
}
13 changes: 12 additions & 1 deletion src/lib/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import * as path from 'path'
import { exec, execFile } from 'child_process'
import { promisify } from 'util'
import { prisma } from './db'
import { removeProjectTraefikConfig } from './traefik'
import { removeProjectTraefikConfig, generateProjectTraefikConfig, verifyDomainDNS } from './traefik'
import { autoProjectDomains } from './dns-target'

import { corePath, projectsPath, secret, jwt, serializeEnv, prepareCompose, supabaseRef, isDokploy } from './runtime'

Expand Down Expand Up @@ -149,6 +150,16 @@ export async function createProject(name: string, userId: string, description?:
createdDir = path.join(getProjectsBasePath(), slug)
await provisionProjectFiles(project)

const auto = autoProjectDomains(slug)
if (auto) {
const [domainVerified, studioDomainVerified] = await Promise.all([verifyDomainDNS(auto.domain), verifyDomainDNS(auto.studioDomain)])
const routed = await prisma.project.update({ where: { id: project.id }, data: { ...auto, domainVerified, studioDomainVerified } })
const env = await updateProjectEnvVars(project.id, { API_EXTERNAL_URL: `https://${auto.domain}/auth/v1`, SUPABASE_PUBLIC_URL: `https://${auto.domain}` })
if (!env.success) throw new Error(env.error)
await generateProjectTraefikConfig({ projectSlug: slug, ...auto, kongPort: 8000, studioPort: 8000 })
return { success: true, project: routed }
}

return { success: true, project }
} catch (error) {
if (createdId) await prisma.$transaction(async tx => {
Expand Down
20 changes: 20 additions & 0 deletions tests/auto-subdomain.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import test from "node:test";
import assert from "node:assert/strict";
import { autoProjectDomains } from "../src/lib/dns-target";

test("PANEL_BASE_DOMAIN gives a new project api-<slug> and <slug> subdomains", () => {
assert.deepEqual(autoProjectDomains("shop-0a1b2c3d4e5f", "Supa.Example.com "), {
domain: "api-shop-0a1b2c3d4e5f.supa.example.com",
studioDomain: "shop-0a1b2c3d4e5f.supa.example.com",
});
});

test("without PANEL_BASE_DOMAIN nothing is assigned (manual domains stay the only path)", () => {
assert.equal(autoProjectDomains("shop-0a1b2c3d4e5f", undefined), null);
assert.equal(autoProjectDomains("shop-0a1b2c3d4e5f", ""), null);
});

test("a slug that is not a valid hostname label is skipped instead of producing a bad domain", () => {
// createProject slugifies "!shop" to "-shop-<hex>"; a label cannot start with "-".
assert.equal(autoProjectDomains("-shop-0a1b2c3d4e5f", "supa.example.com"), null);
});