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..10d6df2c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +# 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 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 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 + +# 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/apps/api/app/services/cli/unified_manager.py b/apps/api/app/services/cli/unified_manager.py index 56dd8744..4130eb3a 100644 --- a/apps/api/app/services/cli/unified_manager.py +++ b/apps/api/app/services/cli/unified_manager.py @@ -18,7 +18,8 @@ def get_project_root() -> str: """Get project root directory using relative path navigation""" current_file_dir = os.path.dirname(os.path.abspath(__file__)) - # unified_manager.py -> cli -> services -> app -> api -> apps -> project-root + # unified_manager.py is in: app/services/cli/ + # Navigate: cli -> services -> app -> api -> apps -> project-root project_root = os.path.join(current_file_dir, "..", "..", "..", "..", "..") return os.path.abspath(project_root) @@ -1026,8 +1027,9 @@ async def _ensure_agent_md(self, project_path: str) -> None: try: # Read system prompt from the source file using relative path current_file_dir = os.path.dirname(os.path.abspath(__file__)) - # unified_manager.py -> cli -> services -> app - app_dir = os.path.join(current_file_dir, "..", "..", "..") + # unified_manager.py is in: app/services/cli/ + # Navigate: cli -> services -> app + app_dir = os.path.join(current_file_dir, "..", "..") app_dir = os.path.abspath(app_dir) system_prompt_path = os.path.join(app_dir, "prompt", "system-prompt.md") 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" > +
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"
},
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