diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml new file mode 100644 index 00000000..fb48d47b --- /dev/null +++ b/.github/workflows/docker-images.yml @@ -0,0 +1,116 @@ +name: Build Docker Images + +on: + push: + branches: + - main + tags: + - "v*" + paths: + - ".dockerignore" + - ".github/workflows/docker-images.yml" + - "docker-compose.build.yml" + - "docker-compose.yml" + - "infrastructure/Dockerfile.api" + - "infrastructure/Dockerfile.web" + - "package-lock.json" + - "package.json" + - "packages/api/**" + - "packages/shared/**" + - "packages/web-next/**" + - "tsconfig.base.json" + pull_request: + paths: + - ".dockerignore" + - ".github/workflows/docker-images.yml" + - "docker-compose.build.yml" + - "docker-compose.yml" + - "infrastructure/Dockerfile.api" + - "infrastructure/Dockerfile.web" + - "package-lock.json" + - "package.json" + - "packages/api/**" + - "packages/shared/**" + - "packages/web-next/**" + - "tsconfig.base.json" + workflow_dispatch: + inputs: + platforms: + description: "Target platforms, e.g. linux/amd64 or linux/amd64,linux/arm64" + required: false + default: "" + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + DEFAULT_PLATFORMS: linux/amd64 + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: api + image: synapse-api + dockerfile: infrastructure/Dockerfile.api + - name: web + image: synapse-web + dockerfile: infrastructure/Dockerfile.web + + steps: + - uses: actions/checkout@v4 + + - name: Resolve image namespace + id: namespace + shell: bash + run: | + owner="${GITHUB_REPOSITORY_OWNER,,}" + echo "owner=${owner}" >> "$GITHUB_OUTPUT" + + - name: Resolve target platforms + id: platforms + shell: bash + env: + INPUT_PLATFORMS: ${{ github.event_name == 'workflow_dispatch' && inputs.platforms || '' }} + REPO_PLATFORMS: ${{ vars.DOCKER_IMAGE_PLATFORMS || '' }} + run: | + platforms="${INPUT_PLATFORMS:-${REPO_PLATFORMS:-${DEFAULT_PLATFORMS}}}" + echo "value=${platforms}" >> "$GITHUB_OUTPUT" + echo "Building ${{ matrix.name }} for ${platforms}" + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ steps.namespace.outputs.owner }}/${{ matrix.image }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=sha- + + - name: Build and publish image + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: ${{ steps.platforms.outputs.value }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.name }} + cache-to: ${{ github.event_name != 'pull_request' && format('type=gha,scope={0},mode=max', matrix.name) || '' }} diff --git a/README.md b/README.md index c4599b39..c0801d1c 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,7 @@ This repository currently ships with a self-hosting path centered on a single Ub - `systemd` for the API and desktop web services - `nginx` as the public entrypoint - Dockerized PostgreSQL and Redis for local infrastructure +- prebuilt GHCR images for optional containerized API and desktop web services - desktop web served from `packages/web-next` - mobile web exported as static assets from `packages/mobile-app` diff --git a/README_CN.md b/README_CN.md index 4bb4f95b..417f4075 100644 --- a/README_CN.md +++ b/README_CN.md @@ -196,6 +196,7 @@ npm run web - `systemd` 负责 API 与桌面 Web 进程 - `nginx` 作为公网入口 - PostgreSQL 与 Redis 通过 Docker 运行 +- API 与桌面 Web 也提供预构建 GHCR 镜像,可选容器化运行 - 桌面端由 `packages/web-next` 提供 - mobile web 由 `packages/mobile-app` 静态导出 diff --git a/README_ES.md b/README_ES.md index 7a386a56..167be880 100644 --- a/README_ES.md +++ b/README_ES.md @@ -196,6 +196,7 @@ Hoy el repositorio trae una ruta de autoalojamiento pensada para una sola máqui - `systemd` para la API y la web de escritorio - `nginx` como punto de entrada público - PostgreSQL y Redis en Docker +- imágenes precompiladas en GHCR para ejecutar opcionalmente la API y la web de escritorio en contenedores - la web de escritorio servida desde `packages/web-next` - el mobile web exportado estáticamente desde `packages/mobile-app` diff --git a/deploy.md b/deploy.md index 6b312d25..66e1158a 100644 --- a/deploy.md +++ b/deploy.md @@ -5,6 +5,7 @@ This repository is deployed on a single Ubuntu host with: - local `nginx` - `systemd` for API and web - Docker for PostgreSQL and Redis +- Prebuilt GHCR images for the API and desktop web containers - desktop web on a production Next.js server - optional localhost-only Next dev server for remote debugging over SSH @@ -63,6 +64,25 @@ sudo docker compose up -d postgres redis Notes: - Redis is bound to loopback and requires a password from `.env` +- The production compose profile can also run the API and desktop web from prebuilt images: + +```bash +sudo docker compose --profile production pull api web +sudo docker compose --profile production up -d postgres redis api web +``` + +By default these services use: + +- `ghcr.io/zai-org/synapse-api:${SYNAPSE_IMAGE_TAG:-latest}` +- `ghcr.io/zai-org/synapse-web:${SYNAPSE_IMAGE_TAG:-latest}` + +Set `SYNAPSE_IMAGE_TAG`, `SYNAPSE_API_IMAGE`, or `SYNAPSE_WEB_IMAGE` in `.env` to pin a release or use a different registry. + +If you need to build the application images locally instead of pulling from GHCR, use the build override: + +```bash +sudo docker compose -f docker-compose.yml -f docker-compose.build.yml --profile production up -d --build postgres redis api web +``` ## 4. Application dependencies diff --git a/docker-compose.build.yml b/docker-compose.build.yml new file mode 100644 index 00000000..07577d28 --- /dev/null +++ b/docker-compose.build.yml @@ -0,0 +1,10 @@ +services: + api: + build: + context: . + dockerfile: infrastructure/Dockerfile.api + + web: + build: + context: . + dockerfile: infrastructure/Dockerfile.web diff --git a/docker-compose.yml b/docker-compose.yml index 84e7bccd..c383f940 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,9 +39,7 @@ services: retries: 5 api: - build: - context: . - dockerfile: infrastructure/Dockerfile.api + image: ${SYNAPSE_API_IMAGE:-ghcr.io/zai-org/synapse-api}:${SYNAPSE_IMAGE_TAG:-latest} container_name: synapse-api environment: DATABASE_URL: postgresql://${POSTGRES_USER:-synapse}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-synapse} @@ -68,9 +66,7 @@ services: - production web: - build: - context: . - dockerfile: infrastructure/Dockerfile.web + image: ${SYNAPSE_WEB_IMAGE:-ghcr.io/zai-org/synapse-web}:${SYNAPSE_IMAGE_TAG:-latest} container_name: synapse-web environment: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} diff --git a/infrastructure/Dockerfile.api b/infrastructure/Dockerfile.api index 8d038f4d..2585cb12 100644 --- a/infrastructure/Dockerfile.api +++ b/infrastructure/Dockerfile.api @@ -3,14 +3,16 @@ WORKDIR /app COPY package.json package-lock.json ./ COPY packages/shared/package.json packages/shared/ COPY packages/api/package.json packages/api/ -RUN npm ci --workspace=packages/shared --workspace=packages/api +# CPU runtime files are bundled by onnxruntime-node; skip optional CUDA provider downloads. +RUN ONNXRUNTIME_NODE_INSTALL=skip ONNXRUNTIME_NODE_INSTALL_CUDA=skip \ + npm ci --workspace=packages/shared --workspace=packages/api COPY tsconfig.base.json ./ COPY packages/shared/ packages/shared/ COPY packages/api/ packages/api/ -RUN npx tsc -b packages/shared +RUN npm run build -w packages/shared && npm run build -w packages/api FROM node:20-alpine WORKDIR /app COPY --from=builder /app/ ./ EXPOSE 3001 -CMD ["npx", "tsx", "packages/api/src/index.ts"] +CMD ["node", "packages/api/dist/index.js"] diff --git a/infrastructure/Dockerfile.web b/infrastructure/Dockerfile.web index b087cc66..5446fea5 100644 --- a/infrastructure/Dockerfile.web +++ b/infrastructure/Dockerfile.web @@ -1,21 +1,28 @@ FROM node:20-alpine AS builder WORKDIR /app -COPY packages/shared /app/packages/shared -WORKDIR /app/packages/shared -RUN npm install -RUN npm run build +COPY package.json package-lock.json ./ +COPY packages/shared/package.json packages/shared/ +COPY packages/web-next/package.json packages/web-next/ +RUN npm ci --workspace=packages/shared --workspace=packages/web-next -COPY packages/web-next /app/packages/web-next -WORKDIR /app/packages/web-next -RUN npm ci -RUN npm run build +COPY tsconfig.base.json ./ +COPY packages/shared/ packages/shared/ +COPY packages/web-next/ packages/web-next/ +RUN npm run build -w packages/shared +RUN npm run build -w packages/web-next FROM node:20-alpine WORKDIR /app -COPY --from=builder /app/packages/web-next/.next ./.next -COPY --from=builder /app/packages/web-next/public ./public -COPY --from=builder /app/packages/web-next/node_modules ./node_modules -COPY --from=builder /app/packages/web-next/package.json ./ +ENV NODE_ENV=production +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/package-lock.json ./package-lock.json +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/packages/shared/package.json ./packages/shared/package.json +COPY --from=builder /app/packages/shared/dist ./packages/shared/dist +COPY --from=builder /app/packages/web-next/.next ./packages/web-next/.next +COPY --from=builder /app/packages/web-next/next.config.mjs ./packages/web-next/next.config.mjs +COPY --from=builder /app/packages/web-next/package.json ./packages/web-next/package.json +COPY --from=builder /app/packages/web-next/public ./packages/web-next/public EXPOSE 3000 -CMD ["npx", "next", "start", "-p", "3000"] +CMD ["npm", "run", "start", "-w", "packages/web-next", "--", "-p", "3000"] diff --git a/package-lock.json b/package-lock.json index 00c93a05..9a5464c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21279,9 +21279,27 @@ "name": "@synapse/shared", "version": "0.1.0", "devDependencies": { + "@types/node": "^25.1.0", "typescript": "^5.4.0" } }, + "packages/shared/node_modules/@types/node": { + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "packages/shared/node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, "packages/web-next": { "version": "0.0.1", "dependencies": { diff --git a/package.json b/package.json index 551cad18..86d3ce01 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dev:daemon": "npm run dev -w packages/remote-agent-daemon", "dev:web": "npm run dev -w packages/web-next", "dev:mock-chat": "node .workbuddy/mock-chat-completions-server.cjs", - "prepare": "git config core.hooksPath .githooks", + "prepare": "(command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1 && git config core.hooksPath .githooks) || true", "format": "node scripts/format-files.mjs --all", "format:check": "node scripts/format-files.mjs --all --check", "format:staged": "lint-staged", diff --git a/packages/shared/package.json b/packages/shared/package.json index 57cdf190..ad89f4d3 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -32,6 +32,7 @@ "dev": "tsc --watch" }, "devDependencies": { + "@types/node": "^25.1.0", "typescript": "^5.4.0" } }