From 5229ccdb0fd366f8cf93c919071ae2af6f39e362 Mon Sep 17 00:00:00 2001 From: "khalid@traclabs.com" Date: Thu, 23 Apr 2026 00:32:32 -0500 Subject: [PATCH] Consolidate Dockerfiles for singular deployment --- Dockerfile | 26 ++++++++++++----- README.md | 73 +++++++++++++++++++++++++--------------------- docker-compose.yml | 31 ++++---------------- entrypoint.sh | 33 +++++++++++++++++++++ 4 files changed, 97 insertions(+), 66 deletions(-) create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 1ed4ef0..77a2738 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,36 +2,48 @@ FROM node:22-alpine AS base WORKDIR /app -# Install deps +# Install deps (all workspaces) COPY package.json package-lock.json* ./ COPY shared/package.json shared/ COPY server/package.json server/ RUN npm install --ignore-scripts -# Copy source +# Rebuild native modules (better-sqlite3) +RUN npm rebuild better-sqlite3 + +# Copy all source COPY . . -# Build +# Build Astro SSR site RUN npm run build -# Production +# ── Production stage ── FROM node:22-alpine AS runtime WORKDIR /app +# Install tsx globally for running the orchestrator +RUN npm install -g tsx + COPY --from=base /app/node_modules ./node_modules COPY --from=base /app/shared ./shared +COPY --from=base /app/server ./server COPY --from=base /app/dist ./dist COPY --from=base /app/package.json ./ COPY --from=base /app/site-context.json ./ COPY --from=base /app/content ./content COPY --from=base /app/content ./content-seed COPY --from=base /app/config ./config -COPY --from=base /app/server/entrypoint.sh /app/entrypoint.sh + +COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh ENV HOST=0.0.0.0 ENV PORT=4321 -EXPOSE 4321 +ENV ORCHESTRATOR_PORT=3001 +ENV REPO_ROOT=/app +ENV NODE_ENV=production + +# Expose both ports +EXPOSE 4321 3001 ENTRYPOINT ["/app/entrypoint.sh"] -CMD ["node", "dist/server/entry.mjs"] diff --git a/README.md b/README.md index 63fa23d..e067e10 100644 --- a/README.md +++ b/README.md @@ -12,33 +12,35 @@ An LLM-powered website editing framework. Edit your site via SMS, a web API, or │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────┐ -│ Orchestrator (Express, port 3001) │ +│ Single Container (entrypoint.sh) │ │ │ -│ Webhook ──► Idempotency ──► Rate Limit ──► │ -│ │ -│ ┌──────────────────────────────────────┐ │ -│ │ In-Process FIFO Queue (concurrency 1) │ -│ │ │ │ -│ │ propose: route ► LLM ► proposal │ │ -│ │ apply: validate ► writeContentFile│ │ -│ └──────────────────────────────────────┘ │ -│ │ -│ SQLite: idempotency, proposals, rate limits, │ -│ audit log │ -└───────────────────────┬─────────────────────────┘ - │ writes canonical JSON - ▼ - ┌──────────────────┐ - │ content/ (JSON) │ ◄── shared volume - │ site-context.json│ - └────────┬─────────┘ - │ reads with TTL cache - ▼ - ┌──────────────────┐ - │ Astro SSR │ - │ (port 4321) │ - │ Homepage + Editor│ - └──────────────────┘ +│ ┌────────────────────────────────────────────┐ │ +│ │ Orchestrator (Express, port 3001) │ │ +│ │ │ │ +│ │ Webhook ──► Idempotency ──► Rate Limit │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────┐ │ │ +│ │ │ In-Process FIFO Queue (concurrency 1) │ │ +│ │ │ propose: route ► LLM ► proposal │ │ │ +│ │ │ apply: validate ► writeContentFile│ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ SQLite: idempotency, proposals, rate │ │ +│ │ limits, audit log │ │ +│ └──────────────────┬─────────────────────────┘ │ +│ │ writes canonical JSON │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ content/ (JSON) │ shared filesystem │ +│ │ site-context.json│ │ +│ └────────┬─────────┘ │ +│ │ reads with TTL cache │ +│ ▼ │ +│ ┌────────────────────────────────────────────┐ │ +│ │ Astro SSR (port 4321) │ │ +│ │ Homepage + Editor │ │ +│ └────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────┘ ``` ## Quick Start @@ -73,7 +75,7 @@ Copy `.env.example` to `.env` and set at minimum: - `VONAGE_API_KEY` — your Vonage API key (from Dashboard) - `VONAGE_API_SECRET` — your Vonage API secret (from Dashboard) - `VONAGE_APPLICATION_ID` — your Vonage application ID -- `VONAGE_PRIVATE_KEY_PATH` — path to the `private.key` file generated when creating the Vonage application +- `VONAGE_PRIVATE_KEY_PATH` — path to the `private.key` file on disk, or set `VONAGE_PRIVATE_KEY` to the base64-encoded PEM content - `VONAGE_API_SIGNATURE_SECRET` — webhook signature secret (from Dashboard → API Settings) See `.env.example` for all options. @@ -81,10 +83,10 @@ See `.env.example` for all options. ### Vonage Setup 1. Create a Vonage application in the Dashboard with Messages capability enabled. -2. Set the inbound message webhook URL to `https://smsapi.kadil.dev/webhooks/inbound` (POST). -3. Set the status webhook URL to `https://smsapi.kadil.dev/webhooks/status` (POST). +2. Set the inbound message webhook URL to `https://dynamicsites.kadil.dev/webhooks/inbound` (POST). +3. Set the status webhook URL to `https://dynamicsites.kadil.dev/webhooks/status` (POST). 4. Under API Settings, ensure Messages API is set as the default for SMS. -5. Copy the generated `private.key` to the project root. +5. Copy the generated `private.key` to the project root (or base64-encode it for `VONAGE_PRIVATE_KEY`). 6. Note your signature secret from Dashboard → API Settings for webhook verification. ### Docker @@ -92,10 +94,12 @@ See `.env.example` for all options. ```bash docker compose build docker compose up -d -# Site: http://localhost:4321 +# Site: http://localhost:4321 # Orchestrator: http://localhost:3001/health ``` +Both the Astro SSR site and the orchestrator run in a single container, sharing the same `content/` directory. The entrypoint script starts both processes and seeds content from the image into the volume on first deploy. + ## Project Structure ``` @@ -135,9 +139,9 @@ docker compose up -d │ ├── sections/ # Astro section components │ └── editor/ # React editor island ├── scripts/ # CLI tools -├── docker-compose.yml # Full stack (web + orchestrator) -├── Dockerfile # SSR site image -└── server/Dockerfile # Orchestrator image +├── Dockerfile # Combined image (Astro SSR + orchestrator) +├── docker-compose.yml # Full stack (single service) +└── entrypoint.sh # Starts both processes + seeds content ``` ## Edit Flow @@ -151,6 +155,7 @@ docker compose up -d ## Key Design Decisions +- **Single container**: Both Astro SSR and the orchestrator run in one container, sharing the filesystem for content reads and writes - **No Redis, no BullMQ**: Simple in-process FIFO queue with concurrency 1 - **No git**: Content persistence is filesystem-only - **SQLite for everything**: Idempotency, proposals, rate limits, audit log diff --git a/docker-compose.yml b/docker-compose.yml index a3ae4c2..0b6cd8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,52 +1,33 @@ services: - web: + app: build: context: . dockerfile: Dockerfile ports: - "4321:4321" - volumes: - - content-data:/app/content - - ./site-context.json:/app/site-context.json - environment: - - HOST=0.0.0.0 - - PORT=4321 - - REPO_ROOT=/app - - SITE_DATA_TTL_MS=${SITE_DATA_TTL_MS:-500} - - EDITOR_SESSION_SECRET=${EDITOR_SESSION_SECRET:-change-me} - - API_EDIT_SECRET=${API_EDIT_SECRET:-change-me} - - PUBLIC_ORCHESTRATOR_URL=http://localhost:3001 - depends_on: - - orchestrator - restart: unless-stopped - - orchestrator: - build: - context: . - dockerfile: server/Dockerfile - ports: - "3001:3001" volumes: - content-data:/app/content - - ./site-context.json:/app/site-context.json - sqlite-data:/app/data environment: + - HOST=0.0.0.0 + - PORT=4321 - ORCHESTRATOR_PORT=3001 - REPO_ROOT=/app - IDEMPOTENCY_DB_PATH=/app/data/dynamic-sites.db - SITE_DATA_TTL_MS=${SITE_DATA_TTL_MS:-500} + - EDITOR_SESSION_SECRET=${EDITOR_SESSION_SECRET:-change-me} - API_EDIT_SECRET=${API_EDIT_SECRET:-change-me} + - PUBLIC_ORCHESTRATOR_URL=${PUBLIC_ORCHESTRATOR_URL:-http://localhost:3001} - OLLAMA_API_KEY=${OLLAMA_API_KEY:-} - OLLAMA_HOST=${OLLAMA_HOST:-https://ollama.com} - VONAGE_API_KEY=${VONAGE_API_KEY:-} - VONAGE_API_SECRET=${VONAGE_API_SECRET:-} - VONAGE_APPLICATION_ID=${VONAGE_APPLICATION_ID:-} - # Use VONAGE_PRIVATE_KEY (base64 or raw PEM) for PaaS/Dokploy deployments, - # or VONAGE_PRIVATE_KEY_PATH with a file mount for local/Docker deployments. - VONAGE_PRIVATE_KEY=${VONAGE_PRIVATE_KEY:-} - VONAGE_PRIVATE_KEY_PATH=${VONAGE_PRIVATE_KEY_PATH:-} - VONAGE_API_SIGNATURE_SECRET=${VONAGE_API_SIGNATURE_SECRET:-} - - CORS_ALLOWED_ORIGIN=http://localhost:4321 + - CORS_ALLOWED_ORIGIN=${CORS_ALLOWED_ORIGIN:-http://localhost:4321} - LOG_LEVEL=${LOG_LEVEL:-info} - PROPOSAL_TTL_MS=${PROPOSAL_TTL_MS:-900000} - SMS_RATE_LIMIT_PER_HOUR=${SMS_RATE_LIMIT_PER_HOUR:-10} diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..dd2fa6d --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/sh +set -e + +# Seed content into the volume if it's empty (first deploy). +SEED_DIR="/app/content-seed" +CONTENT_DIR="/app/content" + +if [ -d "$SEED_DIR" ] && [ -d "$CONTENT_DIR" ]; then + if [ ! -d "$CONTENT_DIR/sections" ] || [ -z "$(ls -A "$CONTENT_DIR/sections" 2>/dev/null)" ]; then + echo "Seeding content from $SEED_DIR into $CONTENT_DIR..." + cp -rn "$SEED_DIR/"* "$CONTENT_DIR/" 2>/dev/null || true + echo "Content seeded." + fi +fi + +echo "Starting orchestrator on port ${ORCHESTRATOR_PORT:-3001}..." +node --import tsx/esm /app/server/src/index.ts & +ORCH_PID=$! + +echo "Starting Astro SSR on port ${PORT:-4321}..." +node /app/dist/server/entry.mjs & +ASTRO_PID=$! + +# Shut down both on any signal +trap "kill $ORCH_PID $ASTRO_PID 2>/dev/null; wait; exit 0" SIGTERM SIGINT + +# Wait for either to exit — if one dies, stop both +wait -n $ORCH_PID $ASTRO_PID 2>/dev/null +EXIT_CODE=$? +echo "A process exited with code $EXIT_CODE, shutting down..." +kill $ORCH_PID $ASTRO_PID 2>/dev/null +wait +exit $EXIT_CODE