Skip to content

feat(api): add gunicorn with uvicorn workers for production deployment#69

Merged
itisnotyourenv merged 1 commit intoitisnotyourenv:mainfrom
arynyklas:src-68-gunicorn-with-uvicorn-workers
Feb 20, 2026
Merged

feat(api): add gunicorn with uvicorn workers for production deployment#69
itisnotyourenv merged 1 commit intoitisnotyourenv:mainfrom
arynyklas:src-68-gunicorn-with-uvicorn-workers

Conversation

@arynyklas
Copy link
Copy Markdown
Contributor

Add gunicorn as the WSGI server with uvicorn workers for production API deployment. This provides better performance, worker process management, and graceful restarts compared to running uvicorn directly.

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Configuration change

Changes Summary

  • Add gunicorn dependency to pyproject.toml with uvicorn worker class support
  • Create entrypoint-api.sh script for containerized gunicorn startup with configurable worker count
  • Add API_WORKERS environment variable (defaults to 4) for controlling worker processes
  • Update Dockerfile.api to use the new entrypoint script and install without dev dependencies
  • Update justfile commands to use uv run consistently across all tasks
  • Update .github/README.md with correct uv run prefixes for all commands
  • Remove deprecated version field from docker-compose.prod.yml
  • Set PYTHONUNBUFFERED=1 in Dockerfile for proper logging in containers
  • Add shell configuration to justfile for cross-platform compatibility

Testing

  • API container builds and starts successfully with gunicorn
  • Worker processes spawn correctly based on API_WORKERS setting
  • Just commands work correctly with uv run prefix
  • Tests pass locally with my changes
  • New and existing unit tests pass locally with my changes

Code Quality

  • I have performed a self-review of my own code
  • My changes generate no new warnings
  • Pre-commit hooks pass (run pre-commit run --all-files)

Related Issues

Related to production deployment improvements.

Additional Notes

The entrypoint script provides sensible defaults:

  • Uses API_WORKERS env var (defaults to 4 workers)
  • Enables access and error logging to stdout/stderr
  • Uses --preload for faster worker startup and memory efficiency
  • Uses uvicorn.workers.UvicornWorker for ASGI compatibility

Migration: Update .env file to include API_WORKERS=4 (or desired count) for production deployments.


Checklist for Reviewers:

  • Code follows project conventions and style guidelines
  • Changes are well-tested with appropriate test coverage
  • Documentation is updated if necessary
  • Security implications have been considered
  • Performance implications have been considered

🤖 Generated with Claude Code

- Add gunicorn as a dependency with uvicorn worker class support
- Create entrypoint-api.sh script for configurable worker count
- Add API_WORKERS env variable to control worker processes
- Update Dockerfile.api to use gunicorn entrypoint in production
- Ensure justfile commands use uv run consistently
- Remove deprecated version field from docker-compose.prod.yml
- Add PYTHONUNBUFFERED=1 for proper logging in containers
- Update README with uv run prefixes for all commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@arynyklas arynyklas changed the title feat(api): add gunicorn with uvicorn workers for production deployment feat(api): add gunicorn with uvicorn workers for production deployment (#68) Feb 17, 2026
@arynyklas arynyklas changed the title feat(api): add gunicorn with uvicorn workers for production deployment (#68) feat(api): add gunicorn with uvicorn workers for production deployment Feb 17, 2026
@itisnotyourenv
Copy link
Copy Markdown
Owner

PR #69 Review: feat(api): add gunicorn with uvicorn workers for production deployment

Good direction — gunicorn + UvicornWorker is the standard ASGI production pattern. The housekeeping (removing deprecated version fields, PYTHONUNBUFFERED=1, uv run prefixes, trailing newlines) is all welcome. However, there are critical issues that will prevent the API from starting in production.


🔴 CRITICAL — Must Fix Before Merge

1. Gunicorn factory pattern: missing parentheses → API won't start

entrypoint-api.sh:9

exec uv run gunicorn src.presentation.api.app:create_app \

Gunicorn does not have a --factory flag like uvicorn. It imports create_app as an attribute and tries to use the function object as the ASGI app — it never calls it. The app is never constructed, no routes are registered, no DI container is created. Gunicorn will log Listening at: http://0.0.0.0:8000 (appearing healthy), but every HTTP request will fail.

Fix:

exec uv run gunicorn "src.presentation.api.app:create_app()" \

Quotes are required because () is special in shell.

2. --preload + async factory = event loop issues after fork

entrypoint-api.sh:15

--preload loads the app in the master process before forking workers. create_app() initializes the Dishka async DI container (with asyncpg pools). After fork(), each worker gets a different event loop, but the pre-loaded app retains references to the master's loop. This causes subtle async failures in production.

Fix: Remove --preload, or verify that all async initialization is fork-safe (unlikely with asyncpg connection pools and Dishka).


🟠 HIGH — Should Fix

3. --no-dev in Dockerfile achieves nothing

Dockerfile.api:8 + pyproject.toml

The [project.optional-dependencies] dev group only contains mypy. Meanwhile, pytest, pytest-*, factory-boy, ruff, pre-commit, ty are all in main dependencies. So --no-dev excludes only mypy — the production image still ships every test framework, linter, and dev tool.

Suggestion: Move test/dev tools to a dev dependency group in pyproject.toml so --no-dev actually works. (This is pre-existing, but the PR explicitly adds --no-dev implying a production optimization.)

4. Justfile test recipe: cleanup skipped on test failure

justfile:54-57

test:
    docker compose -f docker-compose-test.yml up -d
    uv run pytest -n auto -ss -vv --maxfail=1
    docker compose -f docker-compose-test.yml down -v

If pytest fails, just stops — docker compose down -v never runs. Test containers and volumes leak silently. Pre-existing bug, but PR touches these lines.

Fix:

test:
    #!/usr/bin/env bash
    set -eu
    docker compose -f docker-compose-test.yml up -d
    trap 'docker compose -f docker-compose-test.yml down -v' EXIT
    uv run pytest -n auto -ss -vv --maxfail=1

5. README missing --factory flag

.github/README.md:58

uv run uvicorn src.presentation.api.app:create_app --host 0.0.0.0 --port 8080

Missing --factory. Without it, uvicorn uses the function object as the app (same problem as issue #1). The justfile correctly uses --factory but the README doesn't match.


🟡 MEDIUM — Suggestions

6. No USER instruction in Dockerfile — container runs as root

No USER directive means gunicorn and all workers run as root. Pre-existing, but since this PR is about production deployment, good time to add:

RUN adduser --disabled-password --gecos '' appuser
USER appuser

7. No healthcheck on API container in docker-compose.prod.yml

The app has a /health/ endpoint, but the compose file has no healthcheck. Combined with issue #1, Docker reports a broken API as "running." Add:

healthcheck:
  test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')\""]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 15s

8. No input validation for API_WORKERS

If set to a non-numeric value, gunicorn crashes with a raw argparse error and no mention of API_WORKERS. Consider adding validation in the entrypoint.

9. docker-compose.prod.ymlAPI_WORKERS without default

API_WORKERS: ${API_WORKERS}

If unset on host, passes empty string. The entrypoint handles it via :-4, but cleaner to also default in compose: ${API_WORKERS:-4}.

10. Redundant COPY in Dockerfile

COPY . .                                    # already copies entrypoint-api.sh to /app/
COPY entrypoint-api.sh /entrypoint-api.sh   # copies again to /

Either reference /app/entrypoint-api.sh or move the entrypoint COPY before COPY . . for better layer caching.

11. Missing .dockerignore

COPY . . includes .git, tests/, .coverage*, docs/, etc. A .dockerignore would reduce image size and build time.

12. Missing trailing newline in justfile

The PR fixed trailing newlines in .pre-commit-config.yaml and .env.example but the justfile still lacks one.

13. No pre-flight config check

If config-prod.yaml doesn't exist on host, Docker creates /app/config.yaml as an empty directory. Gunicorn crashes with a raw Python traceback. A simple [ -f /app/config.yaml ] check in the entrypoint would give an actionable error.


✅ What's Good

  • Gunicorn + UvicornWorker is the right production architecture
  • PYTHONUNBUFFERED=1 for proper container logging
  • Removing deprecated version fields from all compose files
  • Consistent uv run prefix across justfile and README
  • Configurable API_WORKERS via environment variable
  • set -e in entrypoint script
  • Clean, well-organized PR description

Verdict: Request Changes

Issue #1 (missing factory parentheses) is a deployment-breaking bug — the production API will silently fail on every request. Issue #2 (--preload with async factory) will cause subtle runtime failures. These must be fixed before merge. The remaining items are improvements worth considering.

🤖 Generated with Claude Code

@itisnotyourenv itisnotyourenv merged commit 3b7f3ed into itisnotyourenv:main Feb 20, 2026
7 of 9 checks passed
@arynyklas arynyklas deleted the src-68-gunicorn-with-uvicorn-workers branch March 24, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants