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
|
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"]
|
|
||||||
|
|||||||
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 ──► │
|
│ ┌────────────────────────────────────────────┐ │
|
||||||
│ │
|
│ │ 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
|
||||||
|
|||||||
@@ -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
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