diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index 856ea21..861330f 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -1,9 +1,10 @@ -name: Deploy web app +name: Build web app on: workflow_dispatch: inputs: tag: - description: "Git tag / registry tag (e.g. 1.0.2). Image is built and pushed every run." + description: "Git tag / registry tag (e.g. 1.0.2). Image is built and pushed + every run." required: true type: string @@ -68,57 +69,3 @@ jobs: ${{ env.IMAGE }}:${{ steps.meta.outputs.tag }} ${{ env.IMAGE }}:sha-${{ steps.git.outputs.short }} ${{ env.IMAGE }}:latest - - deploy: - name: Deploy to host - needs: build-package - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Ensure remote directory - uses: appleboy/ssh-action@v1.2.5 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - port: 22 - timeout: 2m - script: mkdir -p ~/pengine - - - name: Copy docker-compose.yml to host - uses: appleboy/scp-action@v0.1.7 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - port: 22 - timeout: 2m - source: deployment/docker-compose.yml - target: ~/pengine/ - overwrite: true - strip_components: 1 - - - name: SSH — docker login, compose pull, up - uses: appleboy/ssh-action@v1.2.5 - env: - GHCR_USER: ${{ github.repository_owner }} - GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PENGINE_WEB_IMAGE: "${{ needs.build-package.outputs.image }}:${{ needs.build-package.outputs.tag }}" - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - port: 22 - timeout: 3m - command_timeout: 10m - envs: GHCR_USER,GHCR_TOKEN,PENGINE_WEB_IMAGE - script: | - set -euo pipefail - cd ~/pengine/deployment - echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin - export PENGINE_WEB_IMAGE - docker compose pull - docker compose up -d diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000..6999f81 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,66 @@ +# Pengine deployment + +## Production (same host as Pengui) + +**Use the Pengui stack** — Pengine is an optional Compose **profile** there, so it shares **`pengui-network`** with nginx (no second compose project, no `external` network). + +1. In the Pengui repo, set **`PENGINE_ENABLE=1`** and **`PENGINE_WEB_IMAGE`** (e.g. `ghcr.io/pengine-ai/pengine-web:1.0.1`) in **GitHub Actions variables** or in `~/pengui/deployment/.env`. +2. Deploy Pengui (`deploy.sh` or CI). That runs `docker compose --profile pengine up -d pengine-web`. +3. Nginx proxies to **`http://pengine-app:1422`** (see Pengui `nginx/templates`). + +**Do not** run this directory’s `docker compose up` on the same server at the same time — you would get a duplicate **`pengine-app`** name. Remove any old standalone Pengine stack first: `docker rm -f pengine-app` (only if moving to Pengui profile). + +## Local / standalone (this repo only) + +```bash +cd deployment +docker compose up -d +curl -fsS http://127.0.0.1:1422/ | head +``` + +## TLS for `pengine.net` + +Configure **`PENGINE_SUBDOMAIN`** on the **Pengui** repo (Certbot + nginx vhost); see Pengui `deployment/README.md`. + +## Remove the container and pull a fresh image + +Use this after a new image tag is published, if the container is stuck, or you want to clear the cached local image. + +### Production (Pengui stack, profile `pengine`) + +Run on the server: + +```bash +cd ~/pengui/deployment + +docker compose --profile pengine stop pengine-web +docker compose rm -f pengine-web + +# If a stray container exists outside compose: +docker rm -f pengine-app 2>/dev/null || true + +# Optional: remove cached images so the next pull is guaranteed fresh +for id in $(docker images 'ghcr.io/pengine-ai/pengine-web' -q); do docker rmi -f "$id"; done 2>/dev/null || true + +docker compose pull pengine-web +docker compose --profile pengine up -d pengine-web +``` + +Private images require **`docker login ghcr.io`** (PAT with `read:packages`) first. + +### Local / standalone (this repo’s `deployment/docker-compose.yml`) + +```bash +cd deployment + +docker compose down +docker rmi ghcr.io/pengine-ai/pengine-web:latest 2>/dev/null || true # adjust tag if needed + +docker compose pull +docker compose up -d +``` + +## Troubleshooting + +- **`network … external … not found`**: use **Pengui + `--profile pengine`**, not a separate compose with `external: pengui-network`. +- **`incorrect label com.docker.compose.network`**: never run `docker network create pengui-network` by hand; let Pengui’s `docker compose up` create the network. diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 7b4f2cc..d8c6963 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -1,13 +1,17 @@ +# Local / standalone: Pengine only (default bridge, port 1422 on host). +# +# Production next to Pengui: use Pengui’s docker-compose with --profile pengine +# (see pengui repo deployment/docker-compose.yml) — same network as nginx, no external network. + services: pengine-web: image: ${PENGINE_WEB_IMAGE:-ghcr.io/pengine-ai/pengine-web:latest} container_name: pengine-app hostname: pengine restart: unless-stopped - # Vite preview — serves `dist/` (must match deploy/Dockerfile CMD). command: ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "1422"] - expose: - - "1422" + ports: + - "1422:1422" environment: - NODE_ENV=production healthcheck: diff --git a/vite.config.ts b/vite.config.ts index f28f0f1..f582596 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -49,6 +49,15 @@ export default defineConfig(async () => { preview: { port: 1422, strictPort: true, + // Required when nginx (or any reverse proxy) sends Host: pengine.net — Vite 7 blocks unknown hosts by default. + allowedHosts: [ + "pengine.net", + "localhost", + "127.0.0.1", + ...(process.env.VITE_PREVIEW_ALLOWED_HOSTS?.split(",") + .map((h) => h.trim()) + .filter(Boolean) ?? []), + ], }, }; });