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
# 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"]

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

View File

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