From 2ab1133e77e179ad226127e399530e7146716fac Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Fri, 22 Aug 2025 16:00:55 -0700 Subject: [PATCH 1/2] feat: add docker support for one-click deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add multi-stage dockerfile for optimized builds - create docker-compose.yml for easy local development - add github actions workflow for automated docker image publishing - create comprehensive deployment documentation - support for anthropic api key via environment variable 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/docker-publish.yml | 61 ++++++++++++ DOCKER_DEPLOYMENT.md | 142 +++++++++++++++++++++++++++ Dockerfile | 77 +++++++++++++++ docker-compose.yml | 26 +++++ 4 files changed, 306 insertions(+) create mode 100644 .github/workflows/docker-publish.yml create mode 100644 DOCKER_DEPLOYMENT.md create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..2162772a --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,61 @@ +name: docker build and publish + +on: + push: + branches: + - main + - feat/dockerize-app + tags: + - 'v*' + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: checkout repository + uses: actions/checkout@v4 + + - name: set up docker buildx + uses: docker/setup-buildx-action@v3 + + - name: log in to github container registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: build and push docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/DOCKER_DEPLOYMENT.md b/DOCKER_DEPLOYMENT.md new file mode 100644 index 00000000..d15808c1 --- /dev/null +++ b/DOCKER_DEPLOYMENT.md @@ -0,0 +1,142 @@ +# claudable docker deployment guide + +## quick start - one click run + +### prerequisites +- docker and docker compose installed +- claude api key from anthropic + +### 1. pull and run (easiest method) + +```bash +# pull the latest image +docker pull ghcr.io/opactorai/claudable:latest + +# run with your api key +docker run -d \ + -p 3000:3000 \ + -p 8080:8080 \ + -e ANTHROPIC_API_KEY="your-claude-api-key-here" \ + -v claudable-data:/app/data \ + --name claudable \ + ghcr.io/opactorai/claudable:latest +``` + +### 2. using docker compose + +create a `.env` file: +```bash +ANTHROPIC_API_KEY=your-claude-api-key-here +``` + +create `docker-compose.yml`: +```yaml +version: '3.8' + +services: + claudable: + image: ghcr.io/opactorai/claudable:latest + ports: + - "3000:3000" + - "8080:8080" + environment: + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + volumes: + - claudable-data:/app/data + restart: unless-stopped + +volumes: + claudable-data: +``` + +run: +```bash +docker compose up -d +``` + +## access the application + +- **frontend**: http://localhost:3000 +- **api**: http://localhost:8080 +- **api docs**: http://localhost:8080/docs + +## build from source + +```bash +# clone repository +git clone https://github.com/opactorai/claudable.git +cd claudable + +# build image +docker build -t claudable:local . + +# run with docker compose +docker compose up -d +``` + +## environment variables + +| variable | description | required | default | +|----------|-------------|----------|---------| +| `ANTHROPIC_API_KEY` | claude api key for ai functionality | yes | - | +| `NEXT_PUBLIC_API_URL` | backend api url | no | http://localhost:8080 | +| `API_PORT` | api server port | no | 8080 | +| `WEB_PORT` | web server port | no | 3000 | + +## data persistence + +the sqlite database is stored in `/app/data` inside the container. to persist data: + +```bash +# named volume (recommended) +docker run -v claudable-data:/app/data ... + +# or bind mount +docker run -v $(pwd)/data:/app/data ... +``` + +## stopping and removing + +```bash +# stop container +docker stop claudable + +# remove container +docker rm claudable + +# remove image +docker rmi ghcr.io/opactorai/claudable:latest +``` + +## troubleshooting + +### ports already in use +change the port mapping: +```bash +docker run -p 3001:3000 -p 8081:8080 ... +``` + +### permission issues +ensure the data directory has correct permissions: +```bash +docker exec claudable chown -R nextjs:nodejs /app/data +``` + +### api connection issues +verify the api is accessible: +```bash +docker logs claudable +curl http://localhost:8080/docs +``` + +## security notes + +- **never commit** your `.env` file with api keys +- use docker secrets for production deployments +- regularly update the base image for security patches +- consider using a reverse proxy for production + +## support + +- github issues: https://github.com/opactorai/claudable/issues +- discord: https://discord.gg/njnbafhnqc \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..16fea078 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +# multi-stage build for claudable app +FROM node:20-alpine AS base + +# install python and dependencies +RUN apk add --no-cache python3 py3-pip python3-dev build-base git + +# set working directory +WORKDIR /app + +# copy package files +COPY package*.json ./ +COPY apps/web/package*.json ./apps/web/ +COPY apps/api/requirements.txt ./apps/api/ + +# install node dependencies +RUN npm ci --only=production + +# install web dependencies +WORKDIR /app/apps/web +RUN npm ci --only=production + +# build stage for next.js +FROM base AS builder +WORKDIR /app +COPY . . + +# build next.js app +WORKDIR /app/apps/web +RUN npm run build + +# production stage +FROM node:20-alpine AS runner +RUN apk add --no-cache python3 py3-pip + +WORKDIR /app + +# create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nextjs -u 1001 + +# copy built application +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./apps/web/.next +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/package*.json ./apps/web/ +COPY --from=builder --chown=nextjs:nodejs /app/apps/api ./apps/api +COPY --from=builder --chown=nextjs:nodejs /app/package*.json ./ +COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts + +# install production dependencies +RUN npm ci --only=production +WORKDIR /app/apps/web +RUN npm ci --only=production + +# setup python environment +WORKDIR /app/apps/api +RUN python3 -m venv .venv && \ + .venv/bin/pip install --no-cache-dir -r requirements.txt + +# create data directory for sqlite +RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data + +USER nextjs + +# expose ports +EXPOSE 3000 8080 + +# environment variables +ENV NODE_ENV=production \ + ANTHROPIC_API_KEY="" \ + NEXT_PUBLIC_API_URL=http://localhost:8080 \ + API_HOST=0.0.0.0 \ + API_PORT=8080 \ + WEB_PORT=3000 + +# start both services +WORKDIR /app +CMD ["sh", "-c", "cd /app/apps/api && .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8080 & cd /app/apps/web && npm start"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..051d8347 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + claudable: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" # next.js frontend + - "8080:8080" # fastapi backend + environment: + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - NODE_ENV=production + - NEXT_PUBLIC_API_URL=http://localhost:8080 + - API_HOST=0.0.0.0 + - API_PORT=8080 + - WEB_PORT=3000 + volumes: + - ./data:/app/data # persist sqlite database + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s \ No newline at end of file From 9956ebde1c8bc35dce9fd1632e57df1112d9825c Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Fri, 22 Aug 2025 19:02:45 -0700 Subject: [PATCH 2/2] fix: resolve typescript build errors for docker deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix motion.div className prop issues by using style attribute - add react-icons to web dependencies - fix useRef type annotations for NodeJS.Timeout - update processToolContent return type signature all typescript errors resolved, docker build now succeeds 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Dockerfile | 16 +++++++++------- apps/web/app/page.tsx | 3 ++- apps/web/components/ChatLog.tsx | 7 ++++++- apps/web/components/ToolResultItem.tsx | 2 +- apps/web/hooks/useUserRequests.ts | 4 ++-- apps/web/package.json | 1 + 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 16fea078..10d6df2c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,23 +7,25 @@ RUN apk add --no-cache python3 py3-pip python3-dev build-base git # set working directory WORKDIR /app -# copy package files +# copy package files and scripts COPY package*.json ./ +COPY scripts ./scripts COPY apps/web/package*.json ./apps/web/ COPY apps/api/requirements.txt ./apps/api/ -# install node dependencies -RUN npm ci --only=production - -# install web dependencies -WORKDIR /app/apps/web -RUN npm ci --only=production +# install node dependencies without running postinstall +RUN npm ci --only=production --ignore-scripts # build stage for next.js FROM base AS builder WORKDIR /app COPY . . +# install all dependencies for build +RUN npm ci --ignore-scripts +WORKDIR /app/apps/web +RUN npm ci + # build next.js app WORKDIR /app/apps/web RUN npm run build diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 7374b497..51a7ff77 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -984,8 +984,8 @@ export default function HomePage() { initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} - className="bg-white dark:bg-gray-900 rounded-lg p-6 max-w-md w-full mx-4 border border-gray-200 dark:border-gray-700" > +
@@ -1029,6 +1029,7 @@ export default function HomePage() { )}
+
)} diff --git a/apps/web/components/ChatLog.tsx b/apps/web/components/ChatLog.tsx index d1407f9d..2c64541a 100644 --- a/apps/web/components/ChatLog.tsx +++ b/apps/web/components/ChatLog.tsx @@ -9,7 +9,12 @@ import ToolResultItem from './ToolResultItem'; // Tool Message Component - Enhanced with new design const ToolMessage = ({ content, metadata }: { content: unknown; metadata?: { tool_name?: string; summary?: string; description?: string; file_path?: string; [key: string]: unknown } }) => { // Process tool content to extract action and file path - const processToolContent = (rawContent: unknown) => { + const processToolContent = (rawContent: unknown): { + action: 'Edited' | 'Created' | 'Read' | 'Deleted' | 'Generated' | 'Searched' | 'Executed'; + filePath: string; + cleanContent: string | undefined; + toolName: string; + } => { let processedContent = '' as string; let action: 'Edited' | 'Created' | 'Read' | 'Deleted' | 'Generated' | 'Searched' | 'Executed' = 'Executed'; let filePath = ''; diff --git a/apps/web/components/ToolResultItem.tsx b/apps/web/components/ToolResultItem.tsx index 5765dca8..96965d91 100644 --- a/apps/web/components/ToolResultItem.tsx +++ b/apps/web/components/ToolResultItem.tsx @@ -110,7 +110,7 @@ const ToolResultItem: React.FC = ({ action, filePath, conte animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.2 }} - className="overflow-hidden" + style={{ overflow: 'hidden' }} >
diff --git a/apps/web/hooks/useUserRequests.ts b/apps/web/hooks/useUserRequests.ts
index 757de0bf..85226c33 100644
--- a/apps/web/hooks/useUserRequests.ts
+++ b/apps/web/hooks/useUserRequests.ts
@@ -14,7 +14,7 @@ export function useUserRequests({ projectId }: UseUserRequestsOptions) {
   const [activeCount, setActiveCount] = useState(0);
   const [isTabVisible, setIsTabVisible] = useState(true); // 기본값 true로 설정
   
-  const intervalRef = useRef();
+  const intervalRef = useRef(null);
   const previousActiveState = useRef(false);
 
   // 탭 활성화 상태 추적
@@ -65,7 +65,7 @@ export function useUserRequests({ projectId }: UseUserRequestsOptions) {
     if (!isTabVisible) {
       if (intervalRef.current) {
         clearInterval(intervalRef.current);
-        intervalRef.current = undefined;
+        intervalRef.current = null;
       }
       return;
     }
diff --git a/apps/web/package.json b/apps/web/package.json
index 0345bb59..35318fc5 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -18,6 +18,7 @@
     "open": "^10.2.0",
     "react": "18.2.0",
     "react-dom": "18.2.0",
+    "react-icons": "^5.5.0",
     "react-markdown": "^10.1.0",
     "react-syntax-highlighter": "^15.5.0"
   },