diff --git a/apps/docs/blume.config.ts b/apps/docs/blume.config.ts
index c70d4ea..17204d0 100644
--- a/apps/docs/blume.config.ts
+++ b/apps/docs/blume.config.ts
@@ -82,4 +82,30 @@ export default defineConfig({
deployment: {
site: "https://dequel.intrep.xyz",
},
+ seo: {
+ og: {
+ fonts: [],
+ palette: {
+ accent: "#ea580c",
+ background: "#0a0a0c",
+ border: "#222228",
+ foreground: "#f4f4f5",
+ muted: "#a1a1aa",
+ },
+ titles: {
+ "/": "Dequel — Self-Hosted PaaS Engine & Container Platform",
+ },
+ },
+ software: {
+ name: "Dequel",
+ operatingSystem: "Linux",
+ applicationCategory: "DeveloperApplication",
+ description:
+ "Self-hosted PaaS container platform. Deploy apps from Git, ZIP, or Docker Compose with zero infrastructure setup.",
+ },
+ organization: {
+ name: "Dequel",
+ url: "https://dequel.intrep.xyz",
+ },
+ },
});
diff --git a/apps/docs/components/SEO.astro b/apps/docs/components/SEO.astro
new file mode 100644
index 0000000..3634a2c
--- /dev/null
+++ b/apps/docs/components/SEO.astro
@@ -0,0 +1,130 @@
+---
+interface Props {
+ title?: string;
+ description?: string;
+ canonicalUrl?: string;
+ ogImage?: string;
+ ogType?: string;
+ keywords?: string[];
+ themeColor?: string;
+}
+
+const {
+ title = "Dequel — Self-Hosted PaaS Engine & Container Platform",
+ description = "The open-source, self-hosted PaaS container platform. Deploy apps from Git, ZIP, or Docker Compose with automatic SSL, multi-server scaling, and zero infrastructure lock-in.",
+ canonicalUrl = "https://dequel.intrep.xyz/",
+ ogImage = "https://dequel.intrep.xyz/og/index.png",
+ ogType = "website",
+ keywords = [
+ "Dequel",
+ "self-hosted PaaS",
+ "PaaS platform",
+ "Docker deployment",
+ "container platform",
+ "DevOps",
+ "Caddy reverse proxy",
+ "automatic SSL",
+ "Let's Encrypt",
+ "BuildKit",
+ "Railpack",
+ "zero vendor lock-in",
+ "homelab",
+ "multi-server deployment",
+ "microservices",
+ ],
+ themeColor = "#0a0a0c",
+} = Astro.props;
+
+const siteName = "Dequel";
+const author = "Dequel";
+
+const structuredData = {
+ "@context": "https://schema.org",
+ "@graph": [
+ {
+ "@type": "WebSite",
+ "@id": "https://dequel.intrep.xyz/#website",
+ "name": "Dequel",
+ "url": "https://dequel.intrep.xyz/",
+ "description": description,
+ "publisher": {
+ "@type": "Organization",
+ "name": "Dequel",
+ "url": "https://dequel.intrep.xyz/",
+ "logo": {
+ "@type": "ImageObject",
+ "url": "https://dequel.intrep.xyz/logo.svg"
+ }
+ }
+ },
+ {
+ "@type": "SoftwareApplication",
+ "@id": "https://dequel.intrep.xyz/#software",
+ "name": "Dequel",
+ "applicationCategory": "DeveloperApplication",
+ "operatingSystem": "Linux",
+ "offers": {
+ "@type": "Offer",
+ "price": "0",
+ "priceCurrency": "USD"
+ },
+ "url": "https://dequel.intrep.xyz/",
+ "description": description,
+ "image": ogImage,
+ "screenshot": "https://dequel.intrep.xyz/screenshots/dashboard.png",
+ "featureList": [
+ "Git push-to-deploy and ZIP uploads",
+ "Docker Compose deployment",
+ "Automatic SSL certificate provisioning via Caddy and Let's Encrypt",
+ "Dynamic multi-server container clustering",
+ "Zero-dependency self-hosted architecture",
+ "Full observability with Prometheus, cAdvisor, and Loki",
+ "Automatic horizontal scaling and auto-healing"
+ ]
+ }
+ ]
+};
+---
+
+
+
+
{title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/docs/docs/installation.md b/apps/docs/docs/installation.md
index 49c8bfd..0e87e89 100644
--- a/apps/docs/docs/installation.md
+++ b/apps/docs/docs/installation.md
@@ -21,6 +21,12 @@ or on additional servers you connect later (see
Run the install script:
+```bash
+curl -fsSL https://dequel.intrep.xyz/install.sh | bash
+```
+
+Alternatively, download directly from the latest GitHub release:
+
```bash
curl -fsSL https://github.com/Lftobs/dequel/releases/latest/download/install.sh | bash
```
diff --git a/apps/docs/package.json b/apps/docs/package.json
index cf9d510..1cb27bc 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -3,6 +3,7 @@
"type": "module",
"version": "0.4.0",
"scripts": {
+ "prebuild": "node -e \"const fs=require('fs');if(fs.existsSync('../../scripts/install.sh'))fs.copyFileSync('../../scripts/install.sh','./public/install.sh');\"",
"dev": "blume dev",
"start": "blume dev",
"build": "blume build",
diff --git a/apps/docs/pages/index.astro b/apps/docs/pages/index.astro
index 497a883..7c9ff47 100644
--- a/apps/docs/pages/index.astro
+++ b/apps/docs/pages/index.astro
@@ -1,5 +1,6 @@
---
import "../styles/global.css";
+import SEO from "../components/SEO.astro";
import Hero from "../components/Hero.astro";
import Stats from "../components/Stats.astro";
import Comparison from "../components/Comparison.astro";
@@ -12,15 +13,7 @@ import CTA from "../components/CTA.astro";
-
-
-
+
-
-
- Dequel — Self-Hosted PaaS Engine & Container Platform
-
diff --git a/apps/docs/public/install.sh b/apps/docs/public/install.sh
new file mode 100755
index 0000000..646da4a
--- /dev/null
+++ b/apps/docs/public/install.sh
@@ -0,0 +1,266 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+REPO="Lftobs/dequel"
+VERSION="${VERSION:-latest}"
+INSTALL_DIR="${INSTALL_DIR:-$HOME/.dequel}"
+
+BOLD='\033[1m'
+DIM='\033[2m'
+GREEN='\033[0;32m'
+AMBER='\033[0;33m'
+RED='\033[0;31m'
+NC='\033[0m'
+
+header() { printf "\n${BOLD}${AMBER}==>${NC}${BOLD} %s${NC}\n" "$*"; }
+info() { printf " ${DIM}%s${NC}\n" "$*"; }
+success() { printf " ${GREEN}✓${NC} %s\n" "$*"; }
+warn() { printf " ${AMBER}⚠${NC} %s\n" "$*"; }
+fail() { printf " ${RED}✗${NC} %s\n" "$*"; exit 1; }
+
+download_if_missing() {
+ local src="$1" dst="$2"
+ if [ -f "$dst" ]; then
+ info " Skipping (exists): $dst"
+ else
+ http_code=$(curl -fsSL -w "%{http_code}" "$src" -o "$dst" 2>/dev/null) || {
+ warn "Failed to download $src (HTTP $http_code)"
+ return 1
+ }
+ success "Downloaded: $dst"
+ fi
+}
+
+check_prerequisites() {
+ header "Checking prerequisites"
+
+ if command -v docker >/dev/null 2>&1; then
+ success "Docker found: $(docker --version)"
+ else
+ fail "Docker is not installed. See https://docs.docker.com/engine/install/"
+ fi
+
+ if docker info >/dev/null 2>&1; then
+ success "Docker socket accessible"
+ else
+ warn "Docker socket not accessible — attempting to add user to docker group"
+ if sudo usermod -aG docker "$USER" 2>/dev/null; then
+ success "Added $USER to docker group"
+ warn "Log out and back in (or run 'newgrp docker') for the change to take effect"
+ fail "Please log out and back in, then re-run the install script"
+ else
+ warn "Could not add user to docker group automatically"
+ warn "Run this manually: sudo usermod -aG docker $USER && newgrp docker"
+ fi
+ fi
+
+ if docker compose version >/dev/null 2>&1; then
+ success "Docker Compose found: $(docker compose version)"
+ COMPOSE_CMD="docker compose"
+ elif docker-compose --version >/dev/null 2>&1; then
+ success "docker-compose found: $(docker-compose --version)"
+ warn "Consider upgrading to 'docker compose' (Docker Compose v2)"
+ COMPOSE_CMD="docker-compose"
+ else
+ fail "Docker Compose is not installed. See https://docs.docker.com/compose/install/"
+ fi
+}
+
+setup_directories() {
+ header "Setting up installation directory"
+ mkdir -p "$INSTALL_DIR/data" "$INSTALL_DIR/workspace" "$INSTALL_DIR/infra/caddy/routes" "$INSTALL_DIR/infra/monitoring/grafana/datasources" "$INSTALL_DIR/infra/monitoring/grafana/dashboards" "$INSTALL_DIR/scripts/auth"
+ info "Installing to: $INSTALL_DIR"
+}
+
+resolve_base_url() {
+ header "Downloading configuration"
+ TAG=""
+
+ if [ "$VERSION" = "pre" ] || [ "$VERSION" = "prerelease" ]; then
+ local release_url="https://api.github.com/repos/$REPO/releases"
+ info "Fetching latest pre-release..."
+ TAG=$(curl -fsSL "$release_url" | grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/') || true
+ elif [ "$VERSION" = "latest" ]; then
+ local release_url="https://api.github.com/repos/$REPO/releases/latest"
+ info "Fetching latest stable release..."
+ TAG=$(curl -fsSL "$release_url" | grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/') || true
+ else
+ TAG="${VERSION#v}"
+ TAG="v$TAG"
+ fi
+
+ if [ -z "$TAG" ]; then
+ warn "Could not determine release. Falling back to 'main' branch."
+ BASE_URL="https://raw.githubusercontent.com/$REPO/main"
+ else
+ BASE_URL="https://raw.githubusercontent.com/$REPO/$TAG"
+ success "Target release: $TAG"
+ fi
+}
+
+download_configs() {
+ resolve_base_url
+
+ if [ -n "$TAG" ]; then
+ local version="${TAG#v}"
+ local tarball_url="https://github.com/$REPO/releases/download/$TAG/dequel-config-$version.tar.gz"
+ local tarball_path=$(mktemp)
+ info "Downloading config bundle..."
+ set +e
+ http_code=$(curl -fsSL -w "%{http_code}" "$tarball_url" -o "$tarball_path" 2>/dev/null)
+ curl_ok=$?
+ set -e
+ if [ "$curl_ok" -eq 0 ] && [ "$http_code" = "200" ]; then
+ tar -xzf "$tarball_path" -C "$INSTALL_DIR"
+ rm -f "$tarball_path"
+ chmod +x "$INSTALL_DIR/dequel" 2>/dev/null || true
+ success "Downloaded and extracted config bundle"
+ return
+ fi
+ rm -f "$tarball_path"
+ warn "Could not download config bundle (HTTP $http_code), falling back to individual files"
+ fi
+
+ download_if_missing "$BASE_URL/docker-compose.yml" "$INSTALL_DIR/docker-compose.yml"
+ download_if_missing "$BASE_URL/infra/caddy/Caddyfile" "$INSTALL_DIR/infra/caddy/Caddyfile"
+ download_if_missing "$BASE_URL/scripts/dequel" "$INSTALL_DIR/dequel"
+ download_if_missing "$BASE_URL/scripts/auth/pam-server.py" "$INSTALL_DIR/scripts/auth/pam-server.py"
+
+ for f in prometheus.yml loki-config.yml promtail-config.yml; do
+ download_if_missing "$BASE_URL/infra/monitoring/$f" "$INSTALL_DIR/infra/monitoring/$f"
+ done
+
+ for f in loki.yml prometheus.yml; do
+ download_if_missing "$BASE_URL/infra/monitoring/grafana/datasources/$f" "$INSTALL_DIR/infra/monitoring/grafana/datasources/$f"
+ done
+
+ for f in dashboards.yml deployed-apps.json; do
+ download_if_missing "$BASE_URL/infra/monitoring/grafana/dashboards/$f" "$INSTALL_DIR/infra/monitoring/grafana/dashboards/$f"
+ done
+}
+
+prompt_config() {
+ header "Configuration"
+
+ local ADMIN_EMAIL=""
+ local HOSTNAME=""
+
+ if [ -t 0 ]; then
+ read -r -p " Admin email (for SSL notifications, optional): " ADMIN_EMAIL
+ read -r -p " Base domain (e.g. dequel.example.com, optional): " HOSTNAME
+ elif (: /dev/null; then
+ read -r -p " Admin email (for SSL notifications, optional): " ADMIN_EMAIL < /dev/tty
+ read -r -p " Base domain (e.g. dequel.example.com, optional): " HOSTNAME < /dev/tty
+ else
+ warn "No terminal — skipping configuration prompt"
+ warn "Set CADDY_EMAIL and CADDY_BASE_DOMAIN in $INSTALL_DIR/.env after install"
+ return
+ fi
+
+ local ENC_KEY
+ ENC_KEY=$(openssl rand -hex 32 2>/dev/null || dd if=/dev/urandom bs=32 count=1 status=none 2>/dev/null | od -A n -t x1 | tr -d ' \n' || fail "Cannot generate encryption key — openssl and dd both failed")
+
+ local PG_PASS
+ PG_PASS=$(openssl rand -hex 16 2>/dev/null || dd if=/dev/urandom bs=16 count=1 status=none 2>/dev/null | od -A n -t x1 | tr -d ' \n' || fail "Cannot generate database password")
+
+ cat > "$INSTALL_DIR/data/dequel.json" < "$INSTALL_DIR/.env"
+ chmod 600 "$INSTALL_DIR/.env"
+ success "Created $INSTALL_DIR/.env"
+}
+
+pull_images() {
+ header "Pulling Docker images"
+ $COMPOSE_CMD -f "$INSTALL_DIR/docker-compose.yml" pull || warn "Some images could not be pulled (they will be built on first run)"
+}
+
+install_cli() {
+ header "Installing dequel CLI"
+ local cli_src="$INSTALL_DIR/dequel"
+ if [ ! -f "$cli_src" ]; then
+ download_if_missing "$BASE_URL/scripts/dequel" "$cli_src"
+ fi
+ chmod +x "$cli_src"
+
+ if [ -w /usr/local/bin ]; then
+ ln -sf "$cli_src" /usr/local/bin/dequel
+ success "Installed: /usr/local/bin/dequel"
+ else
+ info "Installed at: $cli_src"
+ warn "Add to PATH manually: sudo ln -s $cli_src /usr/local/bin/dequel"
+ fi
+}
+
+send_install_telemetry() {
+ [ "${DEQUEL_TELEMETRY_DISABLED:-0}" = "1" ] && return 0
+ local os arch posthog_key
+ os=$(uname -s 2>/dev/null || echo "unknown")
+ arch=$(uname -m 2>/dev/null || echo "unknown")
+ posthog_key="${DEQUEL_POSTHOG_KEY:-phc_dequel_telemetry_public_key}"
+
+ curl -fsSL -X POST "https://us.i.posthog.com/capture/" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "api_key": "'"$posthog_key"'",
+ "event": "installation_completed",
+ "distinct_id": "'"$os-$arch-$(date +%s)"'",
+ "properties": {
+ "os": "'"$os"'",
+ "arch": "'"$arch"'",
+ "version": "'"${TAG:-latest}"'"
+ }
+ }' >/dev/null 2>&1 || true
+}
+
+print_summary() {
+ echo ""
+ echo ""
+ success "${BOLD}Dequel installed${NC}"
+ echo ""
+ echo " ${BOLD}Config dir:${NC} $INSTALL_DIR"
+ echo ""
+ echo " ${BOLD}Usage:${NC}"
+ echo " dequel start Start the Dequel platform"
+ echo " dequel stop Stop all services"
+ echo " dequel status Show service status"
+ echo " dequel logs Follow logs"
+ echo " dequel update Pull latest images and restart"
+ echo " dequel --help Show all commands"
+ echo ""
+ echo " Run ${BOLD}dequel start${NC} to get going."
+ echo ""
+}
+
+main() {
+ echo ""
+ echo "${BOLD} Dequel Installer${NC}"
+ echo ""
+
+ check_prerequisites
+ setup_directories
+ download_configs
+ prompt_config
+ pull_images
+ install_cli
+ send_install_telemetry
+ print_summary
+}
+
+main
+
diff --git a/apps/docs/public/og.png b/apps/docs/public/og.png
new file mode 100644
index 0000000..ffa9d9d
Binary files /dev/null and b/apps/docs/public/og.png differ
diff --git a/apps/docs/vercel.json b/apps/docs/vercel.json
index caa6849..8372a09 100644
--- a/apps/docs/vercel.json
+++ b/apps/docs/vercel.json
@@ -2,5 +2,17 @@
"framework": null,
"buildCommand": "bun run build",
"outputDirectory": "dist",
- "installCommand": "bun install"
+ "installCommand": "bun install",
+ "rewrites": [
+ { "source": "/install", "destination": "/install.sh" }
+ ],
+ "headers": [
+ {
+ "source": "/install.sh",
+ "headers": [
+ { "key": "Content-Type", "value": "text/plain; charset=utf-8" },
+ { "key": "Cache-Control", "value": "public, max-age=300, s-maxage=300" }
+ ]
+ }
+ ]
}