Consolidate Dockerfiles for singular deployment
This commit is contained in:
26
Dockerfile
26
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"]
|
||||
|
||||
69
README.md
69
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) │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ Orchestrator (Express, port 3001) │ │
|
||||
│ │ │ │
|
||||
│ │ 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│
|
||||
└──────────────────┘
|
||||
│ │ 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
|
||||
@@ -96,6 +98,8 @@ docker compose up -d
|
||||
# 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
|
||||
|
||||
@@ -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}
|
||||
|
||||
33
entrypoint.sh
Normal file
33
entrypoint.sh
Normal 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
|
||||
Reference in New Issue
Block a user