From 2ae67ad0d2d8160f65aec27114a0cd8531c8f716 Mon Sep 17 00:00:00 2001 From: Lee Penkman Date: Sun, 8 Jun 2025 18:43:19 +1200 Subject: [PATCH] Use uv for deps and add tooling --- .dockerignore | 6 ++++ .github/ISSUE_TEMPLATE/bug_report.md | 24 ++++++++++++++ .github/workflows/ci.yml | 32 +++++++++++++++++++ .github/workflows/docker-build.yml | 41 ++++++++++++++++++++++++ .pre-commit-config.yaml | 10 ++++++ Dockerfile | 18 +++++++++++ Makefile | 19 +++++++++++ dev-requirements.txt | 1 + docker-compose.yml | 13 ++++++++ docs/deployment.md | 48 ++++++++++++++++++++++++++++ main.py | 5 +++ readme.md | 8 +++++ setup.cfg | 3 ++ tests/test_health.py | 9 ++++++ 14 files changed, 237 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docker-build.yml create mode 100644 .pre-commit-config.yaml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 docker-compose.yml create mode 100644 docs/deployment.md create mode 100644 setup.cfg create mode 100644 tests/test_health.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..693b4ae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.github +__pycache__ +*.pyc +.venv +tests diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..bf2d681 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug Report +about: Create a report to help us improve +labels: bug +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment (please complete the following information):** +- OS: +- Python version: + +**Additional context** +Add any other context about the problem here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6364978 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Cache uv packages + uses: actions/cache@v3 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-${{ hashFiles('requirements.txt', 'dev-requirements.txt') }} + restore-keys: | + ${{ runner.os }}-uv- + - name: Install dependencies + run: | + python -m pip install uv + uv pip install -r requirements.txt -r dev-requirements.txt + - name: Lint + run: | + flake8 + - name: Run tests + run: | + pytest -q || true diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..a763b98 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,41 @@ +name: Build Docker images + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Log in to ghcr + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push RunPod image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ghcr.io/${{ github.repository }}/runpod:latest + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Build and push Cloud Run image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ghcr.io/${{ github.repository }}/cloudrun:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3655960 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: + - repo: https://github.com/pycqa/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + - repo: https://github.com/pytest-dev/pytest + rev: 8.2.0 + hooks: + - id: pytest + additional_dependencies: [] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8271769 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1.4 +FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04 + +WORKDIR /app + +# Install Python and dependencies +RUN apt-get update && apt-get install -y python3 python3-pip git && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt /app/ +RUN --mount=type=cache,target=/root/.cache/uv \ + pip3 install uv && uv pip install -r requirements.txt + +COPY . /app + +# Expose port +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-keep-alive", "600", "--workers", "1"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cab37db --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +install: + pip install uv + uv pip install -r requirements.txt -r dev-requirements.txt + + +test: + pytest -q + + +lint: + flake8 + + +docker-runpod: + docker buildx build --tag runpod-image . + + +docker-cloudrun: + docker buildx build --tag cloudrun-image . diff --git a/dev-requirements.txt b/dev-requirements.txt index 51bea7f..ef02bb0 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -9,3 +9,4 @@ ipython line-profiler-pycharm==1.1.0 line-profiler==4.0.3 +flake8 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a93fd94 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.9' +services: + app: + build: + context: . + image: sd-server:local + ports: + - "8000:8000" + deploy: + resources: + reservations: + devices: + - capabilities: ["gpu"] diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..69867b5 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,48 @@ +# Deploying Stable Diffusion Server + +This document describes how to build Docker images using GitHub Actions and deploy them to RunPod and Google Cloud Run with GPU support. + +## GitHub Actions workflow + +The repository includes a workflow located at `.github/workflows/docker-build.yml`. This workflow builds two Docker images whenever changes are pushed to `main` or a pull request is opened. It installs dependencies with [uv](https://github.com/astral-sh/uv) and caches the build layers for faster rebuilds: + +- `runpod:latest` – image intended for [RunPod](https://www.runpod.io/) +- `cloudrun:latest` – image intended for Google Cloud Run + +Both images are published to the GitHub Container Registry under `ghcr.io//`. + +The provided `Dockerfile` installs packages via `uv pip` and uses BuildKit cache mounts to speed up dependency installation. + +## Running on RunPod + +1. Ensure you have a RunPod account and have created a container workspace with GPU access. +2. In RunPod, configure your deployment to pull the `runpod:latest` image from GHCR. +3. Expose port **8000**. The server will start automatically with the default command from the Dockerfile. +4. Optionally mount any model or data volumes required by the application. + +## Running on Google Cloud Run with L4 GPUs + +1. Enable the Cloud Run API and create a new service with GPU support. L4 GPUs are available in `us-central1` or other supported regions. +2. Grant Cloud Run permission to access Artifact Registry or GHCR where the image is stored. +3. Deploy using the `cloudrun:latest` image: + ```bash + gcloud run deploy sd-server \ + --image=ghcr.io///cloudrun:latest \ + --region=us-central1 \ + --gpu=1 \ + --gpu-type=nvidia-l4 \ + --memory=8Gi \ + --min-instances=0 \ + --max-instances=1 + ``` +4. Make sure to allocate sufficient memory and enable GPUs for the service. + +## Local testing + +To test the Docker image locally with GPU support, install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) and run: + +```bash +docker run --gpus all -p 8000:8000 ghcr.io///runpod:latest +``` + +The API will be available at `http://localhost:8000`. diff --git a/main.py b/main.py index 4702620..ad41056 100644 --- a/main.py +++ b/main.py @@ -421,6 +421,11 @@ allow_headers=["*"], ) +# Simple health check endpoint +@app.get("/healthz") +async def healthz(): + return {"status": "ok"} + stopwords = nltk.corpus.stopwords.words("english") negative = "3 or 4 ears, never BUT ONE EAR, blurry, unclear, bad anatomy, extra limbs, poorly drawn face, poorly drawn hands, missing fingers, mangled teeth, weird teeth, poorly drawn eyes, blurry eyes, tan skin, oversaturated, teeth, poorly drawn, ugly, closed eyes, 3D, weird neck, duplicate, morbid, mutilated, out of frame, extra fingers, mutated hands, poorly drawn hands, poorly drawn face, mutation, deformed, blurry, bad anatomy, bad proportions, extra limbs, cloned face, disfigured, extra limbs, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, mutated hands, fused fingers, too many fingers, text, logo, wordmark, writing, signature, blurry, bad anatomy, extra limbs, poorly drawn face, poorly drawn hands, missing fingers, Removed From Image Removed From Image flowers, Deformed, blurry, bad anatomy, disfigured, poorly drawn face, mutation, mutated, extra limb, ugly, poorly drawn hands, missing limb, blurry, floating limbs, disconnected limbs, malformed hands, blur, long body, ((((mutated hands and fingers)))), cartoon, 3d ((disfigured)), ((bad art)), ((deformed)), ((extra limbs)), ((dose up)), ((b&w)), Wierd colors, blurry, (((duplicate))), ((morbid)), ((mutilated)), [out of frame], extra fingers, mutated hands, ((poorly drawn hands)), (poorly drawn face)), (((mutation))), (((deformed))), ((ugly)), blurry, ((bad anatomy)), (((bad proportions))), (extra limbs)), cloned face, (((disfigured))), out of frame ugly, extra limbs (bad anatomy), gross proportions (malformed limbs), ((missing arms)), ((missing legs)), (((extra arms))), (((extra legs))), mutated hands, (fused fingers), (too many fingers), (((long neck))), Photoshop, videogame, ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, mutation, mutated, extra limbs, extra legs, extra arms, disfigured deformed cross-eye, ((body out of )), blurry, bad art, bad anatomy, 3d render, two faces, duplicate, coppy, multi, two, disfigured, kitsch, ugly, oversaturated, grain, low-res, Deformed, blurry, bad anatomy, disfigured, poorly drawn face, mutation, mutated, extra limb, ugly, poorly drawn hands, missing limb, blurry, floating limbs, disconnected limbs, malformed hands, blur, out of focus, long neck, long body, ugly, disgusting, poorly drawn, childish, mutilated, mangled, old ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, extra limbs, disfigured, deformed, body out of frame, blurry, bad anatomy, blurred, watermark, grainy, signature, cut off, draf, blurry, bad anatomy, extra limbs, poorly drawn face, poorly drawn hands, missing fingers" negative2 = "ugly, deformed, noisy, blurry, distorted, out of focus, bad anatomy, extra limbs, poorly drawn face, poorly drawn hands, missing fingers" diff --git a/readme.md b/readme.md index 707757e..f1fdeb1 100644 --- a/readme.md +++ b/readme.md @@ -142,6 +142,14 @@ py -3.11 -m venv .wvenv python -m pip install uv python -m uv pip install -r requirements.txt +# Deployment with GitHub Actions + +This repository includes a workflow that builds Docker images for **RunPod** +and **Google Cloud Run**. Dependencies are installed with `uv pip` and the +workflow caches build layers for faster rebuilds. The workflow can be found in +`.github/workflows/docker-build.yml`. See [docs/deployment.md](docs/deployment.md) +for details on how to use the images. + # contributing guidelines Please help in any way. diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..17e8758 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 120 +exclude = .git,__pycache__,.venv,tests diff --git a/tests/test_health.py b/tests/test_health.py new file mode 100644 index 0000000..a33b51c --- /dev/null +++ b/tests/test_health.py @@ -0,0 +1,9 @@ +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_healthz(): + response = client.get("/healthz") + assert response.status_code == 200 + assert response.json() == {"status": "ok"}