Skip to content
Merged
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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ Requires secrets: `VERCEL_TOKEN`, `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID`
| `DATABASE_PATH` | `./data/dequel.db` | SQLite database |
| `WORKSPACE_ROOT` | `./workspace` | Build staging |
| `CADDY_ROUTES_DIR` | `./infra/caddy/routes` | Caddy route output |
| `CADDY_BASE_DOMAIN` | `localhost` | Base domain for deployment subdomains. Set to a real domain (e.g. `example.com`) for Let's Encrypt auto-SSL. |
| `CADDY_EMAIL` | _(empty)_ | Email for Let's Encrypt SSL certificate notifications |
| `DOCKER_NETWORK` | `dequel_net` | Docker network for deployments |
| `BUILDKIT_HOST` | `tcp://buildkit:1234` | Buildkit daemon |
| `RAILPACK_BUILD_TIMEOUT_MS` | `1200000` | Build timeout |
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,23 @@ Key environment variables for the API service:
| `DATABASE_PATH` | `./data/dequel.db` | SQLite database location |
| `WORKSPACE_ROOT` | `./workspace` | Build staging directory |
| `CADDY_ROUTES_DIR` | `./infra/caddy/routes` | Caddy route output |
| `CADDY_BASE_DOMAIN` | `localhost` | Base domain for deployment subdomains. Set to a real domain (e.g. `example.com`) for Let's Encrypt auto-SSL. |
| `CADDY_EMAIL` | _(empty)_ | Email for Let's Encrypt SSL certificate notifications |
| `DOCKER_NETWORK` | `dequel_net` | Docker network for deployments |
| `BUILDKIT_HOST` | `tcp://buildkit:1234` | Buildkit daemon address |
| `RAILPACK_BUILD_TIMEOUT_MS` | `1200000` | Build timeout |

## Deployment Ingress

Deployed applications get a subdomain under `localhost`:
Deployed applications get a subdomain under the configured base domain:
```
http://<deploymentId>.localhost
https://<slug>.<CADDY_BASE_DOMAIN>
```

For production, set `CADDY_BASE_DOMAIN` and configure DNS. Caddy
auto-provisions SSL via Let's Encrypt.
When `CADDY_BASE_DOMAIN=localhost` (default), apps are accessible via HTTP at
`http://<slug>.localhost`. For production, set `CADDY_BASE_DOMAIN` to your
domain (e.g. `example.com`) and configure a wildcard DNS `*.example.com`
pointing to your server. Caddy auto-provisions SSL via Let's Encrypt.

## Design Decisions

Expand Down
1 change: 1 addition & 0 deletions apps/api/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions apps/api/src/api/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ export const githubRoutes = new Elysia({ prefix: "/github" })
set.status = 401;
return { error: "Not authenticated" };
}
const webhookUrl = `${config.publicUrl}/api/github/webhook`;
const baseUrl = config.caddyBaseDomain === 'localhost' ? 'http://localhost' : `https://${config.caddyBaseDomain}`;
const webhookUrl = `${baseUrl}/api/github/webhook`;
const integration = await getGithubIntegration();
const secret = integration?.webhookSecret || config.githubWebhookSecret;

Expand Down Expand Up @@ -229,7 +230,8 @@ export const githubRoutes = new Elysia({ prefix: "/github" })
set.status = 401;
return { error: "Not authenticated" };
}
const webhookUrl = `${config.publicUrl}/api/github/webhook`;
const baseUrl = config.caddyBaseDomain === 'localhost' ? 'http://localhost' : `https://${config.caddyBaseDomain}`;
const webhookUrl = `${baseUrl}/api/github/webhook`;
const hooks = await fetchGitHub(`/repos/${params.owner}/${params.repo}/hooks`, token);
const existing = Array.isArray(hooks) ? hooks.find((h: any) => h.config?.url === webhookUrl) : null;
if (!existing) {
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/api/projects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const projectsRoutes = new Elysia()
}
const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 63);
const slug = slugify(project.name);
const domains = [`${slug}.localhost`];
const domains = [`${slug}.${config.caddyBaseDomain}`];
const projectDomains = await listDomains(id);
const verified = projectDomains.filter(d => d.validationStatus === 'verified');
for (const d of verified) {
Expand Down Expand Up @@ -197,7 +197,7 @@ export const projectsRoutes = new Elysia()
}
const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 63);
const slug = slugify(project.name);
const domains = [`${slug}.localhost`];
const domains = [`${slug}.${config.caddyBaseDomain}`];
const projectDomains = await listDomains(id);
const verified = projectDomains.filter(d => d.validationStatus === 'verified');
for (const d of verified) {
Expand Down Expand Up @@ -250,7 +250,7 @@ export const projectsRoutes = new Elysia()
const encoder = new TextEncoder();
const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 63);
const slug = slugify(project.name);
const domains = [`${slug}.localhost`];
const domains = [`${slug}.${config.caddyBaseDomain}`];
const projectDomains = await listDomains(id);
const verified = projectDomains.filter(d => d.validationStatus === 'verified');
for (const d of verified) {
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/orchestrator/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ export const deployContainer = async (
const slug = slugify(opts.projectName || opts.projectId || deploymentId);
const shortId = deploymentId.slice(0, 8);
const containerName = `${slug}-${shortId}`;
const liveUrl = `http://${slug}.localhost`;
const scheme = config.caddyBaseDomain === 'localhost' ? 'http' : 'https';
const liveUrl = `${scheme}://${slug}.${config.caddyBaseDomain}`;

await onLog(`Starting container ${containerName} from image ${imageTag}`);

Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/scaling/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ class ScalingEngine {
targets.push(`deploy-${dep.id}-replica-${i}:${port}`);
}

const caddySnippet = `${slug}.localhost:80 {\n reverse_proxy ${targets.join(' ')}\n}\n`;
const baseDomain = config.caddyBaseDomain === 'localhost' ? `${config.caddyBaseDomain}:80` : config.caddyBaseDomain;
const caddySnippet = `${slug}.${baseDomain} {\n reverse_proxy ${targets.join(' ')}\n}\n`;
await writeFile(routeFile, caddySnippet, 'utf8');

// Reload Caddy
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/utils/__tests__/domain-verifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';

let tmpDir: string;
let routesDir: string;
const testConfig = { caddyRoutesDir: '', appInternalPort: 3000 };
const testConfig = { caddyRoutesDir: '', appInternalPort: 3000, caddyBaseDomain: 'localhost' };

const fileUrl = (path: string) => new URL(path, import.meta.url).toString();
mock.module(fileUrl('../config'), () => ({ config: testConfig }));
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/utils/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface FileConfig {
databasePath?: string;
workspaceRoot?: string;
caddyRoutesDir?: string;
caddyIngressBase?: string;
caddyBaseDomain?: string;
dockerNetwork?: string;
appInternalPort?: number;
buildkitHost?: string;
Expand Down
3 changes: 1 addition & 2 deletions apps/api/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const config = {
databasePath: withFile<string>("DATABASE_PATH", "/app/data/dequel.db"),
workspaceRoot: withFile<string>("WORKSPACE_ROOT", "/app/workspace"),
caddyRoutesDir: withFile<string>("CADDY_ROUTES_DIR", "/caddy/routes"),
caddyIngressBase: withFile<string>("CADDY_INGRESS_BASE", "http://localhost"),
caddyBaseDomain: withFile<string>("CADDY_BASE_DOMAIN", "localhost"),
dockerNetwork: withFile<string>("DOCKER_NETWORK", "dequel_net"),
appInternalPort: withFile<number>("APP_INTERNAL_PORT", "3000", Number),
buildkitHost: withFile<string>("BUILDKIT_HOST", "tcp://buildkit:1234"),
Expand All @@ -36,7 +36,6 @@ export const config = {
smtpPass: withFile<string>("SMTP_PASS", ""),
smtpFrom: withFile<string>("SMTP_FROM", "dequel@localhost"),
alertEvalIntervalMs: withFile<number>("ALERT_EVAL_INTERVAL_MS", "60000", Number),
publicUrl: withFile<string>("PUBLIC_URL", "http://localhost"),
githubClientId: withFile<string>("GITHUB_CLIENT_ID", ""),
githubClientSecret: withFile<string>("GITHUB_CLIENT_SECRET", ""),
githubAppName: withFile<string>("GITHUB_APP_NAME", "Dequel"),
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/utils/domain-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ export const buildCaddySnippet = async (
listDomainsFn: typeof listDomains = listDomains,
appPort?: number,
): Promise<string> => {
let domains = [`${slug}.localhost:80`];
const baseDomain = config.caddyBaseDomain === 'localhost' ? `${config.caddyBaseDomain}:80` : config.caddyBaseDomain;
let domains = [`${slug}.${baseDomain}`];
let port = appPort ?? config.appInternalPort;

if (projectId) {
Expand Down
20 changes: 18 additions & 2 deletions apps/docs/src/content/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ After installation, start the platform:
dequel start
```

Open `http://localhost` to access the dashboard.
Open `http://localhost` to access the dashboard (or the configured `CADDY_BASE_DOMAIN` in production).

## Manual Setup (no install script)

Expand Down Expand Up @@ -64,7 +64,7 @@ curl -fsSL https://raw.githubusercontent.com/Lftobs/dequel/main/infra/monitoring
docker compose -f ~/.dequel/docker-compose.yml up -d
```

The compose file uses pre-built images from GitHub Container Registry, so no source code checkout is needed. Access the dashboard at `http://localhost`.
The compose file uses pre-built images from GitHub Container Registry, so no source code checkout is needed. Access the dashboard at `http://localhost` (or your configured `CADDY_BASE_DOMAIN`).

## Manual Setup with Docker Compose (with source)

Expand All @@ -76,6 +76,21 @@ cd dequel
docker compose up -d --build
```

## Production Setup

To make Dequel publicly accessible with auto-SSL:

1. Set `CADDY_BASE_DOMAIN` and `CADDY_EMAIL` in `~/.dequel/.env`:
```
CADDY_BASE_DOMAIN=example.com
CADDY_EMAIL=admin@example.com
```
2. Configure a wildcard DNS `*.example.com` pointing to your server's IP.
3. Ensure ports `80` and `443` are open in your firewall.
4. Restart Dequel: `dequel restart`

Deployed apps will be reachable at `https://<slug>.example.com` with auto-provisioned Let's Encrypt certificates. The dashboard is available at both `http://example.com` and `https://example.com`.

## The `dequel` CLI

The `dequel` command manages the platform lifecycle:
Expand Down Expand Up @@ -119,6 +134,7 @@ For local development, run the API and web dashboard directly:
export DATABASE_PATH=./data/dequel.db \
WORKSPACE_ROOT=./workspace \
CADDY_ROUTES_DIR=./infra/caddy/routes \
CADDY_BASE_DOMAIN=localhost \
DOCKER_NETWORK=dequel_net \
APP_INTERNAL_PORT=3000
bun apps/api/src/index.ts
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ In this guide, we'll configure a project, push the codebase, and verify that the

## Step 1: Create a Project

Navigate to the Dequel dashboard (normally hosted at `localhost:5173`) and click the **+ Create Project** button in the upper right corner.
Navigate to the Dequel dashboard (at `http://localhost` or your configured `CADDY_BASE_DOMAIN`) and click the **+ Create Project** button in the upper right corner.

- Enter a unique project name (e.g. `my-web-app`).
- Configure CPU limit boundaries (e.g., `0.5 cores`) and Memory bounds (e.g., `512MB`).
Expand Down
16 changes: 16 additions & 0 deletions apps/docs/src/content/docs/system-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ Config file equivalent:

On boot, these values seed the `smtp_settings` table. The password is encrypted at rest using `ENV_ENCRYPTION_KEY`. You can also update these from the Settings page in the dashboard, and send a test email to verify the configuration.

### Ingress

| Variable | Default | Description |
|----------|---------|-------------|
| `CADDY_BASE_DOMAIN` | `localhost` | Base domain for deployment subdomains. Deployed apps are reachable at `https://<slug>.<domain>`. GitHub webhook URLs are derived from this automatically. Set to a real domain (e.g. `example.com`) and configure wildcard DNS for auto-SSL. |
| `CADDY_EMAIL` | `""` | Email address for Let's Encrypt SSL certificate expiration notifications |

Config file equivalent:

```json
{
"caddyBaseDomain": "example.com",
"caddyEmail": "admin@example.com"
}
```

## Full Config File Example

```json
Expand Down
6 changes: 5 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ services:
DATABASE_PATH: /app/data/dequel.db
WORKSPACE_ROOT: /app/workspace
CADDY_ROUTES_DIR: /caddy/routes
CADDY_INGRESS_BASE: http://localhost
CADDY_BASE_DOMAIN: ${CADDY_BASE_DOMAIN:-localhost}
DOCKER_NETWORK: dequel_net
APP_INTERNAL_PORT: 3000
BUILDKIT_HOST: tcp://buildkit:1234
Expand Down Expand Up @@ -103,8 +103,12 @@ services:
"caddyfile",
"--watch",
]
environment:
CADDY_EMAIL: ${CADDY_EMAIL:-}
CADDY_BASE_DOMAIN: ${CADDY_BASE_DOMAIN:-localhost}
ports:
- "80:80"
- "443:443"
volumes:
- ./infra/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./infra/caddy/routes:/etc/caddy/routes
Expand Down
29 changes: 28 additions & 1 deletion infra/caddy/Caddyfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
email support@mini-cms.xyz
email {$CADDY_EMAIL}
}

import /etc/caddy/routes/*.caddy
Expand Down Expand Up @@ -30,3 +30,30 @@ import /etc/caddy/routes/*.caddy
reverse_proxy web:3000
}
}

{$CADDY_BASE_DOMAIN:localhost}:443 {
log {
output stdout
format json
}

encode zstd gzip

handle /api/* {
reverse_proxy api:3001
}

handle /metrics* {
reverse_proxy prometheus:9090
}

redir /grafana /grafana/

handle /grafana/* {
reverse_proxy grafana:3000
}

handle {
reverse_proxy web:3000
}
}
Loading