diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..43f893aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Development +.git/ +.github/ +.vscode/ +.idea/ + +# Build outputs +dist/ +build/ + +# Environment files +.env.local +.env.development +.env.test + +# Testing +coverage/ +.nyc_output/ + +# Misc +*.log +*.swp +*.swo +*~ +.DS_Store +Thumbs.db + +# Documentation +*.md +!README.md + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml + +# Docker +#Dockerfile* +#docker-compose* +#.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..9509f83c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +FROM node:20-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache \ + python3 \ + make \ + g++ \ + git + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install all dependencies (including dev dependencies for building) +RUN npm ci && \ + npm cache clean --force + +# Copy source code +COPY . . + +# Build frontend +RUN npm run build + +# ============================================================================= +# Production stage +# ============================================================================= +FROM node:20-alpine + +# Install runtime dependencies +RUN apk add --no-cache \ + python3 \ + make \ + g++ \ + git \ + ca-certificates \ + tzdata + +WORKDIR /app + +# Copy package files and install production dependencies +COPY package*.json ./ +RUN npm ci --only=production && \ + npm cache clean --force + +# Copy built files from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/server ./server +COPY --from=builder /app/public ./public +COPY --from=builder /app/index.html ./index.html + +# Create necessary directories with proper permissions +RUN mkdir -p /data /config && \ + chown -R node:node /app /data /config + +# Switch to node user (uid/gid 1000) +USER node + +# Environment variables +ENV NODE_ENV=production \ + PORT=3001 \ + DATABASE_PATH=/data/auth.db + +# Expose port +EXPOSE 3001 + +# Start server +CMD ["node", "server/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..5e7d7597 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +networks: + claudecodeui: + name: claudecodeui + public: + external: true + +services: + claudecodeui: + build: + context: . + dockerfile: Dockerfile + # image: claudecodeui:latest + restart: unless-stopped + ports: + - "${EXTERNAL_PORT:-3002}:3001" + env_file: + - path: ./.env + required: true + environment: + - PORT=${PORT:-3001} + - DATABASE_PATH=${DATABASE_PATH:-/data/auth.db} + - CONTEXT_WINDOW=${CONTEXT_WINDOW:-160000} + - CLAUDE_CLI_PATH=${CLAUDE_CLI_PATH:-claude} + - NODE_ENV=production + volumes: + # sadly these paths must be hardcoded (no we cannot use $HOME) because claude code uses + # hardcoded absolute paths in the ../node/.claude file and Docker must have identical full ones + #- /home/faris/Projects/:/home/faris/Projects/ + - ${HOME}/Projects/:${HOME}/Projects/ +# - /mnt/d2/Projects/:/mnt/d2/Projects/ + - ${HOME}/.claude:/home/node/.claude:rw + - claudecodeui-data:/data + - claudecodeui-config:/config + networks: + - claudecodeui + - public + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + claudecodeui-data: + name: claudecodeui_data + claudecodeui-config: + name: claudecodeui_config +