Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ cd "$REPO_ROOT"
if [ ! -f backend/.env ]; then
if [ -f backend/.env.example ]; then
cp backend/.env.example backend/.env
echo "Created backend/.env from example. Remember to set OPENAI_API_KEY."
echo "Created backend/.env from example. Remember to set JWT_SECRET."
echo "Note: ALLOWED_ORIGIN is set for devcontainer (port 3003)."
else
echo "Warning: backend/.env.example not found; create backend/.env manually." >&2
Expand Down
51 changes: 51 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Ignore node_modules to prevent host-compiled binaries from contaminating the build
node_modules
*/node_modules

# Development files
.git
.gitignore
*.md
!AGENTS.md
!CLAUDE.md

# Environment files
.env
.env.local
.env.*.local

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# IDE
.vscode
.idea
*.swp
*.swo
*~

# OS files
.DS_Store
Thumbs.db

# Test coverage
coverage
.nyc_output

# Build artifacts
dist
build
out
.next

# Docker
docker-compose.override.yml

# Data
data
*.db
*.db-*
26 changes: 8 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,10 @@ jobs:
cache: 'npm'
cache-dependency-path: backend/package-lock.json

- name: Install Backend Dependencies
run: npm --prefix backend install

- name: Lint Backend
run: npm --prefix backend run lint

- name: Test Backend
run: npm --prefix backend test
timeout-minutes: 1
- name: Run Backend CI
run: |
chmod +x ci.sh
./ci.sh backend

frontend:
runs-on: ubuntu-latest
Expand All @@ -43,15 +38,10 @@ jobs:
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install Frontend Dependencies
run: npm --prefix frontend install

- name: Lint Frontend
run: npm --prefix frontend run lint

- name: Test Frontend
run: npm --prefix frontend test
timeout-minutes: 1
- name: Run Frontend CI
run: |
chmod +x ci.sh
./ci.sh frontend

# This job will only run if both backend and frontend jobs succeed
# The workflow will fail if either backend or frontend fails
Expand Down
87 changes: 87 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Build and Publish Docker Image

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to build and publish (e.g., v1.0.0)'
required: true
type: string

env:
REGISTRY: docker.io
IMAGE_NAME: qduc/chat

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ variables.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract version tag
id: version
run: |
if [ -n "${{ github.event.inputs.tag }}" ]; then
VERSION="${{ github.event.inputs.tag }}"
else
VERSION="${GITHUB_REF#refs/tags/}"
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_short=${VERSION#v}" >> $GITHUB_OUTPUT

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
target: runner
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version_short }}
${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Create release summary
run: |
echo "## Docker Image Published 🚀" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Version: \`${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Image" >> $GITHUB_STEP_SUMMARY
echo "- \`${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- \`${{ env.IMAGE_NAME }}:latest\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Quick Start" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "docker run -d -p 3000:3000 -v chatforge_data:/data ${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Or with docker-compose:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "curl -O https://raw.githubusercontent.com/qduc/chat/main/docker-compose.yml" >> $GITHUB_STEP_SUMMARY
echo "docker compose up -d" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Then visit http://localhost:3000 and add your provider API key via Settings → Providers & Tools." >> $GITHUB_STEP_SUMMARY
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ chat/
frontend/ # Next.js 15 + React 19 + TypeScript
backend/ # Node.js + Express + SQLite
docs/ # Architecture docs and ADRs
proxy/ # Nginx reverse proxy config
proxy/ # Dev-only Nginx reverse proxy config (compose.dev)
integration/ # Integration tests
requests/ # HTTP request examples
dev.sh # Development orchestration script
Expand All @@ -36,6 +36,8 @@ chat/
- Server-side tool orchestration
- User-based authentication and authorization

> **Production bundling:** The root multi-stage `Dockerfile` exports the Next.js app and copies it into the Express backend, so the `app` container (managed by `prod.sh` / `docker-compose.yml`) serves both `/api` and static assets. The standalone `frontend`, `backend`, and `proxy` containers only exist in the development compose stack for hot reload.

### Core Design Principles

1. **User-Based Data Isolation**: All data operations are scoped to authenticated users (enforced at database level with NOT NULL user_id constraints)
Expand Down Expand Up @@ -116,6 +118,7 @@ chat/
./prod.sh backup # Create database backup
./prod.sh exec <service> <command> # Execute command in container
```
> In production there is a single `app` service. Most `prod.sh exec` commands therefore look like `./prod.sh exec app <command>`.

### Release Management
```bash
Expand Down Expand Up @@ -206,7 +209,7 @@ chat/
- **JWT Authentication**: Token-based authentication with bcrypt password hashing and refresh token support
- **Streaming Protocol**: SSE for real-time updates with usage metadata tracking
- **API Compatibility**: Maintains OpenAI API contract while extending functionality
- **Reverse Proxy**: Nginx proxy routes /api requests to backend in Docker deployments
- **Dev Reverse Proxy**: Nginx proxy routes /api requests to backend in the Docker *development* stack (production bundles everything into one container)
- **Image Storage**: Secure image metadata storage with path-based access control and validation (max 10MB, 5 images/message)
- **File Storage**: Text file uploads with content extraction (max 5MB, 3 files/message, 30+ file types supported)
- **Conversation Snapshots**: Each conversation maintains complete settings snapshot for reproducibility
Expand Down
59 changes: 59 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
ARG NODE_IMAGE=node:22.18.0-bookworm-slim

# --- Frontend Build Stage ---
FROM ${NODE_IMAGE} AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
ENV NEXT_PUBLIC_API_BASE=/api
RUN npm run build

# --- Backend Build Stage ---
FROM ${NODE_IMAGE} AS backend-builder
WORKDIR /app/backend

# Install build dependencies for native modules
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*

COPY backend/package*.json ./

# Clean install to ensure proper compilation in container
RUN npm ci --omit=dev --build-from-source

# Verify better-sqlite3 was built correctly
RUN node -e "require('better-sqlite3'); console.log('better-sqlite3 loaded successfully')"

# --- Final Stage ---
FROM ${NODE_IMAGE} AS runner
WORKDIR /app

# Install runtime dependencies (gosu is Debian equivalent of su-exec)
RUN apt-get update && apt-get install -y gosu libsqlite3-0 && rm -rf /var/lib/apt/lists/*

# Copy backend dependencies
COPY --from=backend-builder --chown=node:node /app/backend/node_modules ./node_modules

# Copy backend source
COPY --chown=node:node backend/ .

# Copy frontend build to backend/public
COPY --from=frontend-builder --chown=node:node /app/frontend/out ./public

# Setup permissions and directories
RUN mkdir -p logs && chown node:node logs
RUN mkdir -p /data && chown node:node /data
RUN chmod +x entrypoint.sh && chown node:node entrypoint.sh

ENV NODE_ENV=production
ENV PORT=3000
ENV INSTALL_ON_START=0

USER node
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD ["node", "-e", "fetch(`http://127.0.0.1:${process.env.PORT || 3000}/health`).then(res => { if (res.ok) process.exit(0); process.exit(1); }).catch(() => process.exit(1));"]

ENTRYPOINT ["./entrypoint.sh"]
CMD ["node","src/index.js"]
43 changes: 38 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,44 @@ ChatForge is a full-stack AI chat application featuring a Next.js 15 frontend an

- Node.js 18 or higher
- Docker and Docker Compose (for containerized deployment)
- OpenAI API key or compatible provider API key
- An OpenAI (or compatible) API key that you'll enter through Settings → Providers & Tools

## Quick Start

**Recommended: Docker Development**
### Option 1: One-Click Docker Hub Deployment (Recommended)

Pull pre-built images from Docker Hub - no cloning required:

```bash
# Download the compose file
curl -O https://raw.githubusercontent.com/qduc/chat/main/docker-compose.hub.yml

# (Optional) Provide infrastructure secrets
echo "JWT_SECRET=change-me" > .env

# Start the stack
docker compose -f docker-compose.hub.yml up -d
```

Visit http://localhost:3000, register your first user, then open **Settings → Providers & Tools** to enter your API key and base URL.

The production compose file now runs a single `app` service built from the root multi-stage `Dockerfile`. That container bundles the Express API, the exported Next.js UI, and the static asset server, so there is no longer a separate frontend or nginx proxy to operate in production.

**Optional infrastructure config** (add to `.env` file):
```bash
JWT_SECRET=your-secret-here # Overrides auto-generated secret
PORT=3000 # External port (default: 3000)
```

### Option 2: Docker Development (with hot reload)

```bash
# Clone the repository
git clone https://github.com/qduc/chat.git && cd chat

# Copy environment files
cp backend/.env.example backend/.env
# Edit backend/.env and set OPENAI_API_KEY and JWT_SECRET
# Edit backend/.env and set JWT_SECRET

# Start with hot reload
./dev.sh up --build
Expand All @@ -47,7 +75,7 @@ cp backend/.env.example backend/.env
./dev.sh logs -f
```

Visit http://localhost:3003
Visit http://localhost:3003. The development compose file still runs dedicated `frontend`, `backend`, and `proxy` containers to keep hot reload fast, but production images collapse into a single runtime service.

For alternative setup options, see [docs/INSTALLATION.md](docs/INSTALLATION.md).

Expand All @@ -73,7 +101,7 @@ chat/
├── frontend/ # Next.js 15 + React 19 + TypeScript
├── backend/ # Node.js + Express + SQLite
├── docs/ # Technical documentation
├── proxy/ # Nginx reverse proxy config
├── proxy/ # Dev-only Nginx reverse proxy config
├── integration/ # Integration tests
├── requests/ # HTTP request examples
├── dev.sh # Development orchestration
Expand All @@ -89,6 +117,11 @@ chat/
./dev.sh test:frontend # Frontend tests only
```

## Deployment Architecture

- **Production (`docker-compose.yml`, `prod.sh`)** – Single `app` container generated by the top-level `Dockerfile`. The multi-stage build compiles the Next.js frontend to a static export and copies it into the Express backend, which serves both `/api` and the UI while persisting data/logs under `/data`.
- **Development (`docker-compose.dev.yml`, `dev.sh`)** – Dedicated `frontend`, `backend`, `proxy`, and `adminer` services for fast iteration with hot reload. The nginx proxy that provides the http://localhost:3003 origin only exists in this dev stack.

## Contributing

Contributions are welcome! Please follow these guidelines:
Expand Down
20 changes: 4 additions & 16 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
# shellcheck disable=SC2034

## Provider selection (default: openai)
PROVIDER=openai
## Authentication
JWT_SECRET=change-me

## Generic provider config (falls back to OpenAI values)
# PROVIDER_BASE_URL=
# PROVIDER_API_KEY=
# PROVIDER_HEADERS_JSON={"X-Custom":"Value"}

## OpenAI-compatible defaults (kept for backward-compat)
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_API_KEY=sk-xxxxx
TAVILY_API_KEY=tvly-xxxxx # Optional: Tavily web search tool
EXA_API_KEY=exa-xxxxx # Optional: Exa web search tool
# SEARXNG_BASE_URL=http://localhost:8080 # Legacy fallback; configure "SearXNG Base URL" in user settings instead

DEFAULT_MODEL=gpt-4.1-mini
TITLE_MODEL=gpt-4.1-mini
## Provider & tool configuration now lives in Settings → Providers & Tools
# Add API keys, base URLs, and default models in-app; they are no longer read from .env files.
PORT=3001
RATE_LIMIT_WINDOW_SEC=60
RATE_LIMIT_MAX=50
Expand Down
Loading
Loading