Skip to content

Commit 573523f

Browse files
committed
feat(ops): add caddy gateway and fix kb dashboard self-host setup
1 parent f22dc77 commit 573523f

13 files changed

Lines changed: 252 additions & 2 deletions

File tree

.env.example

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ IAA_WS_OWNER_TTL_SECONDS=120
2424
IAA_WS_OUTBOX_TTL_SECONDS=300
2525
IAA_WS_TOOL_RESULT_TTL_SECONDS=300
2626
IAA_KNOWLEDGE_BASES_BASE_URL=http://kb-service:8100
27+
# Required. Must be a random secret shared with KBS_SERVICE_JWT_SIGNING_KEY.
28+
# If left as the placeholder value, dashboard KB endpoints will fail.
2729
IAA_KNOWLEDGE_BASES_SIGNING_KEY=change-me-kb-service-signing-key
2830
IAA_KNOWLEDGE_BASES_AUDIENCE=kb-service
2931
IAA_KNOWLEDGE_BASES_JWT_ALGORITHM=HS256
@@ -42,6 +44,7 @@ DATABASE_URL=postgresql://agent:postgres@db:5432/agent
4244
# Knowledge base service
4345
KBS_DATABASE_URL=postgresql+asyncpg://agent:postgres@kb-db:5432/knowledge_bases
4446
KBS_POSTGRES_DB=knowledge_bases
47+
# Required. Must match IAA_KNOWLEDGE_BASES_SIGNING_KEY.
4548
KBS_SERVICE_JWT_SIGNING_KEY=change-me-kb-service-signing-key
4649
KBS_SERVICE_JWT_AUDIENCE=kb-service
4750
KBS_ENCRYPTION_KEY=change-me-generate-with-python-fernet
@@ -62,3 +65,23 @@ GATEWAY_HTTPS_BIND_ADDR=0.0.0.0
6265
GATEWAY_HTTPS_BIND_PORT=443
6366
RESOLVEKIT_PUBLIC_HOST=support.example.com
6467
LETSENCRYPT_EMAIL=devops@example.com
68+
69+
# Optional: dedicated Dockerized Caddy gateway (`infra/caddy`).
70+
# This mode supports one main host + explicit `www`/`dash` + `api` hosts,
71+
# while keeping path routing (`/agent/*`, `/v1/*`) available on the main host.
72+
CADDY_DOCKER_NETWORK=resolvekit_default
73+
CADDY_HTTP_BIND=0.0.0.0:80
74+
CADDY_HTTPS_BIND=0.0.0.0:443
75+
CADDY_LOCAL_BIND=127.0.0.1:8080
76+
77+
# Public domains served by infra/caddy/Caddyfile
78+
CADDY_PRIMARY_HOST=support.example.com
79+
CADDY_WWW_HOST=www.support.example.com
80+
CADDY_DASH_HOST=dash.support.example.com
81+
CADDY_API_HOST=api.support.example.com
82+
83+
# Internal upstream targets (override only if your service/container names differ)
84+
CADDY_DASHBOARD_UPSTREAM=resolvekit_dashboard:3000
85+
CADDY_API_UPSTREAM=resolvekit_api:3002
86+
CADDY_BACKEND_UPSTREAM=resolvekit_backend:8000
87+
CADDY_KB_UPSTREAM=resolvekit_kb_service:8100

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ ResolveKit Backend provides the server-side runtime for embedded app assistants.
2121
- `cp .env.local-deploy.example .env.local-deploy`
2222
- configure your public hostname + Let's Encrypt values in `.env.local-deploy`
2323
- `docker compose -f docker-compose.local-deploy.yml --env-file .env --env-file .env.local-deploy up -d --build`
24+
5. Optional standalone Dockerized Caddy gateway:
25+
- configure `CADDY_*` domain/bind values in `.env`
26+
- `docker compose -f infra/caddy/docker-compose.yml up -d`
27+
6. Optional Caddy from main compose (recommended if you want one command family):
28+
- configure `CADDY_*` values in `.env`
29+
- `docker compose --profile gateway up -d`
2430

2531
## Deployment Modes
2632

dashboard/next.config.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
import type { NextConfig } from "next";
22

3+
const allowedDevOrigins = Array.from(
4+
new Set(
5+
[
6+
process.env.CADDY_PRIMARY_HOST,
7+
process.env.CADDY_WWW_HOST,
8+
process.env.CADDY_DASH_HOST,
9+
process.env.CADDY_API_HOST,
10+
process.env.RESOLVEKIT_PUBLIC_HOST,
11+
process.env.RESOLVEKIT_CONSOLE_HOST,
12+
process.env.RESOLVEKIT_API_HOST,
13+
process.env.RESOLVEKIT_AGENT_HOST,
14+
process.env.NEXT_ALLOWED_DEV_ORIGINS,
15+
]
16+
.filter(Boolean)
17+
.flatMap((value) => String(value).split(","))
18+
.map((value) => value.trim())
19+
.filter(Boolean),
20+
),
21+
);
22+
323
const nextConfig: NextConfig = {
424
reactStrictMode: false,
525
// Dashboard and API run separate Next dev processes from the same source tree.
626
// Allow overriding distDir so they do not contend on a shared .next cache.
727
distDir: process.env.NEXT_DIST_DIR ?? ".next",
28+
...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}),
829
};
930

1031
export default nextConfig;

dashboard/src/app/icon.svg

Lines changed: 13 additions & 0 deletions
Loading

dashboard/src/app/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import "./globals.css";
55
export const metadata: Metadata = {
66
title: "ResolveKit Dashboard",
77
description: "ResolveKit control plane",
8+
icons: {
9+
icon: "/icon.svg",
10+
shortcut: "/icon.svg",
11+
apple: "/icon.svg",
12+
},
813
};
914

1015
export default function RootLayout({

dashboard/src/lib/server/kb-service.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,17 @@ async function doFetch(path: string, init: RequestInit): Promise<Response> {
125125
}
126126

127127
async function callInternal(path: string, payload: Record<string, unknown>, ctx: ActorContext): Promise<Record<string, unknown>> {
128-
const token = await buildServiceToken(ctx);
128+
let token: string;
129+
try {
130+
token = await buildServiceToken(ctx);
131+
} catch (error) {
132+
console.error("KB token generation failed", error);
133+
throw new KBServiceError({
134+
status: 500,
135+
detail: "Knowledge base integration is misconfigured",
136+
code: "kb_auth_misconfigured",
137+
});
138+
}
129139
const response = await doFetch(path, {
130140
method: "POST",
131141
headers: {
@@ -155,7 +165,17 @@ export async function callInternalMultipart(
155165
file: { filename: string; content: Uint8Array; contentType: string },
156166
ctx: ActorContext,
157167
): Promise<Record<string, unknown>> {
158-
const token = await buildServiceToken(ctx);
168+
let token: string;
169+
try {
170+
token = await buildServiceToken(ctx);
171+
} catch (error) {
172+
console.error("KB token generation failed", error);
173+
throw new KBServiceError({
174+
status: 500,
175+
detail: "Knowledge base integration is misconfigured",
176+
code: "kb_auth_misconfigured",
177+
});
178+
}
159179
const form = new FormData();
160180
for (const [key, value] of Object.entries(fields)) {
161181
form.set(key, value);

docker-compose.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ services:
109109
working_dir: /app
110110
depends_on:
111111
- api
112+
env_file: .env
112113
command: >
113114
sh -lc "set -e;
114115
rm -rf .next-dashboard;
@@ -146,6 +147,7 @@ services:
146147
working_dir: /app
147148
depends_on:
148149
- backend
150+
env_file: .env
149151
command: >
150152
sh -lc "set -e;
151153
rm -rf .next-api;
@@ -177,6 +179,29 @@ services:
177179
networks:
178180
- resolvekit
179181

182+
caddy:
183+
image: caddy:2.8.4
184+
container_name: resolvekit_caddy
185+
restart: unless-stopped
186+
profiles: ["gateway"]
187+
depends_on:
188+
- backend
189+
- kb-service
190+
- dashboard
191+
- api
192+
env_file: .env
193+
ports:
194+
- "${CADDY_HTTP_BIND:-0.0.0.0:80}:80"
195+
- "${CADDY_HTTPS_BIND:-0.0.0.0:443}:443"
196+
- "${CADDY_HTTPS_BIND:-0.0.0.0:443}:443/udp"
197+
- "${CADDY_LOCAL_BIND:-127.0.0.1:8080}:8080"
198+
volumes:
199+
- ./infra/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
200+
- ./infra/caddy/data:/data
201+
- ./infra/caddy/config:/config
202+
networks:
203+
- resolvekit
204+
180205
networks:
181206
resolvekit:
182207
name: resolvekit_default

docs/backend/runbooks/local-dev-and-docker.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@
99

1010
## Local deploy notes
1111

12+
### Optional Dockerized Caddy gateway
13+
14+
If you want Caddy separate from the main compose stack, use `infra/caddy`:
15+
16+
- configure `CADDY_PRIMARY_HOST`, `CADDY_WWW_HOST`, `CADDY_DASH_HOST`, `CADDY_API_HOST`, and `LETSENCRYPT_EMAIL` in `.env`
17+
- start gateway: `docker compose -f infra/caddy/docker-compose.yml up -d`
18+
- this gateway reads `.env` directly and proxies to the existing local Docker services on `resolvekit_default`
19+
20+
If you prefer keeping everything in the main compose file, use:
21+
22+
- `docker compose --profile gateway up -d`
23+
24+
This starts the same Caddy config via the `caddy` service in `docker-compose.yml`.
25+
1226
### Single-host quickstart
1327

1428
The local deploy templates are optimized for one public host:

infra/caddy/Caddyfile

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
email {$LETSENCRYPT_EMAIL}
3+
servers {
4+
# Keep original client IP/proto when running behind another private edge.
5+
trusted_proxies static private_ranges
6+
}
7+
}
8+
9+
(resolvekit_routes) {
10+
encode zstd gzip
11+
12+
@agent path /agent /agent/*
13+
handle @agent {
14+
uri strip_prefix /agent
15+
reverse_proxy {$CADDY_BACKEND_UPSTREAM:resolvekit_backend:8000} {
16+
flush_interval -1
17+
transport http {
18+
read_timeout 0
19+
write_timeout 0
20+
}
21+
}
22+
}
23+
24+
@kb path /kb /kb/*
25+
handle @kb {
26+
uri strip_prefix /kb
27+
reverse_proxy {$CADDY_KB_UPSTREAM:resolvekit_kb_service:8100}
28+
}
29+
30+
@api_v1 path /v1 /v1/*
31+
handle @api_v1 {
32+
reverse_proxy {$CADDY_API_UPSTREAM:resolvekit_api:3002}
33+
}
34+
35+
@api_alias path /api /api/*
36+
handle @api_alias {
37+
uri strip_prefix /api
38+
reverse_proxy {$CADDY_API_UPSTREAM:resolvekit_api:3002}
39+
}
40+
41+
handle {
42+
reverse_proxy {$CADDY_DASHBOARD_UPSTREAM:resolvekit_dashboard:3000}
43+
}
44+
}
45+
46+
{$CADDY_PRIMARY_HOST:support.example.com}, {$CADDY_WWW_HOST:www.support.example.com}, {$CADDY_DASH_HOST:dash.support.example.com} {
47+
import resolvekit_routes
48+
}
49+
50+
{$CADDY_API_HOST:api.support.example.com} {
51+
reverse_proxy {$CADDY_BACKEND_UPSTREAM:resolvekit_backend:8000} {
52+
flush_interval -1
53+
transport http {
54+
read_timeout 0
55+
write_timeout 0
56+
}
57+
}
58+
}
59+
60+
:8080 {
61+
import resolvekit_routes
62+
}

infra/caddy/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Caddy Gateway (Docker)
2+
3+
This stack runs Caddy as an external reverse proxy for the local OSS Docker services.
4+
5+
## Start
6+
7+
1. Ensure backend stack is up:
8+
- `docker compose up -d`
9+
2. Configure domains in root `.env`:
10+
- `CADDY_PRIMARY_HOST`
11+
- `CADDY_WWW_HOST`
12+
- `CADDY_DASH_HOST`
13+
- `CADDY_API_HOST`
14+
- `LETSENCRYPT_EMAIL`
15+
3. Start Caddy:
16+
- `docker compose -f infra/caddy/docker-compose.yml up -d`
17+
18+
## Routing
19+
20+
- `https://<CADDY_PRIMARY_HOST>/` -> dashboard (`resolvekit_dashboard:3000`)
21+
- `https://<CADDY_PRIMARY_HOST>/v1/*` -> dashboard API (`resolvekit_api:3002`)
22+
- `https://<CADDY_PRIMARY_HOST>/agent/*` -> backend runtime (`resolvekit_backend:8000`)
23+
- `https://<CADDY_PRIMARY_HOST>/kb/*` -> KB service (`resolvekit_kb_service:8100`)
24+
- `https://<CADDY_API_HOST>/` -> backend runtime (`resolvekit_backend:8000`)
25+
26+
`/api/*` is also supported as an alias for `/v1/*`.
27+
28+
## Notes
29+
30+
- `CADDY_DOCKER_NETWORK` defaults to `resolvekit_default` (the network from `docker-compose.yml`).
31+
- If another edge proxy already owns public `:80/:443`, set:
32+
- `CADDY_HTTP_BIND=127.0.0.1:18080`
33+
- `CADDY_HTTPS_BIND=127.0.0.1:18443`
34+
- Keep `CADDY_LOCAL_BIND` on loopback for tailnet or local-only access (default `127.0.0.1:8080`).

0 commit comments

Comments
 (0)