├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── README.md ├── config ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── django.env.example ├── docker-compose.yml ├── gunicorn.conf.py ├── manage.py ├── nginx └── default.conf ├── poetry.lock └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Django 7 | *.sqlite3 8 | 9 | # Docker env 10 | django.env 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # dotenv 89 | .env 90 | 91 | # virtualenv 92 | .venv 93 | venv/ 94 | ENV/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-buster 2 | 3 | RUN apt-get update && apt-get upgrade -y 4 | 5 | ENV APP_ROOT /app 6 | WORKDIR ${APP_ROOT} 7 | 8 | ARG POETRY_VERSION=1.4.2 9 | RUN pip install poetry==$POETRY_VERSION 10 | RUN poetry config virtualenvs.create false 11 | 12 | COPY pyproject.toml poetry.lock ./ 13 | RUN poetry install --no-interaction --no-root --no-cache 14 | 15 | COPY . ${APP_ROOT} 16 | 17 | CMD ["gunicorn", "-c", "gunicorn.conf.py", "config.wsgi:application"] 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Watt Iamsuri 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 | # Ready-To-Deploy Django, gunicorn, NGINX, Docker Application 2 | Getting a Django 4.2 app up in no time. In this project, gunicorn is used as a WSGI. NGINX is used as a reverse proxy server. 3 | 4 | ## Premise 5 | I have seen one too many Dockerfile with unreadable code. Many code base out there have Docker setup so elaborately that it is unmodifiable. Here, I try to simplify Dockerfile and Docker Compose file as much as possible, so that more than one developer in a team will understand how it works. 6 | 7 | ## For Development 8 | In the root level of this repository, copy the file named `django.env.example` to `django.env` and adjust file variables 9 | ``` 10 | cp django.env.example django.env 11 | ``` 12 | 13 | Build code with docker compose 14 | ``` 15 | docker-compose build 16 | ``` 17 | 18 | Run the built container 19 | ``` 20 | docker-compose up 21 | ``` 22 | 23 | ## Deploy as a Simple Application 24 | Here is a script to install docker and docker-compose. After running these commands, exit from ssh and reconnect to the instance. This can be used in AWS Launch Config 25 | ``` 26 | #!/bin/bash 27 | sudo yum -y update 28 | sudo yum install -y docker 29 | sudo usermod -a -G docker $(whoami) 30 | sudo service docker start 31 | sudo curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 32 | sudo chmod +x /usr/local/bin/docker-compose 33 | ``` 34 | For Ubuntu 35 | ``` 36 | sudo apt update 37 | sudo apt install -y docker.io 38 | sudo usermod -a -G docker $(whoami) 39 | sudo service docker start 40 | sudo curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 41 | sudo chmod +x /usr/local/bin/docker-compose 42 | ``` 43 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiamsuri/django-gunicorn-nginx-docker/1334e961b8457d2edfeae5011616f00978e46d12/config/__init__.py -------------------------------------------------------------------------------- /config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config 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', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2. 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 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', '') 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = os.getenv('DEBUG') != 'False' 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 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 = 'config.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 = 'config.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': BASE_DIR / 'db.sqlite3', 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_TZ = True 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 118 | 119 | STATIC_URL = 'static/' 120 | 121 | # Default primary key field type 122 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 123 | 124 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 125 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for config project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | ] 23 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /django.env.example: -------------------------------------------------------------------------------- 1 | DJANGO_SECRET_KEY=put_your_django_app_secret_key_here 2 | DEBUG=True 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | app: 5 | build: . 6 | container_name: django 7 | restart: always 8 | env_file: 9 | - django.env 10 | ports: 11 | - "8000:8000" 12 | volumes: 13 | - .:/app 14 | command: python manage.py runserver 0.0.0.0:8000 15 | 16 | nginx: 17 | image: nginx:1.23-alpine 18 | container_name: ngx 19 | ports: 20 | - "80:80" 21 | volumes: 22 | - ./nginx:/etc/nginx/conf.d 23 | depends_on: 24 | - app 25 | -------------------------------------------------------------------------------- /gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | workers = 2 2 | threads = 3 3 | bind = '0.0.0.0:8000' 4 | accesslog = '-' 5 | -------------------------------------------------------------------------------- /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', 'config.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 | -------------------------------------------------------------------------------- /nginx/default.conf: -------------------------------------------------------------------------------- 1 | upstream django { 2 | server app:8000; 3 | } 4 | 5 | server { 6 | location / { 7 | try_files $uri @proxy_to_app; 8 | } 9 | 10 | location @proxy_to_app { 11 | proxy_pass http://django; 12 | 13 | proxy_http_version 1.1; 14 | proxy_set_header Upgrade $http_upgrade; 15 | proxy_set_header Connection "upgrade"; 16 | 17 | proxy_redirect off; 18 | proxy_set_header Host $host; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_set_header X-Forwarded-Host $server_name; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asgiref" 5 | version = "3.6.0" 6 | description = "ASGI specs, helper code, and adapters" 7 | category = "main" 8 | optional = false 9 | python-versions = ">=3.7" 10 | files = [ 11 | {file = "asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac"}, 12 | {file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"}, 13 | ] 14 | 15 | [package.extras] 16 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 17 | 18 | [[package]] 19 | name = "django" 20 | version = "4.2" 21 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 22 | category = "main" 23 | optional = false 24 | python-versions = ">=3.8" 25 | files = [ 26 | {file = "Django-4.2-py3-none-any.whl", hash = "sha256:ad33ed68db9398f5dfb33282704925bce044bef4261cd4fb59e4e7f9ae505a78"}, 27 | {file = "Django-4.2.tar.gz", hash = "sha256:c36e2ab12824e2ac36afa8b2515a70c53c7742f0d6eaefa7311ec379558db997"}, 28 | ] 29 | 30 | [package.dependencies] 31 | asgiref = ">=3.6.0,<4" 32 | sqlparse = ">=0.3.1" 33 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 34 | 35 | [package.extras] 36 | argon2 = ["argon2-cffi (>=19.1.0)"] 37 | bcrypt = ["bcrypt"] 38 | 39 | [[package]] 40 | name = "gunicorn" 41 | version = "20.1.0" 42 | description = "WSGI HTTP Server for UNIX" 43 | category = "main" 44 | optional = false 45 | python-versions = ">=3.5" 46 | files = [ 47 | {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, 48 | {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, 49 | ] 50 | 51 | [package.dependencies] 52 | setuptools = ">=3.0" 53 | 54 | [package.extras] 55 | eventlet = ["eventlet (>=0.24.1)"] 56 | gevent = ["gevent (>=1.4.0)"] 57 | setproctitle = ["setproctitle"] 58 | tornado = ["tornado (>=0.2)"] 59 | 60 | [[package]] 61 | name = "setuptools" 62 | version = "67.6.1" 63 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 64 | category = "main" 65 | optional = false 66 | python-versions = ">=3.7" 67 | files = [ 68 | {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, 69 | {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, 70 | ] 71 | 72 | [package.extras] 73 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 74 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 75 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 76 | 77 | [[package]] 78 | name = "sqlparse" 79 | version = "0.4.3" 80 | description = "A non-validating SQL parser." 81 | category = "main" 82 | optional = false 83 | python-versions = ">=3.5" 84 | files = [ 85 | {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, 86 | {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, 87 | ] 88 | 89 | [[package]] 90 | name = "tzdata" 91 | version = "2023.3" 92 | description = "Provider of IANA time zone data" 93 | category = "main" 94 | optional = false 95 | python-versions = ">=2" 96 | files = [ 97 | {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, 98 | {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, 99 | ] 100 | 101 | [metadata] 102 | lock-version = "2.0" 103 | python-versions = "~3.11" 104 | content-hash = "b37d55b9bf87b8b099cf9c0820788821eaaa7261e2ba6f0a46e8e6dabcd76f36" 105 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-gunicorn-nginx-docker" 3 | version = "1.0.0" 4 | description = "" 5 | authors = ["Watt Iamsuri "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "~3.11" 10 | Django = "^4.2" 11 | gunicorn = "^20.1.0" 12 | 13 | [build-system] 14 | requires = ["poetry-core"] 15 | build-backend = "poetry.core.masonry.api" 16 | --------------------------------------------------------------------------------