Consolidate Dockerfiles for singular deployment

This commit is contained in:
khalid@traclabs.com
2026-04-23 00:32:32 -05:00
parent 80459e8336
commit 5229ccdb0f
4 changed files with 97 additions and 66 deletions

View File

@@ -2,36 +2,48 @@ FROM node:22-alpine AS base
WORKDIR /app WORKDIR /app
# Install deps # Install deps (all workspaces)
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
COPY shared/package.json shared/ COPY shared/package.json shared/
COPY server/package.json server/ COPY server/package.json server/
RUN npm install --ignore-scripts RUN npm install --ignore-scripts
# Copy source # Rebuild native modules (better-sqlite3)
RUN npm rebuild better-sqlite3
# Copy all source
COPY . . COPY . .
# Build # Build Astro SSR site
RUN npm run build RUN npm run build
# Production # ── Production stage ──
FROM node:22-alpine AS runtime FROM node:22-alpine AS runtime
WORKDIR /app 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/node_modules ./node_modules
COPY --from=base /app/shared ./shared COPY --from=base /app/shared ./shared
COPY --from=base /app/server ./server
COPY --from=base /app/dist ./dist COPY --from=base /app/dist ./dist
COPY --from=base /app/package.json ./ COPY --from=base /app/package.json ./
COPY --from=base /app/site-context.json ./ COPY --from=base /app/site-context.json ./
COPY --from=base /app/content ./content COPY --from=base /app/content ./content
COPY --from=base /app/content ./content-seed COPY --from=base /app/content ./content-seed
COPY --from=base /app/config ./config 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 RUN chmod +x /app/entrypoint.sh
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV PORT=4321 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"] ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["node", "dist/server/entry.mjs"]

View File

@@ -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 ──► ┌────────────────────────────────────────────┐
│ Orchestrator (Express, port 3001)
│ ┌──────────────────────────────────────┐ │
│ │ In-Process FIFO Queue (concurrency 1) │
│ │ │ │ │ │ │ │
│ │ propose: route ► LLM ► proposal │ │ │ Webhook ──► Idempotency ──► Rate Limit │
│ │ apply: validate ► writeContentFile│ │ │
────────────────────────────────────── │ ┌──────────────────────────────────────
In-Process FIFO Queue (concurrency 1) │
SQLite: idempotency, proposals, rate limits, │ │ propose: route ► LLM ► proposal │
audit log apply: validate ► writeContentFile│
└───────────────────────┬─────────────────────────┘ │ │ └──────────────────────────────────────┘ │ │
writes canonical JSON │ │
SQLite: idempotency, proposals, rate
┌──────────────────┐ │ │ limits, audit log │ │
│ content/ (JSON) │ ◄── shared volume │ └──────────────────┬─────────────────────────┘ │
site-context.json writes canonical JSON
└────────┬─────────┘ │ ▼ │
│ reads with TTL cache ┌──────────────────┐ │
content/ (JSON) │ shared filesystem │
┌──────────────────┐ │ site-context.json│ │
Astro SSR └────────┬─────────┘
(port 4321) reads with TTL cache
Homepage + Editor
────────────────── │ ┌────────────────────────────────────────────┐ │
│ │ Astro SSR (port 4321) │ │
│ │ Homepage + Editor │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
``` ```
## Quick Start ## 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_KEY` — your Vonage API key (from Dashboard)
- `VONAGE_API_SECRET` — your Vonage API secret (from Dashboard) - `VONAGE_API_SECRET` — your Vonage API secret (from Dashboard)
- `VONAGE_APPLICATION_ID` — your Vonage application ID - `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) - `VONAGE_API_SIGNATURE_SECRET` — webhook signature secret (from Dashboard → API Settings)
See `.env.example` for all options. See `.env.example` for all options.
@@ -81,10 +83,10 @@ See `.env.example` for all options.
### Vonage Setup ### Vonage Setup
1. Create a Vonage application in the Dashboard with Messages capability enabled. 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). 2. Set the inbound message webhook URL to `https://dynamicsites.kadil.dev/webhooks/inbound` (POST).
3. Set the status webhook URL to `https://smsapi.kadil.dev/webhooks/status` (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. 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. 6. Note your signature secret from Dashboard → API Settings for webhook verification.
### Docker ### Docker
@@ -96,6 +98,8 @@ docker compose up -d
# Orchestrator: http://localhost:3001/health # 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 ## Project Structure
``` ```
@@ -135,9 +139,9 @@ docker compose up -d
│ ├── sections/ # Astro section components │ ├── sections/ # Astro section components
│ └── editor/ # React editor island │ └── editor/ # React editor island
├── scripts/ # CLI tools ├── scripts/ # CLI tools
├── docker-compose.yml # Full stack (web + orchestrator) ├── Dockerfile # Combined image (Astro SSR + orchestrator)
├── Dockerfile # SSR site image ├── docker-compose.yml # Full stack (single service)
└── server/Dockerfile # Orchestrator image └── entrypoint.sh # Starts both processes + seeds content
``` ```
## Edit Flow ## Edit Flow
@@ -151,6 +155,7 @@ docker compose up -d
## Key Design Decisions ## 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 Redis, no BullMQ**: Simple in-process FIFO queue with concurrency 1
- **No git**: Content persistence is filesystem-only - **No git**: Content persistence is filesystem-only
- **SQLite for everything**: Idempotency, proposals, rate limits, audit log - **SQLite for everything**: Idempotency, proposals, rate limits, audit log

View File

@@ -1,52 +1,33 @@
services: services:
web: app:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
ports: ports:
- "4321:4321" - "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" - "3001:3001"
volumes: volumes:
- content-data:/app/content - content-data:/app/content
- ./site-context.json:/app/site-context.json
- sqlite-data:/app/data - sqlite-data:/app/data
environment: environment:
- HOST=0.0.0.0
- PORT=4321
- ORCHESTRATOR_PORT=3001 - ORCHESTRATOR_PORT=3001
- REPO_ROOT=/app - REPO_ROOT=/app
- IDEMPOTENCY_DB_PATH=/app/data/dynamic-sites.db - IDEMPOTENCY_DB_PATH=/app/data/dynamic-sites.db
- SITE_DATA_TTL_MS=${SITE_DATA_TTL_MS:-500} - 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} - API_EDIT_SECRET=${API_EDIT_SECRET:-change-me}
- PUBLIC_ORCHESTRATOR_URL=${PUBLIC_ORCHESTRATOR_URL:-http://localhost:3001}
- OLLAMA_API_KEY=${OLLAMA_API_KEY:-} - OLLAMA_API_KEY=${OLLAMA_API_KEY:-}
- OLLAMA_HOST=${OLLAMA_HOST:-https://ollama.com} - OLLAMA_HOST=${OLLAMA_HOST:-https://ollama.com}
- VONAGE_API_KEY=${VONAGE_API_KEY:-} - VONAGE_API_KEY=${VONAGE_API_KEY:-}
- VONAGE_API_SECRET=${VONAGE_API_SECRET:-} - VONAGE_API_SECRET=${VONAGE_API_SECRET:-}
- VONAGE_APPLICATION_ID=${VONAGE_APPLICATION_ID:-} - 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=${VONAGE_PRIVATE_KEY:-}
- VONAGE_PRIVATE_KEY_PATH=${VONAGE_PRIVATE_KEY_PATH:-} - VONAGE_PRIVATE_KEY_PATH=${VONAGE_PRIVATE_KEY_PATH:-}
- VONAGE_API_SIGNATURE_SECRET=${VONAGE_API_SIGNATURE_SECRET:-} - 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} - LOG_LEVEL=${LOG_LEVEL:-info}
- PROPOSAL_TTL_MS=${PROPOSAL_TTL_MS:-900000} - PROPOSAL_TTL_MS=${PROPOSAL_TTL_MS:-900000}
- SMS_RATE_LIMIT_PER_HOUR=${SMS_RATE_LIMIT_PER_HOUR:-10} - SMS_RATE_LIMIT_PER_HOUR=${SMS_RATE_LIMIT_PER_HOUR:-10}

33
entrypoint.sh Normal file
View File

@@ -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