diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8fa99a6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +**/venv +**/node_modules +**/__pycache__ +*.env.local +**/.env.local +**/tests +docs/ +.github/ +.git/ +*.md diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..8787514 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,80 @@ +name: docker + +on: + push: + branches: ["**"] + tags: ["v*"] + pull_request: + workflow_call: + +permissions: + contents: read + packages: write + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} + + - name: Build (load locally, no push) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + load: true + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Smoke test + run: | + IMAGE=$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -n1) + echo "Smoke-testing $IMAGE" + docker run -d --name smoke -p 8000:8000 \ + -e AGORA_APP_ID=0123456789abcdef0123456789abcdef \ + -e AGORA_APP_CERTIFICATE=fedcba9876543210fedcba9876543210 \ + "$IMAGE" + set +e + fail=0 + for url in http://localhost:8000/get_config; do + ok="" + for i in $(seq 1 40); do + if curl -fsS "$url" -o /dev/null; then ok=1; echo "OK $url"; break; fi + sleep 1 + done + if [ -z "$ok" ]; then echo "FAIL $url"; fail=1; fi + done + if [ "$fail" -ne 0 ]; then docker logs smoke; fi + docker rm -f smoke + exit $fail + + - name: Log in to GHCR + if: startsWith(github.ref, 'refs/tags/') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push tags + if: startsWith(github.ref, 'refs/tags/') + run: | + printf '%s\n' "${{ steps.meta.outputs.tags }}" | while read -r tag; do + [ -n "$tag" ] && docker push "$tag" + done diff --git a/.gitignore b/.gitignore index 1a07908..bb0325d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -/.vscode \ No newline at end of file +/.vscode +# Superpowers workflow docs (specs/plans) — local only, not tracked +docs/superpowers/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4e70769 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 +FROM python:3.12-slim-bookworm AS runtime + +# Run as a non-root user (created before any COPY so --chown can reference it). +RUN useradd --create-home --uid 10001 app +WORKDIR /app + +# Python dependencies for the FastAPI backend (installed as root into the +# system site-packages, world-readable for the app user at runtime). +COPY server/requirements.txt /tmp/server-req.txt +RUN pip install --no-cache-dir -r /tmp/server-req.txt + +# Backend source, owned by the runtime user. +COPY --chown=app:app server/src /app/server/src + +# Drop privileges for the running process. +USER app + +# server.py reads $PORT (default 8000) and binds 0.0.0.0. +EXPOSE 8000 +CMD ["python", "/app/server/src/server.py"]