mirror of
https://github.com/Dokploy/templates.git
synced 2026-07-15 14:52:05 -05:00
docs: add per-template setup instructions (supabase, trigger.dev)
Adds instructions.md files for the Supabase and Trigger.dev templates and renders them in the template browser as a new Instructions tab in the template dialog (fetched from blueprints/<id>/instructions.md, rendered with a small dependency-free markdown component). Also removes the placeholder blueprints/ackee/instructions.md so it does not surface as an empty Instructions tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||
import { Label } from "./ui/label";
|
||||
import { Clipboard } from "lucide-react";
|
||||
import { Input } from "./ui/input";
|
||||
import Markdown from "./ui/markdown";
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
@@ -32,6 +33,7 @@ interface Template {
|
||||
interface TemplateFiles {
|
||||
dockerCompose: string | null;
|
||||
config: string | null;
|
||||
instructions: string | null;
|
||||
}
|
||||
|
||||
interface TemplateDialogProps {
|
||||
@@ -169,8 +171,23 @@ const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Tabs defaultValue="docker-compose" className="w-full">
|
||||
<Tabs
|
||||
defaultValue={
|
||||
templateFiles?.instructions
|
||||
? "instructions"
|
||||
: "docker-compose"
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="w-full justify-start">
|
||||
{templateFiles?.instructions && (
|
||||
<TabsTrigger
|
||||
value="instructions"
|
||||
className="data-[state=active]:font-bold"
|
||||
>
|
||||
Instructions
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger
|
||||
value="docker-compose"
|
||||
className="data-[state=active]:font-bold"
|
||||
@@ -184,6 +201,24 @@ const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
||||
Configuration
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{templateFiles?.instructions && (
|
||||
<TabsContent value="instructions" className="mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="flex flex-col items-start w-fit justify-start gap-1">
|
||||
<span className="leading-tight text-xl font-semibold">
|
||||
Setup Instructions
|
||||
</span>
|
||||
<span className="leading-tight text-sm text-gray-500">
|
||||
instructions.md
|
||||
</span>
|
||||
</Label>
|
||||
<div className="rounded-md border p-4">
|
||||
<Markdown content={templateFiles.instructions} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="docker-compose" className="mt-4">
|
||||
{templateFiles?.dockerCompose && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -28,6 +28,7 @@ interface Template {
|
||||
interface TemplateFiles {
|
||||
dockerCompose: string | null;
|
||||
config: string | null;
|
||||
instructions: string | null;
|
||||
}
|
||||
|
||||
interface TemplateGridProps {
|
||||
@@ -80,20 +81,34 @@ const TemplateGrid: React.FC<TemplateGridProps> = ({ view }) => {
|
||||
const fetchTemplateFiles = async (templateId: string) => {
|
||||
setModalLoading(true);
|
||||
try {
|
||||
const [dockerComposeRes, configRes] = await Promise.all([
|
||||
fetch(`/blueprints/${templateId}/docker-compose.yml`),
|
||||
fetch(`/blueprints/${templateId}/template.toml`),
|
||||
]);
|
||||
const [dockerComposeRes, configRes, instructionsRes] = await Promise.all(
|
||||
[
|
||||
fetch(`/blueprints/${templateId}/docker-compose.yml`),
|
||||
fetch(`/blueprints/${templateId}/template.toml`),
|
||||
fetch(`/blueprints/${templateId}/instructions.md`),
|
||||
]
|
||||
);
|
||||
|
||||
const dockerCompose = dockerComposeRes.ok
|
||||
? await dockerComposeRes.text()
|
||||
: null;
|
||||
const config = configRes.ok ? await configRes.text() : null;
|
||||
|
||||
setTemplateFiles({ dockerCompose, config });
|
||||
// Guard against SPA fallbacks that return index.html with a 200 status
|
||||
// for templates that don't have an instructions.md file.
|
||||
const instructionsIsMarkdown =
|
||||
instructionsRes.ok &&
|
||||
!(instructionsRes.headers.get("content-type") || "").includes(
|
||||
"text/html"
|
||||
);
|
||||
const instructions = instructionsIsMarkdown
|
||||
? await instructionsRes.text()
|
||||
: null;
|
||||
|
||||
setTemplateFiles({ dockerCompose, config, instructions });
|
||||
} catch (err) {
|
||||
console.error("Error fetching template files:", err);
|
||||
setTemplateFiles({ dockerCompose: null, config: null });
|
||||
setTemplateFiles({ dockerCompose: null, config: null, instructions: null });
|
||||
} finally {
|
||||
setModalLoading(false);
|
||||
}
|
||||
|
||||
177
app/src/components/ui/markdown.tsx
Normal file
177
app/src/components/ui/markdown.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import React from "react";
|
||||
|
||||
// Minimal markdown renderer (no external dependencies) for the per-template
|
||||
// instructions.md files. Supports headings, paragraphs, fenced code blocks,
|
||||
// ordered/unordered lists, and inline code / bold / links.
|
||||
|
||||
const INLINE_PATTERN =
|
||||
/(`[^`]+`)|(\*\*[^*]+\*\*)|(\[[^\]]+\]\([^)\s]+\))/g;
|
||||
|
||||
const renderInline = (text: string): React.ReactNode[] => {
|
||||
const nodes: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let key = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
INLINE_PATTERN.lastIndex = 0;
|
||||
|
||||
while ((match = INLINE_PATTERN.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
nodes.push(text.slice(lastIndex, match.index));
|
||||
}
|
||||
const token = match[0];
|
||||
if (token.startsWith("`")) {
|
||||
nodes.push(
|
||||
<code
|
||||
key={key++}
|
||||
className="px-1.5 py-0.5 rounded bg-muted font-mono text-[0.85em]"
|
||||
>
|
||||
{token.slice(1, -1)}
|
||||
</code>
|
||||
);
|
||||
} else if (token.startsWith("**")) {
|
||||
nodes.push(<strong key={key++}>{token.slice(2, -2)}</strong>);
|
||||
} else {
|
||||
const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(token);
|
||||
if (link) {
|
||||
nodes.push(
|
||||
<a
|
||||
key={key++}
|
||||
href={link[2]}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
|
||||
>
|
||||
{link[1]}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
nodes.push(token);
|
||||
}
|
||||
}
|
||||
lastIndex = match.index + token.length;
|
||||
}
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(text.slice(lastIndex));
|
||||
}
|
||||
return nodes;
|
||||
};
|
||||
|
||||
const HEADING_CLASSES: Record<number, string> = {
|
||||
1: "text-2xl font-bold mt-2",
|
||||
2: "text-xl font-semibold mt-6 border-b pb-1",
|
||||
3: "text-lg font-semibold mt-4",
|
||||
4: "text-base font-semibold mt-3",
|
||||
};
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const Markdown: React.FC<MarkdownProps> = ({ content }) => {
|
||||
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
||||
const blocks: React.ReactNode[] = [];
|
||||
let key = 0;
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
|
||||
// Blank line
|
||||
if (line.trim() === "") {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fenced code block
|
||||
if (line.trim().startsWith("```")) {
|
||||
const code: string[] = [];
|
||||
i++;
|
||||
while (i < lines.length && !lines[i].trim().startsWith("```")) {
|
||||
code.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
i++; // skip closing fence
|
||||
blocks.push(
|
||||
<pre
|
||||
key={key++}
|
||||
className="bg-muted rounded-md p-4 overflow-x-auto text-sm font-mono whitespace-pre"
|
||||
>
|
||||
<code>{code.join("\n")}</code>
|
||||
</pre>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Heading
|
||||
const heading = /^(#{1,4})\s+(.*)$/.exec(line);
|
||||
if (heading) {
|
||||
const level = heading[1].length;
|
||||
const Tag = `h${Math.min(level + 1, 6)}` as keyof React.JSX.IntrinsicElements;
|
||||
blocks.push(
|
||||
<Tag key={key++} className={HEADING_CLASSES[level]}>
|
||||
{renderInline(heading[2])}
|
||||
</Tag>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unordered list
|
||||
if (/^[-*]\s+/.test(line)) {
|
||||
const items: string[] = [];
|
||||
while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
|
||||
items.push(lines[i].replace(/^[-*]\s+/, ""));
|
||||
i++;
|
||||
}
|
||||
blocks.push(
|
||||
<ul key={key++} className="list-disc pl-6 space-y-1">
|
||||
{items.map((item, idx) => (
|
||||
<li key={idx}>{renderInline(item)}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered list
|
||||
if (/^\d+\.\s+/.test(line)) {
|
||||
const items: string[] = [];
|
||||
while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
|
||||
items.push(lines[i].replace(/^\d+\.\s+/, ""));
|
||||
i++;
|
||||
}
|
||||
blocks.push(
|
||||
<ol key={key++} className="list-decimal pl-6 space-y-1">
|
||||
{items.map((item, idx) => (
|
||||
<li key={idx}>{renderInline(item)}</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Paragraph: consume consecutive plain lines
|
||||
const paragraph: string[] = [line];
|
||||
i++;
|
||||
while (
|
||||
i < lines.length &&
|
||||
lines[i].trim() !== "" &&
|
||||
!/^(#{1,4})\s+/.test(lines[i]) &&
|
||||
!/^[-*]\s+/.test(lines[i]) &&
|
||||
!/^\d+\.\s+/.test(lines[i]) &&
|
||||
!lines[i].trim().startsWith("```")
|
||||
) {
|
||||
paragraph.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
blocks.push(
|
||||
<p key={key++} className="leading-relaxed">
|
||||
{renderInline(paragraph.join(" "))}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="space-y-3 text-sm">{blocks}</div>;
|
||||
};
|
||||
|
||||
export default Markdown;
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
## Instructions
|
||||
|
||||
We don't have nothing to show here....
|
||||
61
blueprints/supabase/instructions.md
Normal file
61
blueprints/supabase/instructions.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Supabase Setup Instructions
|
||||
|
||||
## Deploy
|
||||
|
||||
1. In Dokploy, create the service from the **Supabase** template (requires Dokploy `>= 0.22.5`).
|
||||
2. Dokploy automatically generates all secrets for you (`POSTGRES_PASSWORD`, `JWT_SECRET`, `ANON_KEY`, `SERVICE_ROLE_KEY`, `DASHBOARD_PASSWORD`, etc.). You can review them in the **Environment** tab of the service.
|
||||
3. Deploy and wait for all containers to become healthy. The first deploy can take several minutes while the Postgres database initializes.
|
||||
|
||||
## Log in to Supabase Studio
|
||||
|
||||
The main domain of the template points to the `kong` API gateway (port `8000`), which protects Supabase Studio with basic authentication:
|
||||
|
||||
- **Username**: the value of `DASHBOARD_USERNAME` (default: `supabase`)
|
||||
- **Password**: the value of `DASHBOARD_PASSWORD`
|
||||
|
||||
Both values are in the **Environment** tab of the service in Dokploy.
|
||||
|
||||
## API URL and keys
|
||||
|
||||
To connect an application (for example with `supabase-js`):
|
||||
|
||||
- **API URL**: `https://<your-domain>` (requests are routed through Kong)
|
||||
- **anon key**: the value of `ANON_KEY` in the Environment tab
|
||||
- **service_role key**: the value of `SERVICE_ROLE_KEY` in the Environment tab (server-side only, never expose it to browsers)
|
||||
|
||||
## Recommended configuration
|
||||
|
||||
Review these variables in the **Environment** tab before using Supabase in production:
|
||||
|
||||
- `SUPABASE_PUBLIC_URL` and `API_EXTERNAL_URL`: must point to your Supabase domain with the correct `http`/`https` scheme (the template sets them from your domain automatically).
|
||||
- `SITE_URL` and `ADDITIONAL_REDIRECT_URLS`: must point to the application that uses Supabase for authentication.
|
||||
- `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_ADMIN_EMAIL`, `SMTP_SENDER_NAME`: required for auth emails (sign-up confirmations, password resets). The template ships with placeholder values, so no real emails are sent until you configure a real SMTP provider.
|
||||
|
||||
## Warning: changing POSTGRES_PASSWORD after the first deploy
|
||||
|
||||
The Postgres data directory (mounted at `files/volumes/db/data`) is initialized **once**, on the first deploy, using the value of `POSTGRES_PASSWORD` at that moment. The same password is also assigned to the internal Supabase roles (`authenticator`, `pgbouncer`, `supabase_auth_admin`, `supabase_functions_admin`, `supabase_storage_admin`) by an init script that only runs on first boot.
|
||||
|
||||
If you later change `POSTGRES_PASSWORD` in the Environment tab and redeploy, the password stored **inside the database does not change**. The other services will start using the new password while the database still expects the old one, and you will see errors such as `invalid_password` or `password authentication failed`.
|
||||
|
||||
To actually change the password, use one of these options:
|
||||
|
||||
### Option A: change it inside the database (keeps your data)
|
||||
|
||||
1. Open a terminal into the `db` container (in Dokploy: your Supabase service, `db` container, **Terminal**) and run `psql -U postgres`.
|
||||
2. Execute the following, using your new password:
|
||||
|
||||
```sql
|
||||
ALTER USER postgres WITH PASSWORD 'your-new-password';
|
||||
ALTER USER supabase_admin WITH PASSWORD 'your-new-password';
|
||||
ALTER USER authenticator WITH PASSWORD 'your-new-password';
|
||||
ALTER USER pgbouncer WITH PASSWORD 'your-new-password';
|
||||
ALTER USER supabase_auth_admin WITH PASSWORD 'your-new-password';
|
||||
ALTER USER supabase_functions_admin WITH PASSWORD 'your-new-password';
|
||||
ALTER USER supabase_storage_admin WITH PASSWORD 'your-new-password';
|
||||
```
|
||||
|
||||
3. Update `POSTGRES_PASSWORD` in the Environment tab to the same value and redeploy.
|
||||
|
||||
### Option B: reinitialize the database (deletes ALL data)
|
||||
|
||||
Only if the instance has no data you care about: stop the service, delete the `files/volumes/db/data` directory of the service, set the new `POSTGRES_PASSWORD`, and deploy again. The database will be initialized from scratch with the new password.
|
||||
54
blueprints/triggerdotdev/instructions.md
Normal file
54
blueprints/triggerdotdev/instructions.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Trigger.dev Setup Instructions
|
||||
|
||||
## Deploy
|
||||
|
||||
1. In Dokploy, create the service from the **Trigger.dev** template.
|
||||
2. Dokploy generates all required secrets (`MAGIC_LINK_SECRET`, `SESSION_SECRET`, `ENCRYPTION_KEY`, database credentials, etc.) automatically.
|
||||
3. Deploy and wait until the containers are running. The main domain points to the `webapp` service (port `3000`).
|
||||
|
||||
If you serve the app over HTTPS, set `TRIGGER_PROTOCOL=https` in the **Environment** tab (the template defaults to `http`) so that login links use the correct scheme, then redeploy.
|
||||
|
||||
## First login (no email server configured)
|
||||
|
||||
Trigger.dev logs you in with **magic links** sent by email. The template does not configure an email transport by default, so the email is never actually sent. Instead, **the magic link is printed to the logs of the `webapp` container**:
|
||||
|
||||
1. Open `https://<your-domain>` and enter your email address to request a magic link.
|
||||
2. In Dokploy, go to your Trigger.dev service, open the **Logs** tab, and select the `webapp` container/service.
|
||||
3. Look for a recent log entry containing a URL like `.../magic?token=...` (search for `magic`).
|
||||
4. Copy that URL into your browser to complete the login.
|
||||
|
||||
## Configure email sending (optional)
|
||||
|
||||
To have magic links delivered by email, add these variables in the **Environment** tab and redeploy.
|
||||
|
||||
Using SMTP:
|
||||
|
||||
```
|
||||
EMAIL_TRANSPORT=smtp
|
||||
FROM_EMAIL=trigger@example.com
|
||||
REPLY_TO_EMAIL=trigger@example.com
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=your-smtp-user
|
||||
SMTP_PASSWORD=your-smtp-password
|
||||
```
|
||||
|
||||
Using [Resend](https://resend.com):
|
||||
|
||||
```
|
||||
EMAIL_TRANSPORT=resend
|
||||
FROM_EMAIL=trigger@example.com
|
||||
REPLY_TO_EMAIL=trigger@example.com
|
||||
RESEND_API_KEY=your-resend-api-key
|
||||
```
|
||||
|
||||
## Restrict who can sign up (recommended)
|
||||
|
||||
By default anyone who can reach the webapp can request a magic link. Restrict access with a regex of allowed email addresses:
|
||||
|
||||
```
|
||||
WHITELISTED_EMAILS=you@example\.com|teammate@example\.com
|
||||
```
|
||||
|
||||
You can also grant admin rights by email with `ADMIN_EMAILS` (same regex format).
|
||||
Reference in New Issue
Block a user