├── .env.dev-sample ├── .env.prod-sample ├── .env.prod.db-sample ├── .gitignore ├── LICENSE ├── README.md ├── app ├── Dockerfile ├── Dockerfile.prod ├── entrypoint.prod.sh ├── entrypoint.sh ├── hello_django │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── requirements.txt └── upload │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ └── __init__.py │ ├── models.py │ ├── templates │ └── upload.html │ ├── tests.py │ └── views.py ├── docker-compose.prod.yml ├── docker-compose.yml └── nginx ├── Dockerfile └── nginx.conf /.env.dev-sample: -------------------------------------------------------------------------------- 1 | DEBUG=1 2 | SECRET_KEY=foo 3 | DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] 4 | SQL_ENGINE=django.db.backends.postgresql 5 | SQL_DATABASE=hello_django_dev 6 | SQL_USER=hello_django 7 | SQL_PASSWORD=hello_django 8 | SQL_HOST=db 9 | SQL_PORT=5432 10 | DATABASE=postgres 11 | -------------------------------------------------------------------------------- /.env.prod-sample: -------------------------------------------------------------------------------- 1 | DEBUG=0 2 | SECRET_KEY=change_me 3 | DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] 4 | SQL_ENGINE=django.db.backends.postgresql 5 | SQL_DATABASE=hello_django_prod 6 | SQL_USER=hello_django 7 | SQL_PASSWORD=hello_django 8 | SQL_HOST=db 9 | SQL_PORT=5432 10 | DATABASE=postgres 11 | -------------------------------------------------------------------------------- /.env.prod.db-sample: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=hello_django 2 | POSTGRES_PASSWORD=hello_django 3 | POSTGRES_DB=hello_django_prod 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache 3 | .DS_Store 4 | .env.dev 5 | .env.prod 6 | .env.prod.db 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TestDriven.io 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dockerizing Django with Postgres, Gunicorn, and Nginx 2 | 3 | ## Want to learn how to build this? 4 | 5 | Check out the [tutorial](https://testdriven.io/dockerizing-django-with-postgres-gunicorn-and-nginx). 6 | 7 | ## Want to use this project? 8 | 9 | ### Development 10 | 11 | Uses the default Django development server. 12 | 13 | 1. Rename *.env.dev-sample* to *.env.dev*. 14 | 1. Update the environment variables in the *docker-compose.yml* and *.env.dev* files. 15 | 1. Build the images and run the containers: 16 | 17 | ```sh 18 | $ docker-compose up -d --build 19 | ``` 20 | 21 | Test it out at [http://localhost:8000](http://localhost:8000). The "app" folder is mounted into the container and your code changes apply automatically. 22 | 23 | ### Production 24 | 25 | Uses gunicorn + nginx. 26 | 27 | 1. Rename *.env.prod-sample* to *.env.prod* and *.env.prod.db-sample* to *.env.prod.db*. Update the environment variables. 28 | 1. Build the images and run the containers: 29 | 30 | ```sh 31 | $ docker-compose -f docker-compose.prod.yml up -d --build 32 | ``` 33 | 34 | Test it out at [http://localhost:1337](http://localhost:1337). No mounted folders. To apply changes, the image must be re-built. 35 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM python:3.11.4-slim-buster 3 | 4 | # set work directory 5 | WORKDIR /usr/src/app 6 | 7 | # set environment variables 8 | ENV PYTHONDONTWRITEBYTECODE 1 9 | ENV PYTHONUNBUFFERED 1 10 | 11 | # install system dependencies 12 | RUN apt-get update && apt-get install -y netcat 13 | 14 | # install dependencies 15 | RUN pip install --upgrade pip 16 | COPY ./requirements.txt . 17 | RUN pip install -r requirements.txt 18 | 19 | # copy entrypoint.sh 20 | COPY ./entrypoint.sh . 21 | RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh 22 | RUN chmod +x /usr/src/app/entrypoint.sh 23 | 24 | # copy project 25 | COPY . . 26 | 27 | # run entrypoint.sh 28 | ENTRYPOINT ["/usr/src/app/entrypoint.sh"] 29 | -------------------------------------------------------------------------------- /app/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | ########### 2 | # BUILDER # 3 | ########### 4 | 5 | # pull official base image 6 | FROM python:3.11.4-slim-buster as builder 7 | 8 | # set work directory 9 | WORKDIR /usr/src/app 10 | 11 | # set environment variables 12 | ENV PYTHONDONTWRITEBYTECODE 1 13 | ENV PYTHONUNBUFFERED 1 14 | 15 | # install system dependencies 16 | RUN apt-get update && \ 17 | apt-get install -y --no-install-recommends gcc 18 | 19 | # lint 20 | RUN pip install --upgrade pip 21 | RUN pip install flake8==6.0.0 22 | COPY . /usr/src/app/ 23 | RUN flake8 --ignore=E501,F401 . 24 | 25 | # install python dependencies 26 | COPY ./requirements.txt . 27 | RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt 28 | 29 | 30 | ######### 31 | # FINAL # 32 | ######### 33 | 34 | # pull official base image 35 | FROM python:3.11.4-slim-buster 36 | 37 | # create directory for the app user 38 | RUN mkdir -p /home/app 39 | 40 | # create the app user 41 | RUN addgroup --system app && adduser --system --group app 42 | 43 | # create the appropriate directories 44 | ENV HOME=/home/app 45 | ENV APP_HOME=/home/app/web 46 | RUN mkdir $APP_HOME 47 | RUN mkdir $APP_HOME/staticfiles 48 | RUN mkdir $APP_HOME/mediafiles 49 | WORKDIR $APP_HOME 50 | 51 | # install dependencies 52 | RUN apt-get update && apt-get install -y --no-install-recommends netcat 53 | COPY --from=builder /usr/src/app/wheels /wheels 54 | COPY --from=builder /usr/src/app/requirements.txt . 55 | RUN pip install --upgrade pip 56 | RUN pip install --no-cache /wheels/* 57 | 58 | # copy entrypoint.prod.sh 59 | COPY ./entrypoint.prod.sh . 60 | RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.prod.sh 61 | RUN chmod +x $APP_HOME/entrypoint.prod.sh 62 | 63 | # copy project 64 | COPY . $APP_HOME 65 | 66 | # chown all the files to the app user 67 | RUN chown -R app:app $APP_HOME 68 | 69 | # change to the app user 70 | USER app 71 | 72 | # run entrypoint.prod.sh 73 | ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"] 74 | -------------------------------------------------------------------------------- /app/entrypoint.prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$DATABASE" = "postgres" ] 4 | then 5 | echo "Waiting for postgres..." 6 | 7 | while ! nc -z $SQL_HOST $SQL_PORT; do 8 | sleep 0.1 9 | done 10 | 11 | echo "PostgreSQL started" 12 | fi 13 | 14 | exec "$@" 15 | -------------------------------------------------------------------------------- /app/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$DATABASE" = "postgres" ] 4 | then 5 | echo "Waiting for postgres..." 6 | 7 | while ! nc -z $SQL_HOST $SQL_PORT; do 8 | sleep 0.1 9 | done 10 | 11 | echo "PostgreSQL started" 12 | fi 13 | 14 | python manage.py flush --no-input 15 | python manage.py migrate 16 | 17 | exec "$@" 18 | -------------------------------------------------------------------------------- /app/hello_django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/django-on-docker/26be9cd5f891734425e4fe754d6340b92180b471/app/hello_django/__init__.py -------------------------------------------------------------------------------- /app/hello_django/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for hello_django 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 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hello_django.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /app/hello_django/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for hello_django project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 22 | 23 | SECRET_KEY = os.environ.get("SECRET_KEY") 24 | 25 | DEBUG = bool(os.environ.get("DEBUG", default=0)) 26 | 27 | # 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each. 28 | # For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]' 29 | ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ") 30 | 31 | CSRF_TRUSTED_ORIGINS = ["http://localhost:1337"] 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | "django.contrib.admin", 37 | "django.contrib.auth", 38 | "django.contrib.contenttypes", 39 | "django.contrib.sessions", 40 | "django.contrib.messages", 41 | "django.contrib.staticfiles", 42 | 43 | "upload", 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | "django.middleware.security.SecurityMiddleware", 48 | "django.contrib.sessions.middleware.SessionMiddleware", 49 | "django.middleware.common.CommonMiddleware", 50 | "django.middleware.csrf.CsrfViewMiddleware", 51 | "django.contrib.auth.middleware.AuthenticationMiddleware", 52 | "django.contrib.messages.middleware.MessageMiddleware", 53 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 54 | ] 55 | 56 | ROOT_URLCONF = "hello_django.urls" 57 | 58 | TEMPLATES = [ 59 | { 60 | "BACKEND": "django.template.backends.django.DjangoTemplates", 61 | "DIRS": [], 62 | "APP_DIRS": True, 63 | "OPTIONS": { 64 | "context_processors": [ 65 | "django.template.context_processors.debug", 66 | "django.template.context_processors.request", 67 | "django.contrib.auth.context_processors.auth", 68 | "django.contrib.messages.context_processors.messages", 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = "hello_django.wsgi.application" 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 79 | 80 | DATABASES = { 81 | "default": { 82 | "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"), 83 | "NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"), 84 | "USER": os.environ.get("SQL_USER", "user"), 85 | "PASSWORD": os.environ.get("SQL_PASSWORD", "password"), 86 | "HOST": os.environ.get("SQL_HOST", "localhost"), 87 | "PORT": os.environ.get("SQL_PORT", "5432"), 88 | } 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 98 | }, 99 | { 100 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 101 | }, 102 | { 103 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 104 | }, 105 | { 106 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 113 | 114 | LANGUAGE_CODE = "en-us" 115 | 116 | TIME_ZONE = "UTC" 117 | 118 | USE_I18N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 125 | 126 | STATIC_URL = "/static/" 127 | STATIC_ROOT = BASE_DIR / "staticfiles" 128 | 129 | MEDIA_URL = "/media/" 130 | MEDIA_ROOT = BASE_DIR / "mediafiles" 131 | 132 | 133 | # Default primary key field type 134 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 135 | 136 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 137 | -------------------------------------------------------------------------------- /app/hello_django/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from django.conf import settings 4 | from django.conf.urls.static import static 5 | 6 | from upload.views import image_upload 7 | 8 | urlpatterns = [ 9 | path("", image_upload, name="upload"), 10 | path("admin/", admin.site.urls), 11 | ] 12 | 13 | if bool(settings.DEBUG): 14 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 15 | -------------------------------------------------------------------------------- /app/hello_django/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for hello_django 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 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hello_django.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /app/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 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hello_django.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==4.2.3 2 | gunicorn==21.2.0 3 | psycopg2-binary==2.9.6 4 | -------------------------------------------------------------------------------- /app/upload/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/django-on-docker/26be9cd5f891734425e4fe754d6340b92180b471/app/upload/__init__.py -------------------------------------------------------------------------------- /app/upload/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /app/upload/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UploadConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'upload' 7 | -------------------------------------------------------------------------------- /app/upload/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/django-on-docker/26be9cd5f891734425e4fe754d6340b92180b471/app/upload/migrations/__init__.py -------------------------------------------------------------------------------- /app/upload/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /app/upload/templates/upload.html: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | 3 |
4 | {% csrf_token %} 5 | 6 | 7 |
8 | 9 | {% if image_url %} 10 |

File uploaded at: {{ image_url }}

11 | {% endif %} 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /app/upload/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /app/upload/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.core.files.storage import FileSystemStorage 3 | 4 | 5 | def image_upload(request): 6 | if request.method == "POST" and request.FILES["image_file"]: 7 | image_file = request.FILES["image_file"] 8 | fs = FileSystemStorage() 9 | filename = fs.save(image_file.name, image_file) 10 | image_url = fs.url(filename) 11 | print(image_url) 12 | return render(request, "upload.html", { 13 | "image_url": image_url 14 | }) 15 | return render(request, "upload.html") 16 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web: 5 | build: 6 | context: ./app 7 | dockerfile: Dockerfile.prod 8 | command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000 9 | volumes: 10 | - static_volume:/home/app/web/staticfiles 11 | - media_volume:/home/app/web/mediafiles 12 | expose: 13 | - 8000 14 | env_file: 15 | - ./.env.prod 16 | depends_on: 17 | - db 18 | db: 19 | image: postgres:15 20 | volumes: 21 | - postgres_data:/var/lib/postgresql/data/ 22 | env_file: 23 | - ./.env.prod.db 24 | nginx: 25 | build: ./nginx 26 | volumes: 27 | - static_volume:/home/app/web/staticfiles 28 | - media_volume:/home/app/web/mediafiles 29 | ports: 30 | - 1337:80 31 | depends_on: 32 | - web 33 | 34 | volumes: 35 | postgres_data: 36 | static_volume: 37 | media_volume: 38 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web: 5 | build: ./app 6 | command: python manage.py runserver 0.0.0.0:8000 7 | volumes: 8 | - ./app/:/usr/src/app/ 9 | ports: 10 | - 8000:8000 11 | env_file: 12 | - ./.env.dev 13 | depends_on: 14 | - db 15 | db: 16 | image: postgres:15 17 | volumes: 18 | - postgres_data:/var/lib/postgresql/data/ 19 | environment: 20 | - POSTGRES_USER=hello_django 21 | - POSTGRES_PASSWORD=hello_django 22 | - POSTGRES_DB=hello_django_dev 23 | 24 | volumes: 25 | postgres_data: 26 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.25 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | COPY nginx.conf /etc/nginx/conf.d 5 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream hello_django { 2 | server web:8000; 3 | } 4 | 5 | server { 6 | 7 | listen 80; 8 | 9 | location / { 10 | proxy_pass http://hello_django; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header Host $host; 13 | proxy_redirect off; 14 | } 15 | 16 | location /static/ { 17 | alias /home/app/web/staticfiles/; 18 | } 19 | 20 | location /media/ { 21 | alias /home/app/web/mediafiles/; 22 | } 23 | 24 | } 25 | --------------------------------------------------------------------------------