Skip to content

Commit bcf9303

Browse files
committed
Merge Docker support PR opactorai#8 from upstream
2 parents a57ab5a + 9956ebd commit bcf9303

File tree

9 files changed

+320
-5
lines changed

9 files changed

+320
-5
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: docker build and publish
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- feat/dockerize-app
8+
tags:
9+
- 'v*'
10+
pull_request:
11+
branches:
12+
- main
13+
14+
env:
15+
REGISTRY: ghcr.io
16+
IMAGE_NAME: ${{ github.repository }}
17+
18+
jobs:
19+
build:
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
packages: write
24+
25+
steps:
26+
- name: checkout repository
27+
uses: actions/checkout@v4
28+
29+
- name: set up docker buildx
30+
uses: docker/setup-buildx-action@v3
31+
32+
- name: log in to github container registry
33+
if: github.event_name != 'pull_request'
34+
uses: docker/login-action@v3
35+
with:
36+
registry: ${{ env.REGISTRY }}
37+
username: ${{ github.actor }}
38+
password: ${{ secrets.GITHUB_TOKEN }}
39+
40+
- name: extract metadata
41+
id: meta
42+
uses: docker/metadata-action@v5
43+
with:
44+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
45+
tags: |
46+
type=ref,event=branch
47+
type=ref,event=pr
48+
type=semver,pattern={{version}}
49+
type=semver,pattern={{major}}.{{minor}}
50+
type=raw,value=latest,enable={{is_default_branch}}
51+
52+
- name: build and push docker image
53+
uses: docker/build-push-action@v5
54+
with:
55+
context: .
56+
platforms: linux/amd64,linux/arm64
57+
push: ${{ github.event_name != 'pull_request' }}
58+
tags: ${{ steps.meta.outputs.tags }}
59+
labels: ${{ steps.meta.outputs.labels }}
60+
cache-from: type=gha
61+
cache-to: type=gha,mode=max

DOCKER_DEPLOYMENT.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# claudable docker deployment guide
2+
3+
## quick start - one click run
4+
5+
### prerequisites
6+
- docker and docker compose installed
7+
- claude api key from anthropic
8+
9+
### 1. pull and run (easiest method)
10+
11+
```bash
12+
# pull the latest image
13+
docker pull ghcr.io/opactorai/claudable:latest
14+
15+
# run with your api key
16+
docker run -d \
17+
-p 3000:3000 \
18+
-p 8080:8080 \
19+
-e ANTHROPIC_API_KEY="your-claude-api-key-here" \
20+
-v claudable-data:/app/data \
21+
--name claudable \
22+
ghcr.io/opactorai/claudable:latest
23+
```
24+
25+
### 2. using docker compose
26+
27+
create a `.env` file:
28+
```bash
29+
ANTHROPIC_API_KEY=your-claude-api-key-here
30+
```
31+
32+
create `docker-compose.yml`:
33+
```yaml
34+
version: '3.8'
35+
36+
services:
37+
claudable:
38+
image: ghcr.io/opactorai/claudable:latest
39+
ports:
40+
- "3000:3000"
41+
- "8080:8080"
42+
environment:
43+
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
44+
volumes:
45+
- claudable-data:/app/data
46+
restart: unless-stopped
47+
48+
volumes:
49+
claudable-data:
50+
```
51+
52+
run:
53+
```bash
54+
docker compose up -d
55+
```
56+
57+
## access the application
58+
59+
- **frontend**: http://localhost:3000
60+
- **api**: http://localhost:8080
61+
- **api docs**: http://localhost:8080/docs
62+
63+
## build from source
64+
65+
```bash
66+
# clone repository
67+
git clone https://github.com/opactorai/claudable.git
68+
cd claudable
69+
70+
# build image
71+
docker build -t claudable:local .
72+
73+
# run with docker compose
74+
docker compose up -d
75+
```
76+
77+
## environment variables
78+
79+
| variable | description | required | default |
80+
|----------|-------------|----------|---------|
81+
| `ANTHROPIC_API_KEY` | claude api key for ai functionality | yes | - |
82+
| `NEXT_PUBLIC_API_URL` | backend api url | no | http://localhost:8080 |
83+
| `API_PORT` | api server port | no | 8080 |
84+
| `WEB_PORT` | web server port | no | 3000 |
85+
86+
## data persistence
87+
88+
the sqlite database is stored in `/app/data` inside the container. to persist data:
89+
90+
```bash
91+
# named volume (recommended)
92+
docker run -v claudable-data:/app/data ...
93+
94+
# or bind mount
95+
docker run -v $(pwd)/data:/app/data ...
96+
```
97+
98+
## stopping and removing
99+
100+
```bash
101+
# stop container
102+
docker stop claudable
103+
104+
# remove container
105+
docker rm claudable
106+
107+
# remove image
108+
docker rmi ghcr.io/opactorai/claudable:latest
109+
```
110+
111+
## troubleshooting
112+
113+
### ports already in use
114+
change the port mapping:
115+
```bash
116+
docker run -p 3001:3000 -p 8081:8080 ...
117+
```
118+
119+
### permission issues
120+
ensure the data directory has correct permissions:
121+
```bash
122+
docker exec claudable chown -R nextjs:nodejs /app/data
123+
```
124+
125+
### api connection issues
126+
verify the api is accessible:
127+
```bash
128+
docker logs claudable
129+
curl http://localhost:8080/docs
130+
```
131+
132+
## security notes
133+
134+
- **never commit** your `.env` file with api keys
135+
- use docker secrets for production deployments
136+
- regularly update the base image for security patches
137+
- consider using a reverse proxy for production
138+
139+
## support
140+
141+
- github issues: https://github.com/opactorai/claudable/issues
142+
- discord: https://discord.gg/njnbafhnqc

Dockerfile

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# multi-stage build for claudable app
2+
FROM node:20-alpine AS base
3+
4+
# install python and dependencies
5+
RUN apk add --no-cache python3 py3-pip python3-dev build-base git
6+
7+
# set working directory
8+
WORKDIR /app
9+
10+
# copy package files and scripts
11+
COPY package*.json ./
12+
COPY scripts ./scripts
13+
COPY apps/web/package*.json ./apps/web/
14+
COPY apps/api/requirements.txt ./apps/api/
15+
16+
# install node dependencies without running postinstall
17+
RUN npm ci --only=production --ignore-scripts
18+
19+
# build stage for next.js
20+
FROM base AS builder
21+
WORKDIR /app
22+
COPY . .
23+
24+
# install all dependencies for build
25+
RUN npm ci --ignore-scripts
26+
WORKDIR /app/apps/web
27+
RUN npm ci
28+
29+
# build next.js app
30+
WORKDIR /app/apps/web
31+
RUN npm run build
32+
33+
# production stage
34+
FROM node:20-alpine AS runner
35+
RUN apk add --no-cache python3 py3-pip
36+
37+
WORKDIR /app
38+
39+
# create non-root user
40+
RUN addgroup -g 1001 -S nodejs && \
41+
adduser -S nextjs -u 1001
42+
43+
# copy built application
44+
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./apps/web/.next
45+
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
46+
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/package*.json ./apps/web/
47+
COPY --from=builder --chown=nextjs:nodejs /app/apps/api ./apps/api
48+
COPY --from=builder --chown=nextjs:nodejs /app/package*.json ./
49+
COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts
50+
51+
# install production dependencies
52+
RUN npm ci --only=production
53+
WORKDIR /app/apps/web
54+
RUN npm ci --only=production
55+
56+
# setup python environment
57+
WORKDIR /app/apps/api
58+
RUN python3 -m venv .venv && \
59+
.venv/bin/pip install --no-cache-dir -r requirements.txt
60+
61+
# create data directory for sqlite
62+
RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data
63+
64+
USER nextjs
65+
66+
# expose ports
67+
EXPOSE 3000 8080
68+
69+
# environment variables
70+
ENV NODE_ENV=production \
71+
ANTHROPIC_API_KEY="" \
72+
NEXT_PUBLIC_API_URL=http://localhost:8080 \
73+
API_HOST=0.0.0.0 \
74+
API_PORT=8080 \
75+
WEB_PORT=3000
76+
77+
# start both services
78+
WORKDIR /app
79+
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"]

apps/web/app/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -984,8 +984,8 @@ export default function HomePage() {
984984
initial={{ opacity: 0, scale: 0.9 }}
985985
animate={{ opacity: 1, scale: 1 }}
986986
exit={{ opacity: 0, scale: 0.9 }}
987-
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"
988987
>
988+
<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">
989989
<div className="flex items-center gap-3 mb-4">
990990
<div className="w-10 h-10 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center">
991991
<svg className="w-5 h-5 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -1029,6 +1029,7 @@ export default function HomePage() {
10291029
)}
10301030
</button>
10311031
</div>
1032+
</div>
10321033
</motion.div>
10331034
</div>
10341035
)}

apps/web/components/ChatLog.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import ToolResultItem from './ToolResultItem';
99
// Tool Message Component - Enhanced with new design
1010
const ToolMessage = ({ content, metadata }: { content: unknown; metadata?: { tool_name?: string; summary?: string; description?: string; file_path?: string; [key: string]: unknown } }) => {
1111
// Process tool content to extract action and file path
12-
const processToolContent = (rawContent: unknown) => {
12+
const processToolContent = (rawContent: unknown): {
13+
action: 'Edited' | 'Created' | 'Read' | 'Deleted' | 'Generated' | 'Searched' | 'Executed';
14+
filePath: string;
15+
cleanContent: string | undefined;
16+
toolName: string;
17+
} => {
1318
let processedContent = '' as string;
1419
let action: 'Edited' | 'Created' | 'Read' | 'Deleted' | 'Generated' | 'Searched' | 'Executed' = 'Executed';
1520
let filePath = '';

apps/web/components/ToolResultItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const ToolResultItem: React.FC<ToolResultItemProps> = ({ action, filePath, conte
110110
animate={{ height: 'auto', opacity: 1 }}
111111
exit={{ height: 0, opacity: 0 }}
112112
transition={{ duration: 0.2 }}
113-
className="overflow-hidden"
113+
style={{ overflow: 'hidden' }}
114114
>
115115
<div className="mt-2 ml-6 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
116116
<pre className="text-xs text-gray-700 dark:text-gray-300 font-mono whitespace-pre-wrap break-words">

apps/web/hooks/useUserRequests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function useUserRequests({ projectId }: UseUserRequestsOptions) {
1414
const [activeCount, setActiveCount] = useState(0);
1515
const [isTabVisible, setIsTabVisible] = useState(true); // 기본값 true로 설정
1616

17-
const intervalRef = useRef<NodeJS.Timeout>();
17+
const intervalRef = useRef<NodeJS.Timeout | null>(null);
1818
const previousActiveState = useRef(false);
1919

2020
// 탭 활성화 상태 추적
@@ -65,7 +65,7 @@ export function useUserRequests({ projectId }: UseUserRequestsOptions) {
6565
if (!isTabVisible) {
6666
if (intervalRef.current) {
6767
clearInterval(intervalRef.current);
68-
intervalRef.current = undefined;
68+
intervalRef.current = null;
6969
}
7070
return;
7171
}

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"open": "^10.2.0",
1919
"react": "18.2.0",
2020
"react-dom": "18.2.0",
21+
"react-icons": "^5.5.0",
2122
"react-markdown": "^10.1.0",
2223
"react-syntax-highlighter": "^15.5.0"
2324
},

docker-compose.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
version: '3.8'
2+
3+
services:
4+
claudable:
5+
build:
6+
context: .
7+
dockerfile: Dockerfile
8+
ports:
9+
- "3000:3000" # next.js frontend
10+
- "8080:8080" # fastapi backend
11+
environment:
12+
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
13+
- NODE_ENV=production
14+
- NEXT_PUBLIC_API_URL=http://localhost:8080
15+
- API_HOST=0.0.0.0
16+
- API_PORT=8080
17+
- WEB_PORT=3000
18+
volumes:
19+
- ./data:/app/data # persist sqlite database
20+
restart: unless-stopped
21+
healthcheck:
22+
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000"]
23+
interval: 30s
24+
timeout: 10s
25+
retries: 3
26+
start_period: 40s

0 commit comments

Comments
 (0)