diff --git a/app/Dockerfile-aws b/app/Dockerfile-aws index af4607d4..660238d0 100644 --- a/app/Dockerfile-aws +++ b/app/Dockerfile-aws @@ -1,15 +1,38 @@ -# pull official base image -FROM python:3.10-alpine +# Stage 1: build dependencies and collect static files +FROM python:3.10-alpine AS builder -# set work directory WORKDIR /usr/src/app -# set environment variables ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 +ENV PYTHONPYCACHEPREFIX=/root/.cache/pycache/ +ENV DJANGO_SETTINGS_MODULE=peopledepot.settings_aws + +COPY ./requirements-aws.txt . +# hadolint ignore=DL3042 +RUN \ + --mount=type=cache,target=/root/.cache \ + pip install uv==0.1.15 \ + && uv pip install --system -r requirements-aws.txt + +COPY . . +RUN python manage.py collectstatic --noinput + + +# Stage 2: runtime image +FROM python:3.10-alpine AS runtime + +WORKDIR /usr/src/app + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PYTHONPYCACHEPREFIX=/root/.cache/pycache/ +ENV DJANGO_SETTINGS_MODULE=peopledepot.settings_aws # install system dependencies RUN \ + --mount=type=cache,target=/var/cache/apk \ + --mount=type=cache,target=/etc/apk/cache \ apk add \ 'graphviz=~12' @@ -17,28 +40,17 @@ RUN \ COPY Roboto-Regular.ttf /root/.fonts/ RUN fc-cache -f -# install dependencies -COPY ./requirements.txt . -# hadolint ignore=DL3042 -RUN \ - pip install uv==0.1.15 \ - && uv pip install --system -r requirements.txt - +# copy installed Python packages and executables from builder +COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages +COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/gunicorn -# copy project -COPY . . +# copy application code and collected static files from builder +COPY --from=builder /usr/src/app . -# copy entrypoint-aws.sh -COPY ./entrypoint-aws.sh . +# configure entrypoint RUN sed -i 's/\r$//g' /usr/src/app/entrypoint-aws.sh \ && chmod +x /usr/src/app/entrypoint-aws.sh -# Expose the Django port EXPOSE 8000 - -# run entrypoint.sh ENTRYPOINT ["/usr/src/app/entrypoint-aws.sh"] - -# Run Django’s development server -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/app/entrypoint-aws.sh b/app/entrypoint-aws.sh index 9db9e617..df9d1e17 100755 --- a/app/entrypoint-aws.sh +++ b/app/entrypoint-aws.sh @@ -1,5 +1,5 @@ #!/bin/sh -python manage.py migrate +python manage.py migrate --noinput -exec "$@" +exec gunicorn peopledepot.wsgi:application --bind 0.0.0.0:8000 diff --git a/app/peopledepot/settings.py b/app/peopledepot/settings.py index 27fb1879..d41a2f86 100644 --- a/app/peopledepot/settings.py +++ b/app/peopledepot/settings.py @@ -34,7 +34,7 @@ # 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each. # For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]' -ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(",") +ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",") SECURE_HSTS_SECONDS = int(os.getenv("SECURE_HSTS_SECONDS", "0")) diff --git a/app/peopledepot/settings_aws.py b/app/peopledepot/settings_aws.py new file mode 100644 index 00000000..85852ace --- /dev/null +++ b/app/peopledepot/settings_aws.py @@ -0,0 +1,37 @@ +from .settings import * # noqa: F401, F403 + +ROOT_URLCONF = "peopledepot.urls_aws" + +STATIC_ROOT = BASE_DIR / "staticfiles" +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + # 3rd party + "django_extensions", + "rest_framework", + "phonenumber_field", + "timezone_field", + # Local + "core", + "data", +] + +REST_FRAMEWORK = { + "DEFAULT_PERMISSION_CLASSES": ("core.api.permissions.DenyAny",), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_jwt.authentication.JSONWebTokenAuthentication", + ), +} diff --git a/app/peopledepot/urls_aws.py b/app/peopledepot/urls_aws.py new file mode 100644 index 00000000..e63d49d2 --- /dev/null +++ b/app/peopledepot/urls_aws.py @@ -0,0 +1,15 @@ +from django.contrib import admin +from django.http import HttpResponse +from django.urls import include +from django.urls import path + + +def index(request): + return HttpResponse("You're at the peopledepot index.") + + +urlpatterns = [ + path("", index), + path("admin/", admin.site.urls), + path("api/v1/", include("core.api.urls")), +] diff --git a/app/requirements-aws.in b/app/requirements-aws.in new file mode 100644 index 00000000..efc5ac33 --- /dev/null +++ b/app/requirements-aws.in @@ -0,0 +1,11 @@ +django~=4.2.27 +django-extensions +django-phonenumber-field[phonenumbers] +django-timezone-field +djangorestframework +drf-jwt +drf-spectacular +gunicorn +psycopg2-binary +tzdata +whitenoise diff --git a/app/requirements-aws.txt b/app/requirements-aws.txt new file mode 100644 index 00000000..62bfad3a --- /dev/null +++ b/app/requirements-aws.txt @@ -0,0 +1,76 @@ +asgiref==3.11.1 + # via django +attrs==26.1.0 + # via + # jsonschema + # referencing +cffi==2.0.0 + # via cryptography +cryptography==48.0.1 + # via pyjwt +django==4.2.30 + # via + # -r requirements-aws.in + # django-extensions + # django-phonenumber-field + # django-timezone-field + # djangorestframework + # drf-jwt + # drf-spectacular +django-extensions==4.1 + # via -r requirements-aws.in +django-phonenumber-field==8.4.0 + # via -r requirements-aws.in +django-timezone-field==7.2.2 + # via -r requirements-aws.in +djangorestframework==3.17.1 + # via + # -r requirements-aws.in + # drf-jwt + # drf-spectacular +drf-jwt==1.19.2 + # via -r requirements-aws.in +drf-spectacular==0.29.0 + # via -r requirements-aws.in +gunicorn==26.0.0 + # via -r requirements-aws.in +inflection==0.5.1 + # via drf-spectacular +jsonschema==4.26.0 + # via drf-spectacular +jsonschema-specifications==2025.9.1 + # via jsonschema +packaging==26.2 + # via gunicorn +phonenumbers==9.0.32 + # via django-phonenumber-field +psycopg2-binary==2.9.12 + # via -r requirements-aws.in +pycparser==3.0 + # via cffi +pyjwt==2.13.0 + # via drf-jwt +pyyaml==6.0.3 + # via drf-spectacular +referencing==0.37.0 + # via + # jsonschema + # jsonschema-specifications +rpds-py==0.30.0 + # via + # jsonschema + # referencing +sqlparse==0.5.5 + # via django +typing-extensions==4.15.0 + # via + # asgiref + # cryptography + # pyjwt + # referencing +tzdata==2026.2 + # via -r requirements-aws.in +uritemplate==4.2.0 + # via drf-spectacular +whitenoise==6.12.0 + # via -r requirements-aws.in diff --git a/docs/architecture/project_structure.md b/docs/architecture/project_structure.md index 39e82098..5a213b95 100644 --- a/docs/architecture/project_structure.md +++ b/docs/architecture/project_structure.md @@ -37,7 +37,9 @@ app/ ├── manage.py # (7)! ├── requirements.in # (8)! ├── requirements.txt # (9)! -└── setup.cfg # (10)! +├── requirements-aws.in # (10)! +├── requirements-aws.txt # (11)! +└── setup.cfg # (12)! ``` 1. The core app in django. This app contains the API and models. See [Core App](#core-app) below for details. @@ -47,8 +49,10 @@ app/ 1. Dockerfile used to build the Docker image. 1. Entrypoint script called by the Docker image. 1. Django manage.py script. In nearly all cases, there's no good reason to change this. Just leave it alone. -1. Requirements.in file used by `uv pip compile`. See the [uv tool](uv.md) for details. -1. Requirements.txt file generated by `uv pip install`. Do not modify this file. Edit the `requirements.in` file instead. See the [uv tool](uv.md) for details. +1. Development requirements source file used by `uv pip compile`. Contains all dependencies including dev/test packages. See the [uv tool](uv.md) for details. +1. Development requirements file generated from `requirements.in`. Do not modify this file directly. Edit `requirements.in` instead. See the [uv tool](uv.md) for details. +1. AWS/production requirements source file. Contains only production dependencies — no dev or test packages. +1. AWS/production requirements file generated from `requirements-aws.in`. Do not modify this file directly. Edit `requirements-aws.in` instead. 1. Config file for development support tools such as `flake8` and `pytest`. `flake8` is the only tool that doesn't support `pyproject.toml` yet, which is why we have this file. #### Core App diff --git a/scripts/update-dependencies.sh b/scripts/update-dependencies.sh index 175714eb..5d9707b1 100755 --- a/scripts/update-dependencies.sh +++ b/scripts/update-dependencies.sh @@ -5,3 +5,6 @@ set -x # generate requirements.txt with the latest package versions docker compose exec web uv pip compile -o requirements.txt requirements.in --no-header --upgrade + +# generate requirements-aws.txt with the latest package versions +docker compose exec web uv pip compile -o requirements-aws.txt requirements-aws.in --no-header --upgrade --python-version 3.10