From 469110d6477b5c80de6da9661b18cff093a08e78 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Fri, 23 Jan 2026 18:57:21 +0100 Subject: [PATCH] feat: add official Docker image with GHCR publishing workflow - Add docker/Dockerfile with non-root user and configurable environment - Add GitHub Actions workflow to build multi-platform images (amd64/arm64) - Publish to ghcr.io/benoitc/gunicorn on version tags - Update documentation with official image usage examples --- .github/workflows/docker-publish.yml | 57 +++++++++++++++++++++ docker/.dockerignore | 8 +++ docker/Dockerfile | 31 ++++++++++++ docker/docker-entrypoint.sh | 33 ++++++++++++ docs/content/guides/docker.md | 75 ++++++++++++++++++++++++++++ docs/content/install.md | 4 +- 6 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/docker-publish.yml create mode 100644 docker/.dockerignore create mode 100644 docker/Dockerfile create mode 100644 docker/docker-entrypoint.sh diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..8054947d --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,57 @@ +name: Docker Publish +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 00000000..32f0329d --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,8 @@ +.git +.github +__pycache__ +*.pyc +.pytest_cache +.tox +docs +tests diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..1b27c173 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.12-slim + +LABEL org.opencontainers.image.source=https://github.com/benoitc/gunicorn +LABEL org.opencontainers.image.description="Gunicorn Python WSGI HTTP Server" +LABEL org.opencontainers.image.licenses=MIT + +# Create non-root user +RUN useradd --create-home --shell /bin/bash gunicorn + +WORKDIR /app + +# Install gunicorn +RUN pip install --no-cache-dir gunicorn + +# Copy entrypoint script +COPY docker/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Configuration via environment: +# GUNICORN_BIND - full bind address (default: [::]:8000, IPv4+IPv6) +# GUNICORN_HOST - bind host (default: [::]) +# GUNICORN_PORT - bind port (default: 8000) +# GUNICORN_WORKERS - number of workers (default: number of CPUs) +# GUNICORN_ARGS - additional arguments (e.g., "--timeout 120") + +USER gunicorn + +EXPOSE 8000 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["--help"] diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100644 index 00000000..2ac91149 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +# Allow running other commands (e.g., bash for debugging) +if [ "${1:0:1}" = '-' ] || [ -z "${1##*:*}" ]; then + # First arg is a flag or contains ':' (app:callable), run gunicorn + + # Build bind address from GUNICORN_HOST and GUNICORN_PORT, or use GUNICORN_BIND + # Default: listen on both IPv4 and IPv6 + PORT="${GUNICORN_PORT:-8000}" + BIND="${GUNICORN_BIND:-${GUNICORN_HOST:-[::]}:${PORT}}" + + # Add bind if not specified in args or GUNICORN_ARGS + if [[ ! " $* $GUNICORN_ARGS " =~ " --bind " ]] && [[ ! " $* $GUNICORN_ARGS " =~ " -b " ]] && [[ ! "$* $GUNICORN_ARGS" =~ --bind= ]] && [[ ! "$* $GUNICORN_ARGS" =~ -b= ]]; then + set -- --bind "$BIND" "$@" + fi + + # Add workers if not specified - default to number of CPUs + if [[ ! " $* $GUNICORN_ARGS " =~ " --workers " ]] && [[ ! " $* $GUNICORN_ARGS " =~ " -w " ]] && [[ ! "$* $GUNICORN_ARGS" =~ --workers= ]] && [[ ! "$* $GUNICORN_ARGS" =~ -w= ]]; then + WORKERS="${GUNICORN_WORKERS:-$(nproc)}" + set -- --workers "$WORKERS" "$@" + fi + + # Append GUNICORN_ARGS if set + if [ -n "$GUNICORN_ARGS" ]; then + exec gunicorn $GUNICORN_ARGS "$@" + fi + + exec gunicorn "$@" +fi + +# Otherwise, run the command as-is (e.g., bash, sh, python) +exec "$@" diff --git a/docs/content/guides/docker.md b/docs/content/guides/docker.md index 036b38b1..0c9d49eb 100644 --- a/docs/content/guides/docker.md +++ b/docs/content/guides/docker.md @@ -4,6 +4,81 @@ Running Gunicorn in Docker containers is the most common deployment pattern for modern Python applications. This guide covers best practices for containerizing Gunicorn applications. +## Official Docker Image + +Gunicorn provides an official Docker image on GitHub Container Registry: + +```bash +docker pull ghcr.io/benoitc/gunicorn:latest +``` + +### Quick Start + +Mount your application directory and run: + +```bash +docker run -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app +``` + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `GUNICORN_BIND` | Full bind address | `[::]:8000` (IPv4+IPv6) | +| `GUNICORN_HOST` | Bind host | `[::]` | +| `GUNICORN_PORT` | Bind port | `8000` | +| `GUNICORN_WORKERS` | Number of workers | Number of CPUs | +| `GUNICORN_ARGS` | Additional arguments | (none) | + +### With Configuration + +```bash +docker run -p 9000:9000 -v $(pwd):/app \ + -e GUNICORN_PORT=9000 \ + -e GUNICORN_WORKERS=4 \ + -e GUNICORN_ARGS="--timeout 120 --access-logfile -" \ + ghcr.io/benoitc/gunicorn app:app +``` + +### As Base Image (Recommended for Production) + +```dockerfile +FROM ghcr.io/benoitc/gunicorn:24.1.0 + +# Install app dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application +COPY --chown=gunicorn:gunicorn . . + +CMD ["myapp:app", "--workers", "4"] +``` + +### With Docker Compose + +```yaml +services: + web: + image: ghcr.io/benoitc/gunicorn:latest + ports: + - "8000:8000" + volumes: + - ./app:/app + command: ["myapp:app", "--workers", "4"] +``` + +### Available Tags + +- `ghcr.io/benoitc/gunicorn:latest` - Latest release +- `ghcr.io/benoitc/gunicorn:24.1.0` - Specific version +- `ghcr.io/benoitc/gunicorn:24.1` - Minor version +- `ghcr.io/benoitc/gunicorn:24` - Major version + +## Building Your Own Image + +For more control, build a custom image using the patterns below. + ## Basic Dockerfile ```dockerfile diff --git a/docs/content/install.md b/docs/content/install.md index 27c73fed..95b68df9 100644 --- a/docs/content/install.md +++ b/docs/content/install.md @@ -20,8 +20,8 @@ === "Docker" ```bash - docker run -p 8000:8000 -v $(pwd):/app -w /app \ - python:3.12-slim sh -c "pip install gunicorn && gunicorn app:app" + docker pull ghcr.io/benoitc/gunicorn:latest + docker run -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app ``` See the [Docker guide](guides/docker.md) for production configurations.