├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config.ini.sample ├── django_docker ├── db.sqlite3 ├── django_docker │ ├── __init__.py │ ├── settings.py │ ├── settings_production.py │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── hello_world │ ├── __init__.py │ ├── admin.py │ ├── models.py │ ├── templates │ │ └── hello_world │ │ │ └── index.html │ ├── tests.py │ └── views.py ├── manage.py ├── static │ └── css │ │ └── style.css └── templates │ └── base.html ├── fabfile.py ├── initialize.sh ├── nginx.conf ├── requirements.txt └── supervisord.conf /.dockerignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.pyc 4 | config.ini 5 | supervisord.log 6 | supervisord.pid 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.pyc 4 | config.ini 5 | supervisord.log 6 | supervisord.pid 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | # Enable production settings by default; for development, this can be set to 4 | # `false` in `docker run --env` 5 | ENV DJANGO_PRODUCTION=true 6 | 7 | # Set terminal to be noninteractive 8 | ENV DEBIAN_FRONTEND noninteractive 9 | 10 | # Enable MySQL root user creation without interactive input 11 | RUN echo 'mysql-server mysql-server/root_password password devrootpass' | debconf-set-selections 12 | RUN echo 'mysql-server mysql-server/root_password_again password devrootpass' | debconf-set-selections 13 | 14 | # Install packages 15 | RUN apt-get update && apt-get install -y \ 16 | git \ 17 | libmysqlclient-dev \ 18 | mysql-server \ 19 | nginx \ 20 | python-dev \ 21 | python-mysqldb \ 22 | python-setuptools \ 23 | supervisor \ 24 | vim 25 | RUN easy_install pip 26 | 27 | # Handle urllib3 InsecurePlatformWarning 28 | RUN apt-get install -y libffi-dev libssl-dev libpython2.7-dev 29 | RUN pip install urllib3[security] requests[security] ndg-httpsclient pyasn1 30 | 31 | # Configure Django project 32 | ADD . /code 33 | RUN mkdir /djangomedia 34 | RUN mkdir /static 35 | RUN mkdir /logs 36 | RUN mkdir /logs/nginx 37 | RUN mkdir /logs/gunicorn 38 | WORKDIR /code 39 | RUN pip install -r requirements.txt 40 | RUN chmod ug+x /code/initialize.sh 41 | 42 | # Expose ports 43 | # 80 = Nginx 44 | # 8000 = Gunicorn 45 | # 3306 = MySQL 46 | EXPOSE 80 8000 3306 47 | 48 | # Configure Nginx 49 | RUN ln -s /code/nginx.conf /etc/nginx/sites-enabled/django_docker.conf 50 | RUN rm /etc/nginx/sites-enabled/default 51 | 52 | # Run Supervisor (i.e., start MySQL, Nginx, and Gunicorn) 53 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 54 | CMD ["/usr/bin/supervisord"] 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015-16 Joe Mornin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-docker 2 | 3 | This is a production-ready setup for running Django on Docker. It has sensible defaults for security, scaling, and workflow. It's a robust and simple way to run Django projects on production servers. 4 | 5 | ## Quick start 6 | 7 | $ cp config.ini.sample config.ini # add your server details here 8 | $ fab deploy_production 9 | 10 | That's it—you now have a fully Dockerized Django project running on a production server. Read below for details on configuring the project and managing the development workflow. 11 | 12 | ## Installation 13 | 14 | First, [install Docker](https://docs.docker.com/installation/). If you're new to Docker, you might also want to check out the [Hello, world! tutorial](https://docs.docker.com/userguide/dockerizing/). 15 | 16 | Next, clone this repo: 17 | 18 | $ git clone git@github.com:morninj/django-docker.git 19 | $ cd django-docker 20 | 21 | (Mac users should clone it to a directory under `/Users` because of a [Docker bug](https://blog.docker.com/2014/10/docker-1-3-signed-images-process-injection-security-options-mac-shared-directories/) involving Mac shared directories.) 22 | 23 | You can also fork this repo or pull it as image from Docker Hub as [`morninj/django-docker`](https://hub.docker.com/r/morninj/django-docker/). 24 | 25 | 26 | Update the `origin` to point to your own Git repo: 27 | 28 | $ git remote set-url origin https://github.com/user/repo.git 29 | 30 | ## Configure the project 31 | 32 | Project settings live in `config.ini`. It contains sensitive data, so it's excluded in `.gitignore` and `.dockerignore`. Copy `config.ini.sample` to `config.ini`: 33 | 34 | $ cp config.ini.sample config.ini 35 | 36 | Edit `config.ini`. At a minimum, change these settings: 37 | 38 | * `DOCKER_IMAGE_NAME`: change to `/some-image-name`. 39 | * `ROOT_PASSWORD`: this is the password for a Django superuser with username `root`. Change it to something secure. 40 | 41 | Run `docker ps` to make sure your Docker host is running. If it's not, run: 42 | 43 | $ docker-machine start 44 | $ eval "$(docker-machine env )" 45 | 46 | Build the Docker image (you should be in the `django-docker/` directory, which contains the `Dockerfile`): 47 | 48 | $ docker build -t /django-docker . 49 | 50 | Run the Docker image you just created (the command will be explained in the `Development workflow` section below): 51 | 52 | $ docker run -d -p 80:80 -v $(pwd):/code --env DJANGO_PRODUCTION=false /django-docker 53 | 54 | Run `docker ps` to verify that the Docker container is running: 55 | 56 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 57 | 2830610e8c87 /django-docker "/usr/bin/supervisord" 25 seconds ago Up 25 seconds 0.0.0.0:80->80/tcp, 8000/tcp focused_banach 58 | 59 | You should now be able to access the running app through a web browser. Run `docker-machine ls` to get the local IP address for your Docker host: 60 | 61 | NAME ACTIVE DRIVER STATE URL SWARM 62 | mydockerhost * virtualbox Running tcp://192.168.99.100:2376 63 | 64 | Open `http://192.168.99.100` (or your host's address, if it's different) in a browser. You should see a "Hello, world!" message. 65 | 66 | Grab the `CONTAINER ID` from the `docker ps` output above, and use `docker kill` to stop the container: 67 | 68 | $ docker kill 2830610e8c87 69 | 70 | The output of `docker ps` should now be empty: 71 | 72 | $ docker ps 73 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 74 | 75 | ## Development workflow 76 | 77 | You should be inside the `django-docker` folder, which contains the `Dockerfile` and this README. 78 | 79 | Here's the outline of the workflow: 80 | 81 | 1. Run the Docker container and mount the local directory containing the Django project code 82 | 2. Make changes and test them on the container 83 | 3. Commit the changes to the Git repo 84 | 85 | Start the Docker container: 86 | 87 | $ docker run -d -p 80:80 -v $(pwd):/code --env DJANGO_PRODUCTION=false /django-docker 88 | 89 | Here's what the flags do: 90 | 91 | * `-d`: Run in detached mode (i.e., Docker will no longer listen to the console where you ran `docker run`). 92 | * `-p 80:80`: Map port 80 on the host to port 80 on the container. This lets you communicate with Nginx from your browser. 93 | * `-v $(pwd):/code`: Mount the current directory as a volume at `/code` on the Docker container. This lets you edit the code while the container is running so you can test it without having to rebuild the image. 94 | * `--env DJANGO_PRODUCTION=false`: Production settings are enabled by default in `settings.py` and defined in `settings_production.py`. This flag prevents `settings_production.py` from being loaded, which lets you have separate settings for local development (e.g., `DEBUG = True` and a local development database). 95 | 96 | Point your browser to your Docker host's IP address. You should see the "Hello, world!" message again. 97 | 98 | Point your browser to `http:///admin/`. You should be able to log in with username `root` and the root password you set in `config.ini`. 99 | 100 | In your editor of choice, open `django_docker/hello_world/templates/hello_world/index.html`. It looks like this: 101 | 102 | {% extends 'base.html' %} 103 | 104 | {% load staticfiles %} 105 | 106 | {% block content %} 107 |

Hello, world!

108 | {% endblock content %} 109 | 110 | Edit the `

` tag to read `Hello again, world!` and save the file. Refresh the page in your browser and you should see the updated message. 111 | 112 | Next, commit this change to your repo and push it: 113 | 114 | $ git commit -am 'Add "Hello again, world!"' 115 | $ git push origin master 116 | 117 | Run `docker ps` to get the `CONTAINER ID` and use `docker kill` to stop the container: 118 | 119 | $ docker ps 120 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 121 | 39b60b7eb954 /django-docker "/usr/bin/supervisord" 4 minutes ago Up 3 minutes 0.0.0.0:80->80/tcp, 8000/tcp elegant_banach 122 | $ docker kill 39b60b7eb954 123 | 124 | ### Editing files 125 | 126 | Unlike the Django development server, this configuration won't automatically detect and load changes in Python files. You'll have to manually refresh the server when you make changes (except for templates, which will automatically update). First, open a shell on the dev server: 127 | 128 | $ docker exec -ti /bin/bash 129 | 130 | Then, each time you change a Python file, run: 131 | 132 | $ supervisorctl restart gunicorn 133 | 134 | ### Updating models 135 | 136 | When you update your models, `django-docker` will automatically run `python manage.py makemigrations` and `python manage.py migrate` the next time you run the Docker image. There are a few caveats: 137 | 138 | - Don't delete the `migrations/` folders inside your apps (or else you'll have to do something like editing `initialize.sh` to add `migrate --fake-initial`—ugh) 139 | - When adding new model fields, remember to set a `default` (or else `migrate` will fail) 140 | 141 | Still, there will be times when you need to create migrations by hand. Django currently doesn't support fully automated migration creation—for instance, you might get a prompt like this: 142 | 143 | Did you rename job.cost to job.paid (a IntegerField)? [y/N] 144 | 145 | As far as I know, this can't be automated. To handle this scenario, open a shell on your development Docker machine: 146 | 147 | $ docker run -ti -p 80:80 -v $(pwd):/code --env DJANGO_PRODUCTION=false /django-docker /bin/bash 148 | 149 | Then, start the database server and invoke `initialize.sh`: 150 | 151 | $ /etc/init.d/mysql start 152 | $ ./initialize.sh 153 | 154 | This will call `python manage.py makemigrations` and prompt you if necessary. It will create the necessary migration files. The migration will be automatically applied the next time you run the Docker image in production. (This can be scary. Make a clean backup of your code and database _before_ applying the migration in production in case you need to roll back.) 155 | 156 | ## Deployment 157 | 158 | If you don't have a server running yet, start one. An easy and cheap option is the $5/month virtual server from Digital Ocean. They have Ubuntu images with Docker preinstalled. 159 | 160 | You'll also need a separate database server. Two good options are Google Cloud SQL and Amazon RDS. Be sure to create a database named `django` (or anything else, as long as it matches `DATABASE_NAME` in `config.ini`). Also make sure to create a database user that can access this database. Finally, make sure that the production server is authorized to access the database server. (An easy way to verify all of this is to SSH to the production server and run `mysql -h -uroot -p` and then `mysql> CREATE DATABASE django;`.) 161 | 162 | `config.ini` contains settings for production (e.g., the web server's IP address and the database details). Edit these values now. 163 | 164 | If you want to enable additional production settings, you can add them to `django_docker/django_docker/settings_production.py`. 165 | 166 | If your repository is private on Docker Hub, you'll have to run `docker login` first on the remote host. 167 | 168 | The project can be deployed with a single Fabric command. Make sure Fabric is installed (do `pip install fabric`), and then run: 169 | 170 | $ fab deploy_production 171 | 172 | This builds the Docker image, pushes it to Docker Hub, pulls it on the production server, and starts a container with the production settings. 173 | 174 | Verify that your production settings (not the development settings!) are active. Navigate to `http:///spamalot`. You should see the basic Nginx "not found" page. If you see the full Django error page, that means that `DEBUG = True`, which probably means that your production settings are not loaded. 175 | -------------------------------------------------------------------------------- /config.ini.sample: -------------------------------------------------------------------------------- 1 | [general] 2 | DOCKER_IMAGE_NAME = /django-docker 3 | ROOT_PASSWORD = root 4 | 5 | [production] 6 | SECRET_KEY = putsomethingreallylonghere 7 | HOST = 0.0.0.0 ; IP address of your production server 8 | USE_PASSWORD = false ; false = use SSH key authentication; true = use password 9 | USERNAME = root 10 | PASSWORD = (empty) 11 | PUBLIC_KEY = ~/.ssh/id_dsa.pub ; Your public SSH key 12 | DATABASE_HOST = 0.0.0.0 ; IP address of your database server 13 | DATABASE_USERNAME = root 14 | DATABASE_PASSWORD = rootpassword 15 | DATABASE_NAME = django 16 | -------------------------------------------------------------------------------- /django_docker/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morninj/django-docker/7cfae9495166b5c7518fab5a3885ecdf031ed21f/django_docker/db.sqlite3 -------------------------------------------------------------------------------- /django_docker/django_docker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morninj/django-docker/7cfae9495166b5c7518fab5a3885ecdf031ed21f/django_docker/django_docker/__init__.py -------------------------------------------------------------------------------- /django_docker/django_docker/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_docker project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = 'w5-iqfj^5cl6tj0_8*xmgnj5r^@&p@n+)=6g1-!^i0&sohyicg' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | ALLOWED_HOSTS = ['*'] 26 | 27 | # Application definition 28 | 29 | INSTALLED_APPS = ( 30 | 'django.contrib.admin', 31 | 'django.contrib.auth', 32 | 'django.contrib.contenttypes', 33 | 'django.contrib.sessions', 34 | 'django.contrib.messages', 35 | 'django.contrib.staticfiles', 36 | 'hello_world', 37 | ) 38 | 39 | MIDDLEWARE_CLASSES = ( 40 | 'django.contrib.sessions.middleware.SessionMiddleware', 41 | 'django.middleware.common.CommonMiddleware', 42 | 'django.middleware.csrf.CsrfViewMiddleware', 43 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 44 | 'django.contrib.messages.middleware.MessageMiddleware', 45 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 46 | ) 47 | 48 | ROOT_URLCONF = 'django_docker.urls' 49 | 50 | WSGI_APPLICATION = 'django_docker.wsgi.application' 51 | 52 | 53 | # Database 54 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 55 | 56 | DATABASES = { 57 | 'default': { 58 | 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 59 | 'NAME': 'devdb', # Or path to database file if using sqlite3. 60 | # The following settings are not used with sqlite3: 61 | 'USER': 'devuser', 62 | 'PASSWORD': 'devpass', # Entered via fab command; leave blank if using SQLite 63 | 'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. 64 | 'PORT': '', # Set to empty string for default. 65 | } 66 | } 67 | 68 | 69 | # Internationalization 70 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 71 | 72 | LANGUAGE_CODE = 'en-us' 73 | 74 | TIME_ZONE = 'UTC' 75 | 76 | USE_I18N = True 77 | 78 | USE_L10N = True 79 | 80 | USE_TZ = True 81 | 82 | 83 | # Static files (CSS, JavaScript, Images) 84 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 85 | 86 | STATIC_URL = '/static/' 87 | 88 | STATICFILES_DIRS = ( 89 | os.path.join(BASE_DIR, "static"), 90 | ) 91 | 92 | STATIC_ROOT = '/static/'; 93 | 94 | TEMPLATES = [ 95 | { 96 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 97 | 'DIRS': [ 98 | os.path.join(BASE_DIR, 'templates'), 99 | ], 100 | 'APP_DIRS': True, 101 | 'OPTIONS': { 102 | 'context_processors': [ 103 | # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this 104 | # list if you haven't customized them: 105 | 'django.contrib.auth.context_processors.auth', 106 | 'django.template.context_processors.debug', 107 | 'django.template.context_processors.i18n', 108 | 'django.template.context_processors.media', 109 | 'django.template.context_processors.static', 110 | 'django.template.context_processors.tz', 111 | 'django.contrib.messages.context_processors.messages', 112 | ], 113 | 'debug': True, 114 | }, 115 | }, 116 | ] 117 | 118 | # Import production settings if the environment variable DJANGO_PRODUCTION is true 119 | # (It's set to True by default in the Dockerfile, but you can override it with `docker run` for development) 120 | if os.environ['DJANGO_PRODUCTION'] == 'true': 121 | from settings_production import * 122 | -------------------------------------------------------------------------------- /django_docker/django_docker/settings_production.py: -------------------------------------------------------------------------------- 1 | # see https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 2 | ALLOWED_HOSTS = ['*'] 3 | DEBUG = False 4 | 5 | # The values below are loaded from environment variables 6 | import os 7 | 8 | SECRET_KEY = os.environ['SECRET_KEY'] 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 13 | 'NAME': os.environ['DATABASE_NAME'], # Or path to database file if using sqlite3. 14 | # The following settings are not used with sqlite3: 15 | 'USER': os.environ['DATABASE_USERNAME'], 16 | 'PASSWORD': os.environ['DATABASE_PASSWORD'], # Entered via fab command; leave blank if using SQLite 17 | 'HOST': os.environ['DATABASE_HOST'], # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. 18 | 'PORT': '', # Set to empty string for default. 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /django_docker/django_docker/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from hello_world import views as hello_world_views 3 | 4 | from django.contrib import admin 5 | admin.autodiscover() 6 | 7 | urlpatterns = [ 8 | url(r'^admin/', include(admin.site.urls)), 9 | url(r'^$', hello_world_views.hello_world, name='hello_world'), 10 | ] 11 | -------------------------------------------------------------------------------- /django_docker/django_docker/views.py: -------------------------------------------------------------------------------- 1 | from django.http import render 2 | 3 | def hello_world(request): 4 | return render(request, 'hello_world/index.html') 5 | -------------------------------------------------------------------------------- /django_docker/django_docker/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_docker 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/1.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_docker.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /django_docker/hello_world/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morninj/django-docker/7cfae9495166b5c7518fab5a3885ecdf031ed21f/django_docker/hello_world/__init__.py -------------------------------------------------------------------------------- /django_docker/hello_world/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /django_docker/hello_world/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /django_docker/hello_world/templates/hello_world/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load staticfiles %} 4 | 5 | {% block content %} 6 |

Hello, world!

7 | {% endblock content %} 8 | -------------------------------------------------------------------------------- /django_docker/hello_world/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /django_docker/hello_world/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | def hello_world(request): 4 | return render(request, 'hello_world/index.html') 5 | -------------------------------------------------------------------------------- /django_docker/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_docker.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /django_docker/static/css/style.css: -------------------------------------------------------------------------------- 1 | p.hello-world { 2 | font-family: 'Helvetica', sans-serif; 3 | font-size: 36px; 4 | margin: 100px auto; 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /django_docker/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | {% block content %}{% endblock content %} 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | from fabric.api import * 2 | from fabric.contrib.files import * 3 | import os 4 | from ConfigParser import SafeConfigParser 5 | parser = SafeConfigParser() 6 | parser.read(os.path.join(os.path.dirname(__file__), 'config.ini')) 7 | import time 8 | 9 | # Configure server admin login credentials 10 | if parser.get('production', 'USE_PASSWORD'): 11 | env.password = parser.get('production', 'PASSWORD') 12 | else: 13 | env.key_filename = parser.get('production', 'PUBLIC_KEY') 14 | 15 | # Deploy production server 16 | @hosts(parser.get('production', 'USERNAME') + '@' + parser.get('production', 'HOST')) 17 | def deploy_production(): 18 | start_time = time.time(); 19 | print 'Building Docker image...' 20 | local('docker build -t %s .' % parser.get('general', 'DOCKER_IMAGE_NAME')) 21 | print 'Pushing image to Docker Hub...' 22 | local('docker push %s' % parser.get('general', 'DOCKER_IMAGE_NAME')) 23 | print 'Removing any existing Docker containers on the production host...' 24 | run('if [ "$(docker ps -qa)" != "" ]; then docker rm --force `docker ps -qa`; fi') 25 | run('docker ps') 26 | print 'Removing dangling Docker images...' 27 | run('if [ -z "$(docker images -f "dangling=true" -q)" ]; then echo "no images to remove"; else docker rmi $(docker images -f "dangling=true" -q); fi') 28 | print 'Pulling image on production host...' 29 | run('docker pull %s ' % parser.get('general', 'DOCKER_IMAGE_NAME')); 30 | print 'Running image on production host...' 31 | run_command = '''docker run \ 32 | -d \ 33 | -p 80:80 \ 34 | -p 443:443 \ 35 | --env DJANGO_PRODUCTION=true \ 36 | --env ROOT_PASSWORD={ROOT_PASSWORD} \ 37 | --env DATABASE_HOST={DATABASE_HOST} \ 38 | --env DATABASE_USERNAME={DATABASE_USERNAME} \ 39 | --env DATABASE_PASSWORD={DATABASE_PASSWORD} \ 40 | --env DATABASE_NAME={DATABASE_NAME} \ 41 | --env SECRET_KEY={SECRET_KEY} \ 42 | {DOCKER_IMAGE_NAME}'''.format( 43 | ROOT_PASSWORD=parser.get('general', 'ROOT_PASSWORD'), 44 | DOCKER_IMAGE_NAME=parser.get('general', 'DOCKER_IMAGE_NAME'), 45 | DATABASE_HOST=parser.get('production', 'DATABASE_HOST'), 46 | DATABASE_USERNAME=parser.get('production', 'DATABASE_USERNAME'), 47 | DATABASE_PASSWORD=parser.get('production', 'DATABASE_PASSWORD'), 48 | DATABASE_NAME=parser.get('production', 'DATABASE_NAME'), 49 | SECRET_KEY=parser.get('production', 'SECRET_KEY'), 50 | ) 51 | run(run_command); 52 | print '-' * 80 53 | print parser.get('general', 'DOCKER_IMAGE_NAME') + ' successfully deployed to ' + parser.get('production', 'HOST') 54 | print("Deployment time: %s seconds" % (time.time() - start_time)) 55 | -------------------------------------------------------------------------------- /initialize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script initializes the Django project. It will be executed (from 3 | # supervisord) every time the Docker image is run. 4 | 5 | # If we're not in production, create a temporary dev database 6 | if [ "$DJANGO_PRODUCTION" != "true" ]; then 7 | echo "DJANGO_PRODUCTION=false; creating local database..." 8 | # Wait until the MySQL daemon is running 9 | while [ "$(pgrep mysql | wc -l)" -eq 0 ] ; do 10 | echo "MySQL daemon not running; waiting one second..." 11 | sleep 1 12 | done 13 | # Wait until we can successfully connect to the MySQL daemon 14 | until mysql -uroot -pdevrootpass -e ";" ; do 15 | echo "Can't connect to MySQL; waiting one second..." 16 | sleep 1 17 | done 18 | echo "MySQL daemon is running; creating database..." 19 | mysql -uroot -e "CREATE DATABASE devdb; CREATE USER devuser@localhost; SET PASSWORD FOR devuser@localhost=PASSWORD('devpass'); GRANT ALL PRIVILEGES ON devdb.* TO devuser@localhost IDENTIFIED BY 'devpass'; FLUSH PRIVILEGES;" -pdevrootpass; 20 | else 21 | echo "DJANGO_PRODUCTION=true; no local database created" 22 | fi 23 | 24 | # Initialize Django project 25 | python /code/django_docker/manage.py collectstatic --noinput 26 | python /code/django_docker/manage.py syncdb --noinput 27 | python /code/django_docker/manage.py makemigrations 28 | python /code/django_docker/manage.py migrate --noinput 29 | 30 | # Create a Django superuser named `root` if it doesn't yet exist 31 | echo "Creating Django superuser named 'root'..." 32 | if [ "$DJANGO_PRODUCTION" != "true" ]; then 33 | # We're in the dev environment 34 | if [ "$ROOT_PASSWORD" == "" ]; then 35 | # Root password environment variable is not set; so, load it from config.ini 36 | echo "from ConfigParser import SafeConfigParser; parser = SafeConfigParser(); parser.read('/code/config.ini'); from django.contrib.auth.models import User; print 'Root user already exists' if User.objects.filter(username='root') else User.objects.create_superuser('root', 'admin@example.com', parser.get('general', 'ROOT_PASSWORD'))" | python /code/django_docker/manage.py shell 37 | else 38 | # Root password environment variable IS set; so, use it 39 | echo "import os; from django.contrib.auth.models import User; print 'Root user already exists' if User.objects.filter(username='root') else User.objects.create_superuser('root', 'admin@example.com', os.environ['ROOT_PASSWORD'])" | python /code/django_docker/manage.py shell 40 | fi 41 | else 42 | # We're in production; use root password environment variable 43 | echo "import os; from django.contrib.auth.models import User; print 'Root user already exists' if User.objects.filter(username='root') else User.objects.create_superuser('root', 'admin@example.com', os.environ['ROOT_PASSWORD'])" | python /code/django_docker/manage.py shell 44 | fi 45 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | access_log /dev/null; 4 | error_log /logs/nginx/nginx.error.log; 5 | 6 | location /static/ { 7 | root /; 8 | } 9 | 10 | # TODO add media directory 11 | 12 | location / { 13 | proxy_pass_header Server; 14 | proxy_set_header Host $http_host; 15 | proxy_redirect off; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | proxy_set_header X-Scheme $scheme; 18 | proxy_connect_timeout 10; 19 | proxy_read_timeout 10; 20 | proxy_pass http://127.0.0.1:8000/; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | gunicorn 3 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:mysql] 5 | ; Don't start the MySQL daemon if we're in production 6 | command=/bin/bash -c 'if [ "$DJANGO_PRODUCTION" != "true" ]; then /usr/bin/mysqld_safe; fi' 7 | autostart=true 8 | exitcodes=0 9 | startsecs=0 10 | priority=1 11 | autorestart=unexpected 12 | 13 | [program:initialize] 14 | command=/bin/bash /code/initialize.sh 15 | exitcodes=0 16 | startsecs=0 17 | priority=10 18 | 19 | [program:nginx] 20 | command=/usr/sbin/nginx -g "daemon off;" 21 | autostart=true 22 | autorestart=true 23 | priority=20 24 | 25 | [program:gunicorn] 26 | directory=/code/django_docker 27 | command=/usr/local/bin/gunicorn -b 127.0.0.1:8000 -w 4 django_docker.wsgi --log-level=debug --log-file=/logs/gunicorn/gunicorn.log 28 | autostart=true 29 | autorestart=true 30 | priority=20 31 | 32 | [unix_http_server] 33 | file=/var/run//supervisor.sock ; (the path to the socket file) 34 | chmod=0700 ; sockef file mode (default 0700) 35 | 36 | ; the below section must remain in the config file for RPC 37 | ; (supervisorctl/web interface) to work, additional interfaces may be 38 | ; added by defining them in separate rpcinterface: sections 39 | [rpcinterface:supervisor] 40 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 41 | 42 | [supervisorctl] 43 | serverurl=unix:///dev/shm/supervisor.sock ; use a unix:// URL for a unix socket 44 | --------------------------------------------------------------------------------