diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d3d4e13 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +.git +.github +node_modules +.next +out +.vscode +.idea +.DS_Store +npm-debug.log* +yarn-debug.log* +yarn-error.log* +*.log +coverage +.env +.env.local +.env.development +.beads +docs +reference +assets +skills +help +install +remotion.config.ts diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 0000000..74a7152 --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,75 @@ +name: Build and Deploy + +on: + push: + branches: [main, master] + workflow_dispatch: + +env: + IMAGE_NAME: beadboard + REGISTRY: registry.viktorbarzin.me:5050 + +jobs: + build: + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.meta.outputs.sha }} + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - id: meta + run: echo "sha=$(echo ${{ github.sha }} | cut -c1-8)" >> "$GITHUB_OUTPUT" + + - name: Log in to private registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + platforms: linux/amd64 + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.sha }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - name: Trigger Woodpecker deploy + env: + # TODO: set WOODPECKER_REPO_ID once the beadboard repo is registered + # with Woodpecker (infra CLAUDE.md: "Woodpecker API uses numeric repo IDs"). + WOODPECKER_REPO_ID: "TBD" + run: | + if [ "$WOODPECKER_REPO_ID" = "TBD" ]; then + echo "Woodpecker repo not yet registered — skipping deploy trigger." + echo "Image built and pushed as ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag }}" + exit 0 + fi + for attempt in 1 2 3; do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "https://ci.viktorbarzin.me/api/repos/${WOODPECKER_REPO_ID}/pipelines" \ + -H "Authorization: Bearer ${{ secrets.WOODPECKER_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "{\"branch\":\"main\",\"variables\":{\"IMAGE_TAG\":\"${{ needs.build.outputs.image_tag }}\"}}") + if [ "$STATUS" -ge 200 ] && [ "$STATUS" -lt 300 ]; then + echo "Woodpecker deploy triggered (HTTP $STATUS)" + exit 0 + fi + echo "Attempt $attempt failed (HTTP $STATUS), retrying in 30s..." + sleep 30 + done + echo "Failed to trigger Woodpecker deploy after 3 attempts" + exit 1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..379c534 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# syntax=docker/dockerfile:1.7 + +FROM node:20-alpine AS deps +WORKDIR /app +ENV NODE_ENV=production +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev=false + + +FROM node:20-alpine AS builder +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + + +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +# Install bd (beads) CLI so the Dolt client can locate it if ever needed. +RUN apk add --no-cache curl ca-certificates \ + && addgroup -S nextjs -g 1001 \ + && adduser -S nextjs -u 1001 -G nextjs + +COPY --from=builder --chown=nextjs:nextjs /app/public ./public +COPY --from=builder --chown=nextjs:nextjs /app/.next ./.next +COPY --from=builder --chown=nextjs:nextjs /app/node_modules ./node_modules +COPY --from=builder --chown=nextjs:nextjs /app/package.json ./package.json +COPY --from=builder --chown=nextjs:nextjs /app/next.config.ts ./next.config.ts + +USER nextjs +EXPOSE 3000 +CMD ["npm", "start"]