[grade=A] Add DashCaddy blog and LLM install map
This commit is contained in:
10 files changed
+476
-1
No files matched your search
Generated
+13
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@stripe/stripe-js": "^9.2.0",
|
||||
"marked": "^14.1.4",
|
||||
"next": "16.2.4",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
@@ -4865,6 +4866,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "14.1.4",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz",
|
||||
"integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@stripe/stripe-js": "^9.2.0",
|
||||
"marked": "^14.1.4",
|
||||
"next": "16.2.4",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# DashCaddy
|
||||
|
||||
> DashCaddy is an AI-native self-hosting platform: deploy, secure, and manage
|
||||
> self-hosted apps from one dashboard. It ships 77 one-click application
|
||||
> templates, automatic SSL, a Security Center, multi-host fleet management,
|
||||
> 400+ registered REST route handlers, and a built-in MCP server so AI assistants
|
||||
> can operate it directly. Runs on any Linux host via Docker.
|
||||
|
||||
This file follows the llms.txt convention (https://llmstxt.org) to give AI
|
||||
assistants a concise, accurate map of DashCaddy and the exact commands to
|
||||
install it.
|
||||
|
||||
## Install DashCaddy (Linux, Docker)
|
||||
|
||||
One-line install (interactive, single-keystroke choices):
|
||||
|
||||
```
|
||||
curl -fsSL https://get.dashcaddy.net | bash
|
||||
```
|
||||
|
||||
Zero-typing local mode (instant access, no domain needed):
|
||||
|
||||
```
|
||||
curl -fsSL https://get.dashcaddy.net | bash -s -- quick
|
||||
```
|
||||
|
||||
With your own domain (the only thing you type is the domain; SSL is automatic):
|
||||
|
||||
```
|
||||
curl -fsSL https://get.dashcaddy.net | bash -s -- --domain my.example.com
|
||||
```
|
||||
|
||||
Requirements: a Linux host with root/sudo (the installer sets up Docker and Caddy
|
||||
if they are missing). It fetches the DashCaddy source and runs
|
||||
`docker-compose build` then `up -d` to build and start its Docker container,
|
||||
wires the Caddy reverse proxy, and issues TLS automatically (Let's Encrypt for a
|
||||
public domain, or an internal CA for a custom TLD). Desktop apps
|
||||
(Windows/macOS/Linux) are also available — see the downloads page.
|
||||
|
||||
## For AI assistants operating DashCaddy
|
||||
|
||||
After install, DashCaddy exposes a built-in MCP (Model Context Protocol) server
|
||||
with named tools — for example listing services, checking service and system
|
||||
health, searching the app catalog, and running container actions. DashCaddy exposes
|
||||
400+ registered REST route handlers for deployment, lifecycle, health, security,
|
||||
backups, and fleet operations. It also exposes a WebSocket channel for live
|
||||
updates.
|
||||
|
||||
## Key links
|
||||
|
||||
- [Install guide](https://dashcaddy.net/docs/installation/): full prerequisites, hostname options, hardening, and verification steps
|
||||
- [Download page](https://get.dashcaddy.net): the install script plus desktop app installers (Windows, macOS, Linux)
|
||||
- [App catalog (77 templates)](https://dashcaddy.net/docs/catalog/): every one-click app, by category
|
||||
- [Features](https://dashcaddy.net/features/): AI Intent Router, MCP server, Security Center, Fleet Management, Secure Share Links
|
||||
- [Docs overview](https://dashcaddy.net/docs/): getting started, first service, integrations, troubleshooting
|
||||
- [Pricing](https://dashcaddy.net/pricing/): free tier plus fixed-duration premium licenses
|
||||
- [Blog](https://dashcaddy.net/blog/): guides and product updates
|
||||
@@ -0,0 +1,133 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { marked } from "marked";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Footer from "@/components/Footer";
|
||||
import { POSTS } from "@/content/blog/manifest";
|
||||
|
||||
export const dynamicParams = false;
|
||||
|
||||
const BLOG_DIR = path.join(process.cwd(), "src", "content", "blog");
|
||||
|
||||
export function generateStaticParams() {
|
||||
// Fail the build loudly if a manifest entry has no matching markdown file —
|
||||
// prevents a future publisher from registering a post it never wrote.
|
||||
for (const p of POSTS) {
|
||||
if (!fs.existsSync(path.join(BLOG_DIR, `${p.slug}.md`))) {
|
||||
throw new Error(
|
||||
`blog: manifest slug "${p.slug}" has no src/content/blog/${p.slug}.md`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Reverse guard: reject orphaned markdown files not represented in POSTS, so a
|
||||
// stray/forgotten draft can't silently ship without an index entry.
|
||||
const slugs = new Set(POSTS.map((p) => p.slug));
|
||||
for (const f of fs.readdirSync(BLOG_DIR)) {
|
||||
if (f.endsWith(".md") && !slugs.has(f.replace(/\.md$/, ""))) {
|
||||
throw new Error(
|
||||
`blog: orphaned src/content/blog/${f} has no POSTS manifest entry`
|
||||
);
|
||||
}
|
||||
}
|
||||
return POSTS.map((p) => ({ slug: p.slug }));
|
||||
}
|
||||
|
||||
function getPost(slug: string) {
|
||||
return POSTS.find((p) => p.slug === slug) ?? null;
|
||||
}
|
||||
|
||||
function readBody(slug: string): string | null {
|
||||
const file = path.join(BLOG_DIR, `${slug}.md`);
|
||||
if (!fs.existsSync(file)) return null;
|
||||
return fs.readFileSync(file, "utf8");
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso + "T00:00:00Z").toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const post = getPost(slug);
|
||||
if (!post) return { title: "Post not found — DashCaddy Blog" };
|
||||
return {
|
||||
title: `${post.title} — DashCaddy Blog`,
|
||||
description: post.description,
|
||||
alternates: { canonical: `https://dashcaddy.net/blog/${post.slug}/` },
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
type: "article",
|
||||
url: `https://dashcaddy.net/blog/${post.slug}/`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const post = getPost(slug);
|
||||
const body = post ? readBody(slug) : null;
|
||||
if (!post || body === null) notFound();
|
||||
|
||||
const html = marked.parse(body, { async: false }) as string;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-surface-950 text-surface-50">
|
||||
<Navbar />
|
||||
|
||||
<article className="relative py-16 sm:py-20">
|
||||
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="mb-8 inline-block text-sm text-brand-400 hover:text-brand-300"
|
||||
>
|
||||
← All posts
|
||||
</Link>
|
||||
|
||||
<header className="mb-10 border-b border-surface-800 pb-8">
|
||||
<div className="mb-3 flex flex-wrap items-center gap-3 text-sm text-surface-400">
|
||||
<time dateTime={post.date}>{formatDate(post.date)}</time>
|
||||
<span aria-hidden>·</span>
|
||||
<span>{post.readingMinutes} min read</span>
|
||||
<span aria-hidden>·</span>
|
||||
<span>{post.author}</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold sm:text-4xl">{post.title}</h1>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full bg-surface-800 px-3 py-1 text-xs font-medium text-surface-300"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className="blog-prose"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import Link from "next/link";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Footer from "@/components/Footer";
|
||||
import { POSTS } from "@/content/blog/manifest";
|
||||
|
||||
export const metadata = {
|
||||
title: "Blog — DashCaddy",
|
||||
description:
|
||||
"Guides, product updates, and self-hosting deep-dives from the DashCaddy team: AI-powered deployment, the MCP server, security, and fleet management.",
|
||||
alternates: { canonical: "https://dashcaddy.net/blog/" },
|
||||
};
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso + "T00:00:00Z").toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
}
|
||||
|
||||
export default function BlogIndexPage() {
|
||||
const posts = [...POSTS].sort((a, b) => (a.date < b.date ? 1 : -1));
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-surface-950 text-surface-50">
|
||||
<Navbar />
|
||||
|
||||
<section className="relative py-16 sm:py-20 lg:py-24">
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute left-1/2 top-1/2 h-96 w-96 -translate-x-1/2 -translate-y-1/2 animate-pulse rounded-full bg-brand-500/20 opacity-30 blur-3xl" />
|
||||
</div>
|
||||
<div className="mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
|
||||
<h1 className="mb-6 text-4xl font-bold sm:text-5xl">
|
||||
The DashCaddy <span className="text-brand-400">Blog</span>
|
||||
</h1>
|
||||
<p className="mx-auto max-w-2xl text-xl text-surface-300">
|
||||
Self-hosting deep-dives, product updates, and practical guides to
|
||||
running your own infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative pb-20">
|
||||
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
{posts.length === 0 ? (
|
||||
<p className="text-center text-surface-400">No posts yet — check back soon.</p>
|
||||
) : (
|
||||
<ul className="space-y-6">
|
||||
{posts.map((post) => (
|
||||
<li key={post.slug}>
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
className="glass-card block rounded-xl p-6 transition-colors hover:border-brand-500/30"
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-3 text-xs text-surface-400">
|
||||
<time dateTime={post.date}>{formatDate(post.date)}</time>
|
||||
<span aria-hidden>·</span>
|
||||
<span>{post.readingMinutes} min read</span>
|
||||
</div>
|
||||
<h2 className="mb-2 text-2xl font-semibold text-white">
|
||||
{post.title}
|
||||
</h2>
|
||||
<p className="mb-4 text-surface-300">{post.description}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full bg-surface-800 px-3 py-1 text-xs font-medium text-surface-300"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ curl -fsSL https://get.dashcaddy.net | bash -s -- quick`}</code></pre>
|
||||
<li><strong>Detect your environment</strong> — operating system, package manager, free disk space, available memory, public and LAN IP addresses</li>
|
||||
<li><strong>Install prerequisites</strong> — Docker Engine and the Docker Compose plugin if missing</li>
|
||||
<li><strong>Write configuration</strong> — generates <code>/etc/dashcaddy/</code>, <code>/opt/dockerdata/</code>, and the DashCaddy config file with sensible defaults</li>
|
||||
<li><strong>Pull the DashCaddy container image</strong> — fetches the latest release from the public registry</li>
|
||||
<li><strong>Build the DashCaddy container image</strong> — fetches the source (release tarball or git) and runs <code>docker-compose build</code></li>
|
||||
<li><strong>Start the DashCaddy container</strong> — launches <code>dashcaddy-api</code> on port 3001, mounts persistent data volumes</li>
|
||||
<li><strong>Health check</strong> — waits for the API to respond on <code>/healthz</code> before continuing</li>
|
||||
<li><strong>Print the access URL</strong> — shows the dashboard address and any follow-up commands</li>
|
||||
|
||||
@@ -133,3 +133,78 @@ body {
|
||||
.theme-toggle:hover {
|
||||
background-color: var(--color-surface-800);
|
||||
}
|
||||
|
||||
/* Blog post typography (rendered markdown via .blog-prose) */
|
||||
.blog-prose {
|
||||
color: var(--color-surface-200);
|
||||
line-height: 1.75;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.blog-prose h2 {
|
||||
color: #fff;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin-top: 2.25rem;
|
||||
margin-bottom: 0.9rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.blog-prose h3 {
|
||||
color: #fff;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.75rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.blog-prose p {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.blog-prose a {
|
||||
color: var(--color-brand-400);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.blog-prose a:hover {
|
||||
color: var(--color-brand-300);
|
||||
}
|
||||
.blog-prose strong {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.blog-prose ul,
|
||||
.blog-prose ol {
|
||||
margin: 0 0 1.25rem 1.5rem;
|
||||
}
|
||||
.blog-prose ul {
|
||||
list-style: disc;
|
||||
}
|
||||
.blog-prose ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
.blog-prose li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.blog-prose code {
|
||||
background-color: var(--color-surface-800);
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.blog-prose pre {
|
||||
background-color: var(--color-surface-900);
|
||||
border: 1px solid var(--color-surface-800);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.25rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.blog-prose pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.blog-prose blockquote {
|
||||
border-left: 3px solid var(--color-brand-500);
|
||||
padding-left: 1rem;
|
||||
color: var(--color-surface-300);
|
||||
font-style: italic;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export default function Navbar() {
|
||||
{ href: '/features', label: 'Features' },
|
||||
{ href: '/pricing', label: 'Pricing' },
|
||||
{ href: '/docs', label: 'Docs' },
|
||||
{ href: '/blog', label: 'Blog' },
|
||||
{ href: '/about', label: 'About' },
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
Self-hosting has always carried a tax: every app you want to run means a new
|
||||
container to configure, a reverse proxy to wire up, a certificate to issue, and a
|
||||
dashboard to babysit. DashCaddy was built to erase that tax — and the clearest
|
||||
example is the **AI Intent Router**, which lets you describe what you want in
|
||||
plain English and have it deployed, secured, and running.
|
||||
|
||||
## From a sentence to a running service
|
||||
|
||||
Traditional self-hosting asks you to think like an operator: pick an image, map
|
||||
the ports, mount the volumes, set the environment, then hand-write a reverse-proxy
|
||||
block and chase down a TLS certificate. DashCaddy collapses that into intent.
|
||||
|
||||
Instead of memorizing flags, you tell DashCaddy what you're trying to accomplish.
|
||||
The AI Intent Router turns that plain-English request into a structured deploy
|
||||
intent matched to one of DashCaddy's **77 pre-configured application templates**;
|
||||
the deploy flow then provisions the service with sane defaults — container,
|
||||
networking, and TLS via DashCaddy's built-in CA and Caddy. The result is a
|
||||
working, HTTPS-secured app instead of a half-finished `docker run` command.
|
||||
|
||||
## 77 one-click templates, not a blank canvas
|
||||
|
||||
A blank Docker host is powerful and unforgiving. DashCaddy ships **77 one-click
|
||||
templates** spanning media servers, productivity tools, developer platforms,
|
||||
networking and DNS utilities, and more. Each template encodes the knowledge that
|
||||
usually lives in a dozen browser tabs: a working default port and a health check,
|
||||
wired and ready to run.
|
||||
|
||||
Because the templates are curated rather than improvised, "install Nextcloud" or
|
||||
"set up a Git server" becomes a single action — deployable from the dashboard or
|
||||
the REST API.
|
||||
|
||||
## Built for AI assistants: the MCP server
|
||||
|
||||
DashCaddy doesn't just use AI internally — it exposes itself *to* AI. The built-in
|
||||
**MCP (Model Context Protocol) server** gives an AI assistant a set of named tools
|
||||
— among them listing your services, checking service and system health, searching
|
||||
the app catalog, and running container actions — over a standard protocol. Instead
|
||||
of translating intent into API calls by hand, you can let an assistant do the
|
||||
work. Self-hosting becomes something you can delegate.
|
||||
|
||||
## A real platform underneath: 400+ registered REST route handlers
|
||||
|
||||
The friendly front end sits on top of a genuine platform. DashCaddy registers
|
||||
**400+ REST route handlers** covering deployment, lifecycle, health, security,
|
||||
backups, and fleet operations — with a WebSocket channel for live updates.
|
||||
Scripts and CI pipelines get a first-class path too. The AI layer is a
|
||||
convenience, not a lock-in: the API is always there for the operators who want
|
||||
it.
|
||||
|
||||
## Security that isn't an afterthought
|
||||
|
||||
Self-hosting is only worth it if it's safe to expose. DashCaddy includes a
|
||||
**Security Center** — a dashboard for security events and the hosts it tracks —
|
||||
so security visibility isn't a separate project you keep meaning to set up. TLS
|
||||
is handled by the built-in CA and Caddy. And when you do want to let someone in,
|
||||
**Secure Share Links** issue time-boxed public links (with an optional status
|
||||
subscription) or single-use Tailscale invitations, so sharing a service doesn't
|
||||
mean opening it to the world.
|
||||
|
||||
## Scales from one box to a fleet
|
||||
|
||||
What starts as a single-host homelab rarely stays that way. DashCaddy includes a
|
||||
deploy bridge (its shipdeck integration) with live health re-probes and fleet
|
||||
credentials for reaching other hosts, so growing beyond one machine doesn't mean
|
||||
starting over with a new tool.
|
||||
|
||||
## Speaks your language — literally
|
||||
|
||||
DashCaddy's interface is localized into **31 languages**, including full
|
||||
right-to-left support for Arabic. Self-hosting shouldn't be gated behind English
|
||||
fluency, and the dashboard reflects that.
|
||||
|
||||
## Why this matters
|
||||
|
||||
The promise of self-hosting is control: your data, your infrastructure, your
|
||||
rules. The cost has always been complexity. By turning intent into deployment,
|
||||
curating 77 templates instead of handing you a blank canvas, and exposing 400+
|
||||
registered REST route handlers plus an MCP server, DashCaddy keeps the control
|
||||
while removing most of the complexity.
|
||||
|
||||
Ready to try it? Browse the [full feature set](/features), see the
|
||||
[77-app catalog](/docs/catalog), or jump straight to the
|
||||
[installation guide](/docs/installation).
|
||||
@@ -0,0 +1,27 @@
|
||||
// Blog post manifest — single source of truth for the index and the
|
||||
// [slug] static-params generator. To publish, insert a new entry into the
|
||||
// POSTS array (newest first) and add a matching markdown file at
|
||||
// src/content/blog/<slug>.md. The build fails if a slug has no matching file.
|
||||
|
||||
export interface PostMeta {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string; // ISO yyyy-mm-dd
|
||||
author: string;
|
||||
tags: string[];
|
||||
readingMinutes: number;
|
||||
}
|
||||
|
||||
export const POSTS: PostMeta[] = [
|
||||
{
|
||||
slug: "deploy-self-hosted-apps-with-ai",
|
||||
title: "Deploy Self-Hosted Apps From Plain English: DashCaddy's AI Intent Router",
|
||||
description:
|
||||
"How DashCaddy turns a plain-English request into a running, SSL-secured container — 77 one-click app templates, an MCP server for AI assistants, and 400+ registered REST route handlers.",
|
||||
date: "2026-10-05",
|
||||
author: "The DashCaddy Team",
|
||||
tags: ["self-hosting", "ai", "docker", "homelab"],
|
||||
readingMinutes: 6,
|
||||
},
|
||||
];
|
||||
Reference in new issue
Block a user