Skip to content
Open
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
61 changes: 61 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -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
142 changes: 142 additions & 0 deletions DOCKER_DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -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
79 changes: 79 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
4 changes: 2 additions & 2 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,6 @@ export default function HomePage() {
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Ask Claudable to create a blog about..."
maxLength={50000}
disabled={isCreatingProject}
className="flex w-full rounded-md px-2 py-2 placeholder:text-gray-400 dark:placeholder:text-white/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 resize-none text-[16px] leading-snug md:text-base focus-visible:ring-0 focus-visible:ring-offset-0 bg-transparent focus:bg-transparent flex-1 text-gray-900 dark:text-white overflow-y-auto"
style={{ height: '120px' }}
Expand Down Expand Up @@ -985,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"
>
<div 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">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand Down Expand Up @@ -1030,6 +1029,7 @@ export default function HomePage() {
)}
</button>
</div>
</div>
</motion.div>
</div>
)}
Expand Down
7 changes: 6 additions & 1 deletion apps/web/components/ChatLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/ToolResultItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const ToolResultItem: React.FC<ToolResultItemProps> = ({ action, filePath, conte
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
style={{ overflow: 'hidden' }}
>
<div className="mt-2 ml-6 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<pre className="text-xs text-gray-700 dark:text-gray-300 font-mono whitespace-pre-wrap break-words">
Expand Down
1 change: 0 additions & 1 deletion apps/web/components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,6 @@ export default function ChatInput({
className="flex w-full ring-offset-background placeholder:text-gray-500 dark:placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 resize-none text-[16px] leading-snug md:text-base max-h-[200px] bg-transparent focus:bg-transparent flex-1 m-1 rounded-md p-0 text-gray-900 dark:text-gray-100"
id="chatinput"
placeholder={placeholder}
maxLength={50000}
disabled={disabled}
style={{ minHeight: '40px', height: '40px' }}
/>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/hooks/useUserRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function useUserRequests({ projectId }: UseUserRequestsOptions) {
const [activeCount, setActiveCount] = useState(0);
const [isTabVisible, setIsTabVisible] = useState(true); // 기본값 true로 설정

const intervalRef = useRef<NodeJS.Timeout>();
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const previousActiveState = useRef(false);

// 탭 활성화 상태 추적
Expand Down Expand Up @@ -65,7 +65,7 @@ export function useUserRequests({ projectId }: UseUserRequestsOptions) {
if (!isTabVisible) {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = undefined;
intervalRef.current = null;
}
return;
}
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
26 changes: 26 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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