Skip to content

Commit 1854d28

Browse files
authored
Merge pull request #8 from ZengLiangYi/phase1-cloud-containerization
cloud containerization
2 parents 02605b8 + ddff1a2 commit 1854d28

96 files changed

Lines changed: 5377 additions & 156 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.git
2+
.github
3+
node_modules
4+
server/node_modules
5+
client/node_modules
6+
site/node_modules
7+
site
8+
promo
9+
docs
10+
electron
11+
dist
12+
server/dist
13+
client/dist
14+
electron/dist
15+
release
16+
data
17+
data-demo
18+
*.db
19+
*.db-journal
20+
*.log
21+
.env
22+
.env.local
23+
.astro
24+
.claude
25+
.trae
26+
.codebuddy
27+
.kiro
28+
.agents
29+
.uploads
30+
skills

.env.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
# Server
88
PORT=3721
9+
# Docker Compose binds to localhost by default. Put a reverse proxy with HTTPS in
10+
# front of ChatCrystal before exposing it publicly.
11+
# BIND_ADDRESS=127.0.0.1
12+
# CHATCRYSTAL_HOST_PORT=3721
913
# DATA_DIR=./data
1014

1115
# Data source overrides
@@ -57,3 +61,15 @@ PORT=3721
5761
# LLM_BASE_URL=https://openrouter.ai/api/v1
5862
# LLM_API_KEY=...
5963
# LLM_MODEL=anthropic/claude-3.5-haiku
64+
65+
# Docker cloud deployment
66+
# CHATCRYSTAL_IMAGE_TAG=latest
67+
# CHATCRYSTAL_CLOUD_MODE=true
68+
# CHATCRYSTAL_API_TOKEN=
69+
# CHATCRYSTAL_ALLOW_INSECURE_REMOTE_HTTP=false
70+
#
71+
# In Docker, localhost points inside the container.
72+
# For Ollama on Docker Desktop, use:
73+
# CHATCRYSTAL_DOCKER_LLM_BASE_URL=http://host.docker.internal:11434
74+
# CHATCRYSTAL_DOCKER_EMBEDDING_BASE_URL=http://host.docker.internal:11434
75+
# The default docker-compose.yml already maps host.docker.internal for Linux.

.github/workflows/docker.yml

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
name: Docker
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- Dockerfile
7+
- docker-compose.yml
8+
- docker-compose.build.yml
9+
- .dockerignore
10+
- package.json
11+
- package-lock.json
12+
- tsconfig.base.json
13+
- README.md
14+
- LICENSE
15+
- NOTICE
16+
- scripts/**
17+
- server/**
18+
- client/**
19+
- shared/**
20+
- .github/workflows/docker.yml
21+
push:
22+
branches: [main]
23+
tags:
24+
- v*
25+
paths:
26+
- Dockerfile
27+
- docker-compose.yml
28+
- docker-compose.build.yml
29+
- .dockerignore
30+
- package.json
31+
- package-lock.json
32+
- tsconfig.base.json
33+
- README.md
34+
- LICENSE
35+
- NOTICE
36+
- scripts/**
37+
- server/**
38+
- client/**
39+
- shared/**
40+
- .github/workflows/docker.yml
41+
workflow_dispatch:
42+
43+
permissions:
44+
contents: read
45+
46+
env:
47+
IMAGE_NAME: ghcr.io/zengliangyi/chatcrystal
48+
49+
jobs:
50+
validate:
51+
runs-on: ubuntu-latest
52+
permissions:
53+
contents: read
54+
steps:
55+
- uses: actions/checkout@v5
56+
with:
57+
persist-credentials: false
58+
59+
- name: Set up Docker Buildx
60+
uses: docker/setup-buildx-action@v3
61+
62+
- name: Validate Compose
63+
run: docker compose config
64+
65+
- name: Validate Compose source build override
66+
run: docker compose -f docker-compose.yml -f docker-compose.build.yml config
67+
68+
- name: Build image
69+
run: docker build -t chatcrystal:test .
70+
71+
- name: Smoke test CLI in image
72+
run: docker run --rm --entrypoint crystal chatcrystal:test --version
73+
74+
- name: Smoke test container
75+
run: |
76+
docker run -d --name chatcrystal-smoke \
77+
-e CHATCRYSTAL_CLOUD_MODE=true \
78+
-e CHATCRYSTAL_API_TOKEN=ci-smoke-token-123456 \
79+
-e DATA_DIR=/data \
80+
-p 127.0.0.1::3721 \
81+
chatcrystal:test
82+
mapped="$(docker port chatcrystal-smoke 3721/tcp)"
83+
port="${mapped##*:}"
84+
for _attempt in $(seq 1 30); do
85+
if curl -fsS "http://127.0.0.1:${port}/api/health"; then
86+
exit 0
87+
fi
88+
sleep 1
89+
done
90+
docker logs chatcrystal-smoke
91+
exit 1
92+
93+
- name: Cleanup
94+
if: always()
95+
run: docker rm -f chatcrystal-smoke || true
96+
97+
publish:
98+
if: github.event_name != 'pull_request'
99+
needs: validate
100+
runs-on: ubuntu-latest
101+
permissions:
102+
contents: read
103+
packages: write
104+
concurrency:
105+
group: docker-publish-${{ github.ref }}
106+
cancel-in-progress: true
107+
steps:
108+
- uses: actions/checkout@v5
109+
with:
110+
persist-credentials: false
111+
fetch-depth: 0
112+
113+
- name: Verify publish ref
114+
run: |
115+
git fetch --no-tags origin main
116+
if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
117+
remote_sha="$(git rev-parse origin/main)"
118+
if [ "${GITHUB_SHA}" != "${remote_sha}" ]; then
119+
echo "::error::Refusing to publish because ${GITHUB_SHA} is not the current origin/main (${remote_sha})."
120+
exit 1
121+
fi
122+
elif [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
123+
if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then
124+
echo "::error::Refusing to publish tag ${GITHUB_REF_NAME} because ${GITHUB_SHA} is not reachable from origin/main."
125+
exit 1
126+
fi
127+
else
128+
echo "::error::Docker image publishing is limited to main and v* tags."
129+
exit 1
130+
fi
131+
132+
- name: Set up QEMU
133+
uses: docker/setup-qemu-action@v3
134+
135+
- name: Set up Docker Buildx
136+
uses: docker/setup-buildx-action@v3
137+
138+
- name: Log in to GHCR
139+
uses: docker/login-action@v3
140+
with:
141+
registry: ghcr.io
142+
username: ${{ github.actor }}
143+
password: ${{ secrets.GITHUB_TOKEN }}
144+
145+
- name: Docker metadata
146+
id: meta
147+
uses: docker/metadata-action@v5
148+
with:
149+
images: ${{ env.IMAGE_NAME }}
150+
tags: |
151+
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
152+
type=sha,prefix=sha-
153+
type=ref,event=tag
154+
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
155+
156+
- name: Publish image
157+
id: publish
158+
uses: docker/build-push-action@v6
159+
with:
160+
context: .
161+
platforms: linux/amd64,linux/arm64
162+
push: true
163+
tags: ${{ steps.meta.outputs.tags }}
164+
labels: ${{ steps.meta.outputs.labels }}
165+
166+
- name: Verify public pull
167+
run: |
168+
docker logout ghcr.io || true
169+
if ! docker pull "${IMAGE_NAME}@${{ steps.publish.outputs.digest }}"; then
170+
echo "::error::GHCR image is not publicly pullable. Open the package settings for ghcr.io/zengliangyi/chatcrystal, change visibility to Public, then rerun this workflow."
171+
exit 1
172+
fi
173+
if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
174+
docker pull "${IMAGE_NAME}:latest"
175+
fi

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Added Docker cloud deployment for a personal single-instance ChatCrystal server.
6+
- Added GHCR publishing workflow and Compose defaults for pulling the published image.
7+
- Added first-run setup mode and shared bearer token authentication for Web, API, CLI, and MCP.
8+
- Added CLI cloud connection commands and remote import from local AI tool histories.
9+
- Disabled server-side local scan import in cloud mode to avoid scanning container paths.
10+
311
## [0.4.9] - 2026-04-29
412

513
### Experience Quality Gate

Dockerfile

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
FROM node:22-alpine AS build
2+
3+
WORKDIR /app
4+
5+
COPY package.json package-lock.json tsconfig.base.json ./
6+
COPY server/package.json server/package.json
7+
COPY client/package.json client/package.json
8+
COPY shared/package.json shared/package.json
9+
10+
RUN npm ci
11+
12+
COPY . .
13+
RUN npm run build
14+
15+
FROM node:22-alpine AS runtime
16+
17+
WORKDIR /app
18+
LABEL org.opencontainers.image.source="https://github.com/ZengLiangYi/ChatCrystal"
19+
LABEL org.opencontainers.image.description="ChatCrystal cloud server"
20+
LABEL org.opencontainers.image.licenses="Apache-2.0"
21+
22+
ENV NODE_ENV=production
23+
ENV PORT=3721
24+
ENV DATA_DIR=/data
25+
ENV CHATCRYSTAL_CLOUD_MODE=true
26+
27+
COPY package.json package-lock.json ./
28+
COPY server/package.json server/package.json
29+
COPY shared/package.json shared/package.json
30+
31+
RUN npm ci --omit=dev --workspace server --workspace shared --include-workspace-root=false --ignore-scripts \
32+
&& npm cache clean --force
33+
34+
COPY --from=build /app/server/dist ./server/dist
35+
COPY --from=build /app/client/dist ./client/dist
36+
COPY --from=build /app/shared/types ./shared/types
37+
COPY --from=build /app/README.md ./README.md
38+
COPY --from=build /app/LICENSE ./LICENSE
39+
COPY --from=build /app/NOTICE ./NOTICE
40+
41+
RUN chmod +x /app/server/dist/server/src/cli/index.js \
42+
&& ln -s /app/server/dist/server/src/cli/index.js /usr/local/bin/crystal \
43+
&& mkdir -p /data \
44+
&& chown -R node:node /data
45+
46+
USER node
47+
EXPOSE 3721
48+
VOLUME ["/data"]
49+
50+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
51+
CMD node -e "fetch('http://127.0.0.1:3721/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
52+
53+
CMD ["npm", "start", "-w", "server"]

0 commit comments

Comments
 (0)