├── .dockerignore ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── deployment ├── Dockerfile └── scripts │ ├── backend │ ├── start.sh │ └── wait-for-it.sh │ ├── celery │ ├── start-beat.sh │ └── start-worker.sh │ └── nginx │ └── nginx.conf ├── docker-compose.prod.yml ├── docker-compose.yml ├── env.example ├── pyproject.toml ├── scripts ├── format.sh ├── lint.sh └── test.sh └── src ├── manage.py ├── project_name ├── __init__.py ├── asgi.py ├── celery.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── dev.py │ ├── prod.py │ └── test.py ├── urls.py └── wsgi.py ├── requirements.dev.txt ├── requirements.txt ├── test_app ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tasks.py ├── urls.py └── views.py └── tests ├── __init__.py ├── conftest.py └── test_app_1 ├── __init__.py └── test_example.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore files generated by text editors 2 | *~ 3 | *.swp 4 | 5 | # Ignore version control files and directories 6 | .git 7 | .gitignore 8 | svn 9 | CVS 10 | 11 | # Ignore Python bytecode files and cache directories 12 | *.pyc 13 | __pycache__ 14 | 15 | # Ignore local development settings files 16 | .env 17 | .local 18 | .venv 19 | 20 | # Ignore Docker build context directories 21 | .dockerignore 22 | .docker 23 | 24 | # Ignore node_modules directory 25 | node_modules 26 | 27 | # Ignore Celery beat schedule file 28 | celerybeat-schedule 29 | 30 | # Ignore Celery worker pid file 31 | celeryd.pid 32 | 33 | # Ignore Celery task result files 34 | celery-task-meta-* 35 | 36 | # Ignore Django static files 37 | /static 38 | 39 | # Ignore Django media files 40 | /media 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Application Directories 2 | src/staticfiles/ 3 | src/mediafiles/ 4 | src/static/ 5 | src/media 6 | 7 | test-compose.yml 8 | 9 | .venv 10 | .idea 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | *.so 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | *.manifest 34 | *.spec 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | *.py,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | cover/ 50 | *.mo 51 | *.pot 52 | *.log 53 | local_settings.py 54 | db.sqlite3 55 | db.sqlite3-journal 56 | instance/ 57 | .webassets-cache 58 | .scrapy 59 | docs/_build/ 60 | .pybuilder/ 61 | target/ 62 | .ipynb_checkpoints 63 | profile_default/ 64 | ipython_config.py 65 | __pypackages__/ 66 | celerybeat-schedule 67 | celerybeat.pid 68 | *.sage.py 69 | .env 70 | .env.prod 71 | env/ 72 | venv/ 73 | ENV/ 74 | env.bak/ 75 | venv.bak/ 76 | .spyderproject 77 | .spyproject 78 | .ropeproject 79 | /site 80 | .mypy_cache/ 81 | .dmypy.json 82 | dmypy.json 83 | .pyre/ 84 | .pytype/ 85 | cython_debug/ 86 | letsencrypt/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lirim Shala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | export $(shell sed 's/=.*//' .env) 3 | 4 | SHELL := /bin/sh 5 | PROJECTNAME ?= default_app_name 6 | APP_NAME := $(PROJECTNAME) 7 | BACKEND_APP_NAME := $(APP_NAME)-backend 8 | 9 | define HELP 10 | 11 | Manage $(PROJECTNAME). Usage: 12 | 13 | make lint Run linter 14 | make format Run formatter 15 | make test Run tests 16 | make super-user Create super user 17 | make make-migrations Make migrations 18 | make migrate Migrate 19 | make build-dev Build and run dev environment 20 | make stop-dev Stop dev environment 21 | make stop-prod Stop prod environment 22 | make build-prod Build and run prod environment 23 | make all Show help 24 | 25 | endef 26 | 27 | export HELP 28 | 29 | help: 30 | @echo "$$HELP" 31 | 32 | lint: 33 | @bash ./scripts/lint.sh 34 | 35 | format: 36 | @bash ./scripts/format.sh 37 | 38 | test: 39 | @bash ./scripts/test.sh 40 | 41 | super-user: 42 | docker exec -it $(BACKEND_APP_NAME) sh "-c" \ 43 | "python manage.py createsuperuser" 44 | 45 | make-migrations: 46 | docker exec -it $(BACKEND_APP_NAME) $(SHELL) "-c" \ 47 | "python manage.py makemigrations" 48 | 49 | migrate: 50 | docker exec -it $(BACKEND_APP_NAME) $(SHELL) "-c" \ 51 | "python manage.py migrate" 52 | 53 | build-dev: 54 | DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose -f docker-compose.yml up --build -d 55 | 56 | build-prod: 57 | DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose -f docker-compose.prod.yml up --build -d 58 | 59 | stop-dev: 60 | @docker-compose -f docker-compose.yml down 61 | 62 | stop-prod: 63 | @docker-compose -f docker-compose.prod.yml down 64 | 65 | all: help 66 | 67 | .PHONY: help lint format test super-user make-migrations migrate build-dev build-prod stop-dev stop-prod all 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Docker Quickstart 2 | 3 | This quickstart provides an easy way to initiate a Django project using Docker. It comes with pre-configured services including PostgreSQL, Redis, Celery (worker and beat), Nginx, and Traefik, ready to run a Django web application. Additionally, it provides a few handy shortcuts for easier development. 4 | 5 | --- 6 | 7 | ## Features 🚀 8 | 9 | - **Django** web application framework 10 | - **PostgreSQL** database 11 | - **Redis** in-memory data structure store 12 | - **Celery** worker and beat services for running background tasks asynchronously 13 | - **Nginx** web server for serving static and media files, and proxying requests to the Django application 14 | - **Traefik** reverse proxy for routing requests to the appropriate service and providing SSL termination 15 | 16 | ## Included Packages and Tools 🛠️ 17 | 18 | - **Pytest**: Testing framework 19 | - **Pytest Sugar**: A pytest plugin for a better look 20 | - **Pytest Django**: A pytest plugin providing useful tools for testing Django applications 21 | - **Coverage**: Test coverage tool 22 | - **Ruff**: Linter 23 | - **Black**: Code formatter 24 | 25 | ## Requirements 📋 26 | 27 | - Docker & Docker Compose - [Install and Use Docker](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04) 28 | - Python 3.10 or higher 29 | - Make (optional for shortcuts) 30 | 31 | --- 32 | 33 | ## Getting Started 🏁 34 | 35 | 1. **Clone the repository:** 36 | ```bash 37 | git clone https://github.com/godd0t/django-docker-quickstart.git 38 | ``` 39 | 40 | 2. **Change directory into the project:** 41 | ```bash 42 | cd django-docker-quickstart 43 | ``` 44 | 45 | 3. **Copy the `env.example` file to `.env` and update the values as needed:** 46 | 47 | - **For Linux/macOS:** 48 | ```bash 49 | cp env.example .env 50 | ``` 51 | - **For Windows (Command Prompt):** 52 | ```cmd 53 | Copy-Item -Path env.example -Destination .env 54 | ``` 55 | 56 | --- 57 | 58 | ## Initial Setup ⚙️ 59 | 60 | ### Development Prerequisites 61 | 62 | 1. **Create a virtual environment:** 63 | ```bash 64 | python -m venv venv 65 | ``` 66 | 67 | 2. **Activate the virtual environment:** 68 | ```bash 69 | source venv/bin/activate 70 | ``` 71 | 72 | 3. **(Optional) Install the development requirements specific to your IDE for enhanced functionality and support.** 73 | ```bash 74 | pip install -r src/requirements.dev.txt 75 | ``` 76 | 77 | 4. **Build the image and run the container:** 78 | 79 | - If buildkit is not enabled, enable it and build the image: 80 | ```bash 81 | DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose -f docker-compose.yml up --build -d 82 | ``` 83 | 84 | - If buildkit is enabled, build the image: 85 | ```bash 86 | docker-compose -f docker-compose.yml up --build -d 87 | ``` 88 | 89 | - Or, use the shortcut: 90 | ```bash 91 | make build-dev 92 | ``` 93 | 94 | You can now access the application at http://localhost:8000. The development environment allows for immediate reflection of code changes. 95 | 96 | ### Production Setup 97 | 98 | 1. **Build the image and run the container:** 99 | 100 | - If buildkit is not enabled, enable it and build the image: 101 | ```bash 102 | DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose -f docker-compose.prod.yml up --build -d 103 | ``` 104 | 105 | - If buildkit is enabled, build the image: 106 | ```bash 107 | docker-compose -f docker-compose.prod.yml up --build -d 108 | ``` 109 | - Or, use the shortcut: 110 | ```bash 111 | make build-prod 112 | ``` 113 | 114 | --- 115 | 116 | ## Shortcuts 🔑 117 | 118 | This project includes several shortcuts to streamline the development process: 119 | 120 | - **Create migrations:** 121 | ```bash 122 | make make-migrations 123 | ``` 124 | 125 | - **Run migrations:** 126 | ```bash 127 | make migrate 128 | ``` 129 | 130 | - **Run the linter:** 131 | ```bash 132 | make lint 133 | ``` 134 | 135 | - **Run the formatter:** 136 | ```bash 137 | make format 138 | ``` 139 | 140 | - **Run the tests:** 141 | ```bash 142 | make test 143 | ``` 144 | 145 | - **Create a super user:** 146 | ```bash 147 | make super-user 148 | ``` 149 | 150 | - **Build and run dev environment:** 151 | ```bash 152 | make build-dev 153 | ``` 154 | 155 | - **Build and run prod environment:** 156 | ```bash 157 | make build-prod 158 | ``` 159 | --- -------------------------------------------------------------------------------- /deployment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | ENV PYTHONDONTWRITEBYTECODE=1 3 | ENV PYTHONUNBUFFERED=1 4 | WORKDIR /usr/src/app 5 | 6 | RUN --mount=type=cache,target=/var/cache/apt \ 7 | apt-get update && \ 8 | apt-get install --no-install-recommends -y build-essential libpq-dev \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | 12 | COPY src/requirements.txt ./requirements.txt 13 | 14 | RUN --mount=type=cache,target=/root/.cache/pip \ 15 | pip install pip --upgrade \ 16 | && pip install -r requirements.txt 17 | 18 | 19 | COPY deployment/scripts /app/deployment/scripts 20 | 21 | RUN chmod -R +x /app/deployment/scripts/* 22 | 23 | COPY src/ ./ 24 | -------------------------------------------------------------------------------- /deployment/scripts/backend/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run migrations, collect static files and start server 4 | if [ "$APP_ENV" != "prod" ]; then 5 | python manage.py makemigrations --noinput 6 | python manage.py migrate --noinput 7 | python manage.py runserver "$APP_HOST":"$APP_PORT" 8 | else 9 | python manage.py makemigrations --noinput 10 | python manage.py migrate --noinput 11 | python manage.py collectstatic --noinput 12 | gunicorn "$APP_NAME".wsgi:application --bind "$APP_HOST":"$APP_PORT" --workers 3 --log-level=info 13 | fi 14 | -------------------------------------------------------------------------------- /deployment/scripts/backend/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | WAITFORIT_cmdname=${0##*/} 5 | 6 | echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 28 | echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 29 | else 30 | echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" 31 | fi 32 | WAITFORIT_start_ts=$(date +%s) 33 | while : 34 | do 35 | if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then 36 | nc -z $WAITFORIT_HOST $WAITFORIT_PORT 37 | WAITFORIT_result=$? 38 | else 39 | (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 40 | WAITFORIT_result=$? 41 | fi 42 | if [[ $WAITFORIT_result -eq 0 ]]; then 43 | WAITFORIT_end_ts=$(date +%s) 44 | echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" 45 | break 46 | fi 47 | sleep 1 48 | done 49 | return $WAITFORIT_result 50 | } 51 | 52 | wait_for_wrapper() 53 | { 54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 55 | if [[ $WAITFORIT_QUIET -eq 1 ]]; then 56 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 57 | else 58 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 59 | fi 60 | WAITFORIT_PID=$! 61 | trap "kill -INT -$WAITFORIT_PID" INT 62 | wait $WAITFORIT_PID 63 | WAITFORIT_RESULT=$? 64 | if [[ $WAITFORIT_RESULT -ne 0 ]]; then 65 | echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 66 | fi 67 | return $WAITFORIT_RESULT 68 | } 69 | 70 | # process arguments 71 | while [[ $# -gt 0 ]] 72 | do 73 | case "$1" in 74 | *:* ) 75 | WAITFORIT_hostport=(${1//:/ }) 76 | WAITFORIT_HOST=${WAITFORIT_hostport[0]} 77 | WAITFORIT_PORT=${WAITFORIT_hostport[1]} 78 | shift 1 79 | ;; 80 | --child) 81 | WAITFORIT_CHILD=1 82 | shift 1 83 | ;; 84 | -q | --quiet) 85 | WAITFORIT_QUIET=1 86 | shift 1 87 | ;; 88 | -s | --strict) 89 | WAITFORIT_STRICT=1 90 | shift 1 91 | ;; 92 | -h) 93 | WAITFORIT_HOST="$2" 94 | if [[ $WAITFORIT_HOST == "" ]]; then break; fi 95 | shift 2 96 | ;; 97 | --host=*) 98 | WAITFORIT_HOST="${1#*=}" 99 | shift 1 100 | ;; 101 | -p) 102 | WAITFORIT_PORT="$2" 103 | if [[ $WAITFORIT_PORT == "" ]]; then break; fi 104 | shift 2 105 | ;; 106 | --port=*) 107 | WAITFORIT_PORT="${1#*=}" 108 | shift 1 109 | ;; 110 | -t) 111 | WAITFORIT_TIMEOUT="$2" 112 | if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi 113 | shift 2 114 | ;; 115 | --timeout=*) 116 | WAITFORIT_TIMEOUT="${1#*=}" 117 | shift 1 118 | ;; 119 | --) 120 | shift 121 | WAITFORIT_CLI=("$@") 122 | break 123 | ;; 124 | --help) 125 | usage 126 | ;; 127 | *) 128 | echoerr "Unknown argument: $1" 129 | usage 130 | ;; 131 | esac 132 | done 133 | 134 | if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then 135 | echoerr "Error: you need to provide a host and port to test." 136 | usage 137 | fi 138 | 139 | WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} 140 | WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} 141 | WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} 142 | WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} 143 | 144 | # Check to see if timeout is from busybox? 145 | WAITFORIT_TIMEOUT_PATH=$(type -p timeout) 146 | WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) 147 | 148 | WAITFORIT_BUSYTIMEFLAG="" 149 | if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then 150 | WAITFORIT_ISBUSY=1 151 | # Check if busybox timeout uses -t flag 152 | # (recent Alpine versions don't support -t anymore) 153 | if timeout &>/dev/stdout | grep -q -e '-t '; then 154 | WAITFORIT_BUSYTIMEFLAG="-t" 155 | fi 156 | else 157 | WAITFORIT_ISBUSY=0 158 | fi 159 | 160 | if [[ $WAITFORIT_CHILD -gt 0 ]]; then 161 | wait_for 162 | WAITFORIT_RESULT=$? 163 | exit $WAITFORIT_RESULT 164 | else 165 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 166 | wait_for_wrapper 167 | WAITFORIT_RESULT=$? 168 | else 169 | wait_for 170 | WAITFORIT_RESULT=$? 171 | fi 172 | fi 173 | 174 | if [[ $WAITFORIT_CLI != "" ]]; then 175 | if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then 176 | echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" 177 | exit $WAITFORIT_RESULT 178 | fi 179 | exec "${WAITFORIT_CLI[@]}" 180 | else 181 | exit $WAITFORIT_RESULT 182 | fi 183 | -------------------------------------------------------------------------------- /deployment/scripts/celery/start-beat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | celery -A "$APP_NAME" beat -l info 7 | -------------------------------------------------------------------------------- /deployment/scripts/celery/start-worker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | celery -A "$APP_NAME" worker -l info 7 | -------------------------------------------------------------------------------- /deployment/scripts/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | events {} 2 | 3 | http { 4 | include /etc/nginx/mime.types; 5 | default_type application/octet-stream; 6 | 7 | upstream backend { 8 | server backend:8000; 9 | } 10 | 11 | server { 12 | listen 80; 13 | 14 | location / { 15 | proxy_pass http://backend; 16 | proxy_set_header Host $host; 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 19 | proxy_set_header X-Forwarded-Proto $scheme; 20 | } 21 | 22 | location /static/ { 23 | alias /usr/src/app/static/; 24 | } 25 | 26 | location /media/ { 27 | alias /usr/src/app/media/; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | backend: 5 | container_name: "${APP_NAME}-backend" 6 | build: 7 | context: . 8 | dockerfile: deployment/Dockerfile 9 | args: 10 | - APP_NAME=${APP_NAME} 11 | - APP_HOST=${APP_HOST} 12 | - APP_PORT=${APP_PORT} 13 | volumes: 14 | - ./src:/usr/src/app/ 15 | - ./deployment/scripts:/app/deployment/scripts/ 16 | - static_files:/usr/src/app/static 17 | - media_files:/usr/src/app/media 18 | labels: 19 | - "traefik.enable=true" 20 | - "traefik.http.routers.${APP_NAME}-backend.rule=Host(`${APP_DOMAIN}`)" 21 | - "traefik.http.routers.${APP_NAME}-backend.entrypoints=web" 22 | - "traefik.http.services.${APP_NAME}-backend.loadbalancer.server.port=${APP_PORT:-8000}" 23 | env_file: .env 24 | expose: 25 | - "${APP_PORT:-8000}" 26 | depends_on: 27 | db: 28 | condition: service_healthy 29 | command: [ "/bin/sh", "/app/deployment/scripts/backend/start.sh" ] 30 | 31 | db: 32 | image: postgres:15.2-alpine 33 | container_name: "${APP_NAME}-db" 34 | hostname: "${POSTGRES_HOST:-db}" 35 | volumes: 36 | - postgres_data_dir:/var/lib/postgresql/data/ 37 | env_file: .env 38 | expose: 39 | - "${POSTGRES_PORT:-5432}" 40 | shm_size: 1g 41 | healthcheck: 42 | test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "${POSTGRES_DB}"] 43 | interval: 10s 44 | timeout: 5s 45 | retries: 5 46 | 47 | 48 | redis: 49 | container_name: "${APP_NAME}-redis" 50 | image: redis:latest 51 | volumes: 52 | - redis_data:/data 53 | 54 | celery-worker: &celery-worker 55 | container_name: "${APP_NAME}-celery-worker" 56 | build: 57 | context: . 58 | dockerfile: deployment/Dockerfile 59 | volumes: 60 | - ./src:/usr/src/app/ 61 | - ./deployment/scripts:/app/deployment/scripts/ 62 | env_file: .env 63 | depends_on: 64 | - db 65 | - redis 66 | - backend 67 | command: [ "/bin/sh", "/app/deployment/scripts/celery/start-worker.sh" ] 68 | 69 | celery-beat: 70 | <<: *celery-worker 71 | container_name: "${APP_NAME}-celery-beat" 72 | command: [ "/bin/sh", "/app/deployment/scripts/celery/start-beat.sh" ] 73 | 74 | nginx: 75 | image: nginx:latest 76 | container_name: "${APP_NAME}-nginx" 77 | volumes: 78 | - ./deployment/scripts/nginx/nginx.conf:/etc/nginx/nginx.conf:ro 79 | - static_files:/usr/src/app/static 80 | - media_files:/usr/src/app/media 81 | labels: 82 | - "traefik.enable=true" 83 | - "traefik.http.routers.${APP_NAME}-nginx.rule=Host(`${APP_DOMAIN}`)" 84 | - "traefik.http.routers.${APP_NAME}-nginx.entrypoints=websecure" 85 | - "traefik.http.services.${APP_NAME}-nginx.loadbalancer.server.port=80" 86 | - "traefik.http.routers.${APP_NAME}-nginx.tls=true" 87 | - "traefik.http.routers.${APP_NAME}-nginx.tls.certresolver=myresolver" 88 | expose: 89 | - "80" 90 | - "443" 91 | depends_on: 92 | - backend 93 | - traefik 94 | 95 | traefik: 96 | image: traefik:v2.5 97 | container_name: "${APP_NAME}-traefik" 98 | command: 99 | - "--providers.docker=true" 100 | - "--providers.docker.exposedbydefault=false" 101 | - "--providers.docker.watch=true" 102 | - "--entrypoints.web.address=:80" 103 | - "--entrypoints.websecure.address=:443" 104 | - "--entrypoints.web.http.redirections.entryPoint.to=websecure" 105 | - "--entrypoints.web.http.redirections.entryPoint.scheme=https" 106 | - "--entrypoints.web.http.redirections.entrypoint.permanent=true" 107 | - "--api.dashboard=true" 108 | - "--certificatesresolvers.myresolver.acme.httpchallenge=true" 109 | - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web" 110 | - "--certificatesresolvers.myresolver.acme.email=${LETSENCRYPT_EMAIL}" 111 | - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" 112 | - "--log.level=DEBUG" 113 | - "--accesslog=true" 114 | - "--tracing=true" 115 | ports: 116 | - "80:80" 117 | - "443:443" 118 | volumes: 119 | - "/var/run/docker.sock:/var/run/docker.sock" 120 | - "./letsencrypt:/letsencrypt" 121 | 122 | volumes: 123 | static_files: 124 | media_files: 125 | postgres_data_dir: 126 | redis_data: 127 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | backend: 5 | container_name: "${APP_NAME}-backend" 6 | build: 7 | context: . 8 | dockerfile: deployment/Dockerfile 9 | args: 10 | - APP_NAME=${APP_NAME} 11 | - APP_HOST=${APP_HOST} 12 | - APP_PORT=${APP_PORT} 13 | volumes: 14 | - ./src:/usr/src/app/ 15 | - ./deployment/scripts:/app/deployment/scripts/ 16 | env_file: .env 17 | ports: 18 | - "${APP_PORT}:${APP_PORT}" 19 | depends_on: 20 | db: 21 | condition: service_healthy 22 | command: [ "/bin/sh", "/app/deployment/scripts/backend/start.sh" ] 23 | 24 | db: 25 | image: postgres:15.2-alpine 26 | container_name: "${APP_NAME}-db" 27 | hostname: "${POSTGRES_HOST:-db}" 28 | volumes: 29 | - postgres_data_dir:/var/lib/postgresql/data/ 30 | env_file: .env 31 | expose: 32 | - "${POSTGRES_PORT:-5432}" 33 | shm_size: 1g 34 | healthcheck: 35 | test: [ "CMD", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "${POSTGRES_DB}" ] 36 | interval: 10s 37 | timeout: 5s 38 | retries: 5 39 | 40 | 41 | redis: 42 | container_name: "${APP_NAME}-redis" 43 | image: redis:latest 44 | volumes: 45 | - redis_data:/data 46 | 47 | celery-worker: &celery-worker 48 | container_name: "${APP_NAME}-celery-worker" 49 | build: 50 | context: . 51 | dockerfile: deployment/Dockerfile 52 | volumes: 53 | - ./src:/usr/src/app/ 54 | - ./deployment/scripts:/app/deployment/scripts/ 55 | env_file: .env 56 | depends_on: 57 | - db 58 | - redis 59 | - backend 60 | command: [ "/bin/sh", "/app/deployment/scripts/celery/start-worker.sh" ] 61 | 62 | celery-beat: 63 | <<: *celery-worker 64 | container_name: "${APP_NAME}-celery-beat" 65 | command: [ "/bin/sh", "/app/deployment/scripts/celery/start-beat.sh" ] 66 | 67 | volumes: 68 | postgres_data_dir: 69 | redis_data: 70 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # Application configuration variables 2 | APP_NAME=project_name 3 | APP_HOST=0.0.0.0 4 | APP_PORT=8000 5 | SECRET_KEY='ur secret key' 6 | APP_ENV='dev' # Could be dev, prod or test 7 | DEBUG=True 8 | ALLOWED_HOSTS='app.backend.dev' 9 | CSRF_TRUSTED_ORIGINS='https://*.backend.dev' 10 | CORS_ALLOWED_ORIGINS=http://app.backend.dev,http://localhost:3000 11 | 12 | # Postgres configuration variables 13 | POSTGRES_USER=postgres 14 | POSTGRES_PASSWORD=postgres 15 | POSTGRES_DB=postgres 16 | POSTGRES_PORT=5432 17 | POSTGRES_HOST=db 18 | 19 | 20 | # Traefik configuration variables 21 | LETSENCRYPT_EMAIL=your_email 22 | APP_DOMAIN=app.backend.dev 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "project" 3 | version = "0.1.0" 4 | authors = ["Your Name "] 5 | 6 | 7 | # TESTING 8 | [tool.pytest.ini_options] 9 | minversion = "6.0" 10 | addopts = "-ra -q --force-sugar --no-migrations --reuse-db --log-cli-level=INFO" 11 | testpaths = [ 12 | "tests", 13 | ] 14 | pythonpath = [".", "src"] 15 | python_files = "tests.py test_*.py *_tests.py" 16 | DJANGO_SETTINGS_MODULE = "project_name.settings.test" 17 | filterwarnings = [ 18 | 'ignore::DeprecationWarning:kombu.*:', 19 | 'ignore::DeprecationWarning:celery.*:', 20 | ] 21 | 22 | [tool.coverage.report] 23 | fail_under = 85 24 | show_missing = "true" 25 | exclude_lines = [ 26 | "pragma: no cover", 27 | "raise NotImplementedError", 28 | "if TYPE_CHECKING:", 29 | "if __name__ == .__main__.:", 30 | "import*", 31 | "def __str__", 32 | "def on_success", 33 | "def clean", 34 | "if missing", 35 | "if relations.exists()", 36 | "(FileDoesNotExistException, FileNotSupportedException)", 37 | ] 38 | 39 | 40 | [tool.coverage.run] 41 | omit = [ 42 | "*/tests/*", 43 | "*/migrations/*", 44 | "*/urls.py", 45 | "*/settings/*", 46 | "*/wsgi.py", 47 | "manage.py", 48 | "*__init__.py", 49 | ] 50 | source = ["src"] 51 | 52 | 53 | # LINTING 54 | [tool.black] 55 | line-length = 88 56 | target-version = ['py311'] 57 | include = '\.pyi?$' 58 | # 'extend-exclude' excludes files or directories in addition to the defaults 59 | extend-exclude = ''' 60 | ^(.*/)?migrations/.*$ 61 | ''' 62 | 63 | 64 | [tool.ruff] 65 | format = "grouped" 66 | line-length = 88 # black default 67 | extend-exclude = [ 68 | "src/migrations/*", 69 | "src/media/*", 70 | "src/static/*", 71 | "src/manage.py", 72 | "*/test_data/*", 73 | "*__init__.py", 74 | ] 75 | 76 | select = ["E", "F"] 77 | ignore = [ 78 | "E501", # line too long, handled by black 79 | "B008", # do not perform function calls in argument defaults 80 | "C901", # too complex 81 | "F405", # name may be undefined, or defined from star imports 82 | ] 83 | 84 | 85 | # Allow unused variables when underscore-prefixed. 86 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 87 | 88 | # Assume Python 3.11. 89 | target-version = "py311" 90 | 91 | [tool.ruff.mccabe] 92 | # Unlike Flake8, default to a complexity level of 10. 93 | max-complexity = 10 94 | 95 | 96 | [tool.ruff.isort] 97 | force-to-top = ["src"] 98 | known-first-party = ["src"] 99 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | APP_PATH="src" 4 | 5 | ruff $APP_PATH --fix 6 | black $APP_PATH -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | APP_PATH="src" 4 | 5 | ruff $APP_PATH 6 | black $APP_PATH --check 7 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | coverage run -m pytest -v 4 | exit 0 5 | -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | APP_ENV = os.getenv("APP_ENV", "dev") 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"project_name.settings.{APP_ENV}") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /src/project_name/__init__.py: -------------------------------------------------------------------------------- 1 | # This will make sure the app is always imported when 2 | # Django starts so that shared_task will use this app. 3 | from .celery import app as celery_app 4 | 5 | __all__ = ("celery_app",) 6 | -------------------------------------------------------------------------------- /src/project_name/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for project_name project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | APP_ENV = os.getenv("APP_ENV", "dev") 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"project_name.settings.{APP_ENV}") 16 | 17 | application = get_asgi_application() 18 | -------------------------------------------------------------------------------- /src/project_name/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | from celery.schedules import crontab 5 | 6 | # Set the default Django settings module for the 'celery' program. 7 | APP_ENV = os.getenv("APP_ENV", "dev") 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"project_name.settings.{APP_ENV}") 9 | 10 | app = Celery("project_name") 11 | 12 | # Using a string here means the worker doesn't have to serialize 13 | # the configuration object to child processes. 14 | # - namespace='CELERY' means all celery-related configuration keys 15 | # should have a `CELERY_` prefix. 16 | app.config_from_object("django.conf:settings", namespace="CELERY") 17 | 18 | # Load task modules from all registered Django apps. 19 | app.autodiscover_tasks() 20 | 21 | 22 | @app.task(bind=True) 23 | def debug_task(self): 24 | print(f"Request: {self.request!r}") 25 | 26 | 27 | app.conf.beat_schedule = { 28 | "delete_job_files": { 29 | "task": "test_periodic_task", 30 | # Every 1 minute for testing purposes 31 | "schedule": crontab(minute="*/1"), 32 | }, 33 | } 34 | 35 | app.conf.timezone = "UTC" 36 | -------------------------------------------------------------------------------- /src/project_name/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godd0t/django-docker-quickstart/2b76208c26ea28660ed7fa99b81089d7666da0de/src/project_name/settings/__init__.py -------------------------------------------------------------------------------- /src/project_name/settings/base.py: -------------------------------------------------------------------------------- 1 | from os import getenv as os_getenv, path as os_path # noqa 2 | from pathlib import Path 3 | 4 | from django.core.management.utils import get_random_secret_key 5 | 6 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 7 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 8 | 9 | SECRET_KEY = os_getenv( 10 | "SECRET_KEY", get_random_secret_key() 11 | ) # If SECRET_KEY is not set, generate a random one 12 | APP_ENV = os_getenv("APP_ENV", "dev") 13 | DEBUG = os_getenv("DEBUG", "true").lower() in ["True", "true", "1", "yes", "y"] 14 | 15 | ALLOWED_HOSTS = os_getenv("ALLOWED_HOSTS", "localhost").split(",") 16 | 17 | 18 | if DEBUG: 19 | CORS_ORIGIN_ALLOW_ALL = True 20 | else: 21 | CSRF_TRUSTED_ORIGINS = os_getenv("CSRF_TRUSTED_ORIGINS", "http://localhost").split( 22 | "," 23 | ) 24 | CORS_ALLOWED_ORIGINS = os_getenv("CORS_ALLOWED_ORIGINS", "http://localhost").split( 25 | "," 26 | ) 27 | 28 | # Application definition 29 | 30 | INSTALLED_APPS = [ 31 | "django.contrib.admin", 32 | "django.contrib.auth", 33 | "django.contrib.contenttypes", 34 | "django.contrib.sessions", 35 | "django.contrib.messages", 36 | "django.contrib.staticfiles", 37 | "corsheaders", 38 | "test_app", 39 | ] 40 | 41 | MIDDLEWARE = [ 42 | "django.middleware.security.SecurityMiddleware", 43 | "whitenoise.middleware.WhiteNoiseMiddleware", 44 | "django.contrib.sessions.middleware.SessionMiddleware", 45 | "corsheaders.middleware.CorsMiddleware", # CorsMiddleware should be placed as high as possible, 46 | "django.middleware.common.CommonMiddleware", 47 | "django.middleware.csrf.CsrfViewMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | "django.contrib.messages.middleware.MessageMiddleware", 50 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 51 | ] 52 | 53 | ROOT_URLCONF = "project_name.urls" 54 | 55 | TEMPLATES = [ 56 | { 57 | "BACKEND": "django.template.backends.django.DjangoTemplates", 58 | "DIRS": [], 59 | "APP_DIRS": True, 60 | "OPTIONS": { 61 | "context_processors": [ 62 | "django.template.context_processors.debug", 63 | "django.template.context_processors.request", 64 | "django.contrib.auth.context_processors.auth", 65 | "django.contrib.messages.context_processors.messages", 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = "project_name.wsgi.application" 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 75 | 76 | DATABASES = { 77 | "default": { 78 | "ENGINE": "django.db.backends.postgresql", 79 | "NAME": os_getenv("POSTGRES_DB", "postgres"), 80 | "USER": os_getenv("POSTGRES_USER", "postgres"), 81 | "PASSWORD": os_getenv("POSTGRES_PASSWORD", "postgres"), 82 | "HOST": os_getenv("POSTGRES_HOST", "db"), 83 | "PORT": os_getenv("POSTGRES_PORT", "5432"), 84 | } 85 | } 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 93 | }, 94 | { 95 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 96 | }, 97 | { 98 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 99 | }, 100 | { 101 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 102 | }, 103 | ] 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 107 | 108 | LANGUAGE_CODE = "en-us" 109 | 110 | TIME_ZONE = "UTC" 111 | 112 | USE_I18N = True 113 | 114 | USE_TZ = True 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 118 | 119 | STATIC_URL = "static/" 120 | STATIC_ROOT = os_path.join(BASE_DIR, "static") 121 | MEDIA_URL = "media/" 122 | MEDIA_ROOT = os_path.join(BASE_DIR, "media") 123 | 124 | # Default primary key field type 125 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 126 | 127 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 128 | 129 | # Redis 130 | REDIS_DB_KEYS = { 131 | "dev": 0, 132 | "test": 1, 133 | "prod": 2, 134 | } 135 | 136 | # Redis settings 137 | 138 | REDIS_HOST = os_getenv("REDIS_HOST", "redis") 139 | REDIS_PORT = os_getenv("REDIS_PORT", 6379) 140 | 141 | REDIS_DB = REDIS_DB_KEYS.get(APP_ENV, 0) 142 | REDIS_URL = f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}" 143 | 144 | # Celery settings 145 | 146 | CELERY_BROKER_URL = REDIS_URL 147 | CELERY_RESULT_BACKEND = REDIS_URL 148 | -------------------------------------------------------------------------------- /src/project_name/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | -------------------------------------------------------------------------------- /src/project_name/settings/prod.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | STORAGES = { 4 | "default": { 5 | "BACKEND": "django.core.files.storage.FileSystemStorage", 6 | }, 7 | "staticfiles": { 8 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /src/project_name/settings/test.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | 4 | DATABASES = { 5 | "default": { 6 | "ENGINE": "django.db.backends.sqlite3", 7 | "NAME": ":memory:", 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/project_name/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.urls import path, include 6 | 7 | urlpatterns = [ 8 | path("admin/", admin.site.urls), 9 | path("test_app/", include("test_app.urls")), 10 | ] 11 | 12 | if settings.DEBUG: 13 | urlpatterns += staticfiles_urlpatterns() 14 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 15 | -------------------------------------------------------------------------------- /src/project_name/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project_name project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | APP_ENV = os.getenv("APP_ENV", "dev") 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"project_name.settings.{APP_ENV}") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /src/requirements.dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest==7.3.1 3 | pytest-django==4.5.2 4 | pytest-sugar==0.9.7 5 | coverage[toml]==7.2.5 6 | black==23.3.0 7 | ruff==0.0.265 8 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==5.1.1 2 | asgiref==3.6.0 3 | billiard==3.6.4.0 4 | celery==5.2.7 5 | click==8.1.3 6 | click-didyoumean==0.3.0 7 | click-plugins==1.1.1 8 | click-repl==0.2.0 9 | Django==4.2.1 10 | django-cors-headers==3.14.0 11 | gunicorn==20.1.0 12 | kombu==5.2.4 13 | prompt-toolkit==3.0.38 14 | psycopg==3.1.9 15 | pytz==2023.3 16 | six==1.16.0 17 | sqlparse==0.4.4 18 | typing_extensions==4.5.0 19 | vine==5.0.0 20 | wcwidth==0.2.6 21 | redis==4.5.5 22 | whitenoise==6.4.0 23 | -------------------------------------------------------------------------------- /src/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godd0t/django-docker-quickstart/2b76208c26ea28660ed7fa99b81089d7666da0de/src/test_app/__init__.py -------------------------------------------------------------------------------- /src/test_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from test_app.models import TestModel 4 | 5 | 6 | @admin.register(TestModel) 7 | class TestModelAdmin(admin.ModelAdmin): 8 | list_display = ("name", "description") 9 | -------------------------------------------------------------------------------- /src/test_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestAppConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "test_app" 7 | -------------------------------------------------------------------------------- /src/test_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.1 on 2023-05-10 11:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='TestModel', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=255)), 19 | ('description', models.TextField()), 20 | ('file', models.FileField(upload_to='test_app/files/')), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/test_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godd0t/django-docker-quickstart/2b76208c26ea28660ed7fa99b81089d7666da0de/src/test_app/migrations/__init__.py -------------------------------------------------------------------------------- /src/test_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models # noqa 2 | 3 | 4 | class TestModel(models.Model): 5 | name = models.CharField(max_length=255) 6 | description = models.TextField() 7 | file = models.FileField(upload_to="test_app/files/") 8 | 9 | def __str__(self): 10 | return self.name 11 | -------------------------------------------------------------------------------- /src/test_app/tasks.py: -------------------------------------------------------------------------------- 1 | from project_name.celery import app 2 | 3 | 4 | @app.task(bind=True, name="test_periodic_task") 5 | def test_periodic_task(self): # noqa: Adding self since we are using bind=True 6 | print("Hello from periodic task") 7 | -------------------------------------------------------------------------------- /src/test_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path # noqa 2 | 3 | urlpatterns = [ 4 | # ... other urls 5 | ] 6 | -------------------------------------------------------------------------------- /src/test_app/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render # noqa 2 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godd0t/django-docker-quickstart/2b76208c26ea28660ed7fa99b81089d7666da0de/src/tests/__init__.py -------------------------------------------------------------------------------- /src/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.contrib.auth.models import User 3 | 4 | 5 | @pytest.fixture(autouse=True) 6 | def enable_db_access_for_all_tests(db): 7 | """ 8 | This fixture enables database access for all tests. 9 | """ 10 | pass 11 | 12 | 13 | @pytest.fixture 14 | def test_user(): 15 | return User.objects.create_user( 16 | username="test_user", email="test_user@test.com", password="test_password" 17 | ) 18 | -------------------------------------------------------------------------------- /src/tests/test_app_1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godd0t/django-docker-quickstart/2b76208c26ea28660ed7fa99b81089d7666da0de/src/tests/test_app_1/__init__.py -------------------------------------------------------------------------------- /src/tests/test_app_1/test_example.py: -------------------------------------------------------------------------------- 1 | def test_example(test_user): 2 | assert test_user.username == "test_user" 3 | assert test_user.email is not None 4 | --------------------------------------------------------------------------------