├── backups └── .gitkeep ├── nginx ├── ssl │ └── .gitkeep ├── Dockerfile ├── setupconf.sh ├── nginx_nossl.conf └── nginx_ssl.conf ├── backend ├── __init__.py ├── media │ └── .gitkeep ├── apps │ ├── __init__.py │ ├── home │ │ ├── __init__.py │ │ ├── templates │ │ │ └── home │ │ │ │ └── index.html │ │ ├── views.py │ │ ├── urls.py │ │ └── tests.py │ └── testutils.py ├── conf │ ├── __init__.py │ ├── settings_test.py │ ├── settings_prod.py │ └── settings.py ├── .dockerignore ├── bin │ ├── django.sh │ ├── collectstatic.sh │ ├── test.sh │ ├── develop.sh │ ├── install.sh │ ├── start_production.sh │ └── install_package.sh ├── requirements.txt ├── Dockerfile ├── manage.py ├── Dockerfile-production ├── gunicorn.conf.py ├── wsgi.py ├── templates │ └── base.html ├── context_processors.py └── urls.py ├── logs └── prod log go here.txt ├── frontend ├── src │ ├── frontend sources here.txt │ ├── style │ │ └── index.styl │ ├── images │ │ └── hamster.jpg │ └── js │ │ └── index.js ├── .babelrc ├── webpack │ ├── dev-server.js │ ├── webpack.config.development.js │ ├── webpack.config.base.js │ └── webpack.config.production.js └── package.json ├── bin ├── start_production.sh ├── stop_production.sh ├── npm.sh ├── django.sh ├── develop.sh ├── pipinstall.sh ├── psql.sh ├── django_prod.sh ├── build_production.sh ├── test.sh ├── build_frontend.sh ├── backup.sh ├── deploy.sh ├── env.sh ├── restore.sh └── init_db.sh ├── docker-compose.db.yml ├── .gitignore ├── docker-compose.yml ├── docker-compose.test.yml ├── docker-compose.override.yml ├── docker-compose.production.yml └── README.md /backups/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nginx/ssl/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/media/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logs/prod log go here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/home/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/frontend sources here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/home/templates/home/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | Dockerfile* 4 | pipcache -------------------------------------------------------------------------------- /backend/bin/django.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./bin/install.sh 3 | python3 manage.py "$@" -------------------------------------------------------------------------------- /frontend/src/style/index.styl: -------------------------------------------------------------------------------- 1 | body 2 | background-color: green 3 | font-size: 70px -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } -------------------------------------------------------------------------------- /backend/bin/collectstatic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./bin/install.sh 3 | python3 manage.py collectstatic --noinput -------------------------------------------------------------------------------- /backend/bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./bin/install.sh 3 | python3 manage.py test --noinput --liveserver=localhost:8082 "$@" -------------------------------------------------------------------------------- /bin/start_production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # start production server 3 | 4 | source bin/env.sh 5 | dcprod up -d 6 | echo "started" -------------------------------------------------------------------------------- /bin/stop_production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # stop production server 3 | 4 | source bin/env.sh 5 | dcprod stop 6 | echo "stopped" -------------------------------------------------------------------------------- /backend/bin/develop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./bin/install.sh 3 | python3 manage.py migrate 4 | python3 manage.py runserver 0.0.0.0:8000 -------------------------------------------------------------------------------- /frontend/src/images/hamster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domasx2/docker-django-webpack-starter/HEAD/frontend/src/images/hamster.jpg -------------------------------------------------------------------------------- /backend/apps/home/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | def index(request): 4 | return render(request, 'home/index.html') -------------------------------------------------------------------------------- /bin/npm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #run npm command. use this to install new packages to dev 4 | source bin/env.sh 5 | 6 | dcdev run --rm frontend npm $@ -------------------------------------------------------------------------------- /backend/bin/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #install django dependencies 4 | mkdir -p pipcache 5 | pip3 install --cache-dir=/app/pipcache -r requirements.txt -------------------------------------------------------------------------------- /backend/conf/settings_test.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | STATIC_ASSETS_JSON = os.path.join(BASE_DIR, 'assets.json') 4 | 5 | WEBPACK_DEV_SERVER = False 6 | -------------------------------------------------------------------------------- /bin/django.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #run django management command in development mode 4 | source bin/env.sh 5 | 6 | dcdev run --rm django ./bin/django.sh "$@" -------------------------------------------------------------------------------- /bin/develop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #start development server on :8000 4 | 5 | source bin/env.sh 6 | 7 | dcdev build 8 | ./bin/init_db.sh 9 | dcdev up 10 | -------------------------------------------------------------------------------- /bin/pipinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #install new pytho ndep via pip, add it to requirements.txt 4 | source bin/env.sh 5 | 6 | dcdev run --rm django ./bin/install_package.sh $@ -------------------------------------------------------------------------------- /backend/apps/home/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from apps.home.views import index as index_view 3 | 4 | urlpatterns = [ 5 | url(r'^.*?', index_view, name='index') 6 | ] -------------------------------------------------------------------------------- /bin/psql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # open psql session to production db 3 | 4 | source bin/env.sh 5 | 6 | dcprod -f docker-compose.db.yml run --rm dbclient psql -h db -U $DB_USER $DB_NAME 7 | -------------------------------------------------------------------------------- /docker-compose.db.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | dbclient: 4 | image: postgres:9.6 5 | depends_on: 6 | - db 7 | volumes: 8 | - "./backups:/backups" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *sublime* 2 | __pycache__ 3 | *.pyc 4 | node_modules 5 | pipcache 6 | frontend/dist/* 7 | nginx/static/* 8 | nginx/ssl/* 9 | *.log 10 | backups/*.bak 11 | /backend/assets.json 12 | -------------------------------------------------------------------------------- /bin/django_prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #run django management command in production mode 4 | 5 | source bin/env.sh 6 | 7 | dcprod run -e DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_PROD --rm django python3 manage.py $@ -------------------------------------------------------------------------------- /bin/build_production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #builds production images 4 | 5 | source bin/env.sh 6 | 7 | ./bin/build_frontend.sh 8 | cp ./frontend/dist/assets.json ./backend/ 9 | dcprod build 10 | rm ./backend/assets.json -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | # postgres database adapater 2 | psycopg2==2.6.2 3 | 4 | # django framework itself 5 | Django==1.10.5 6 | 7 | #app server for prod 8 | gunicorn==19.6.0 9 | 10 | #selenium for e2e tests 11 | selenium==3.0.2 -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source bin/env.sh 3 | 4 | if ! [[ $* == *--skipbuild* ]]; then 5 | ./bin/build_frontend.sh 6 | else 7 | echo "skipping frontend build..." 8 | fi 9 | 10 | dctest run --rm django ./bin/test.sh "$@" -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.9 2 | 3 | ADD ./nginx/ssl /etc/nginx/ssl 4 | 5 | ADD ./nginx/nginx_nossl.conf /etc/nginx/ 6 | ADD ./nginx/nginx_ssl.conf /etc/nginx/ 7 | ADD ./frontend/dist /static 8 | ADD ./nginx/setupconf.sh . 9 | RUN ./setupconf.sh -------------------------------------------------------------------------------- /backend/bin/start_production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-conf.settings_prod} 3 | echo djsettings: 4 | echo $DJANGO_SETTINGS_MODULE 5 | python3 manage.py migrate 6 | gunicorn -c /app/gunicorn.conf.py wsgi:application -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN pip3 install --upgrade pip 6 | RUN pip3 install wheel 7 | 8 | RUN mkdir -p /app/static /tmp/logs/app 9 | 10 | VOLUME /app 11 | VOLUME /static 12 | 13 | WORKDIR /app 14 | EXPOSE 8000 -------------------------------------------------------------------------------- /backend/bin/install_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | package_name=$1 3 | if [[ -z $package_name ]] 4 | then 5 | echo "specify package name" 6 | exit 7 | fi 8 | 9 | pip3 install $package_name && pip3 freeze | grep -i $package_name | awk '{print "\n\n" $0}' >> ./requirements.txt -------------------------------------------------------------------------------- /bin/build_frontend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #compile frontend production build to frontend/dist 4 | 5 | source bin/env.sh 6 | 7 | dcdev build 8 | dcdev run --rm frontend npm run-script build 9 | dcdev run --rm -e DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_PROD django ./bin/collectstatic.sh -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | volumes: 3 | data: 4 | services: 5 | db: 6 | image: postgres:9.6 7 | environment: 8 | POSTGRES_USER: django 9 | POSTRES_PASSWORD: django 10 | volumes: 11 | - data:/var/lib/postgresql/data -------------------------------------------------------------------------------- /backend/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", "conf.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /backend/Dockerfile-production: -------------------------------------------------------------------------------- 1 | FROM python:3.5 2 | ENV PYTHONUNBUFFERED 1 3 | RUN pip3 install --upgrade pip 4 | RUN mkdir -p /app 5 | WORKDIR /app 6 | COPY ./requirements.txt /app/requirements.txt 7 | RUN pip3 install -r requirements.txt 8 | 9 | COPY . /app/ 10 | EXPOSE 8000 11 | CMD ./bin/start_production.sh -------------------------------------------------------------------------------- /backend/apps/home/tests.py: -------------------------------------------------------------------------------- 1 | import time 2 | from apps.testutils import SeleniumTestCase 3 | 4 | class SampleTests(SeleniumTestCase): 5 | 6 | def test_something(self): 7 | self.driver.get(self.live_server_url) 8 | p = self.driver.find_element_by_css_selector('p') 9 | self.assertEqual(p.text, 'hello world') -------------------------------------------------------------------------------- /backend/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | bind = "0.0.0.0:8000" 4 | workers = multiprocessing.cpu_count() * 2 + 1 5 | errorlog = '/tmp/logs/gunicorn/access.log' 6 | loglevel = 'info' 7 | accesslog = '/tmp/logs/gunicorn/error.log' 8 | 9 | secure_scheme_headers = {'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'} -------------------------------------------------------------------------------- /bin/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # create a db backups to backups/ 3 | 4 | source bin/env.sh 5 | 6 | BACKUP_FILE=django$1_$(date +'%Y_%m_%dT%H_%M_%S').bak 7 | docker-compose -f docker-compose.yml -f $DOCKER_CONFIG_PROD -f docker-compose.db.yml run --rm dbclient pg_dump -Fc -h db -U $DB_USER -d $DB_NAME -f /backups/$BACKUP_FILE 8 | echo "backup $BACKUP_FILE created" -------------------------------------------------------------------------------- /backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project 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.9/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", "conf.settings_prod") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /nginx/setupconf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd /etc/nginx 3 | rm nginx.conf 4 | if ls ssl/*.crt &> /dev/null; then 5 | mv nginx_ssl.conf nginx.conf 6 | CRT=$(ls ssl/*.crt | head -n 1) 7 | KEY=$(ls ssl/*.key | head -n 1) 8 | echo "using ssl, key=${KEY} crt=${CRT}" 9 | sed -i "s#ssl/server.crt#$CRT#g" /etc/nginx/nginx.conf 10 | sed -i "s#ssl/server.key#$KEY#g" /etc/nginx/nginx.conf 11 | else 12 | echo "not using ssl" 13 | mv /etc/nginx/nginx_nossl.conf /etc/nginx/nginx.conf 14 | fi -------------------------------------------------------------------------------- /frontend/src/js/index.js: -------------------------------------------------------------------------------- 1 | import '../style/index.styl'; 2 | import hamster_img_url from '../images/hamster.jpg'; 3 | 4 | //import npm packages, node modules, other files here 5 | 6 | window.addEventListener('load', function() { 7 | const p = document.createElement('p'); 8 | p.innerHTML = 'hello world'; 9 | document.body.insertBefore(p, document.body.firstChild); 10 | 11 | const img = document.createElement('img'); 12 | img.src = hamster_img_url; 13 | document.body.insertBefore(img, document.body.firstChild); 14 | }); -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #builds production images, 4 | #initializes database or creates db backup if it was initialized already 5 | #(re)starts prodcution containers 6 | 7 | source bin/env.sh 8 | 9 | #for init_db.sh 10 | export DOCKER_INIT_DB_CONFIG=$DOCKER_CONFIG_PROD 11 | 12 | if [ $(docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG ps | grep dbdata | wc -l) == 0 ]; then 13 | ./bin/init_db.sh 14 | else 15 | ./bin/backup.sh 16 | fi 17 | ./bin/build_production.sh 18 | ./bin/stop_production.sh 19 | ./bin/start_production.sh -------------------------------------------------------------------------------- /backend/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block "title" %}My application{% endblock %} 9 | {% if not WEBPACK_DEV_SERVER %} 10 | 11 | {% endif %} 12 | 13 | 14 | {% block "content" %}{% endblock %} 15 | 16 | {% if WEBPACK_DEV_SERVER %} 17 | 18 | {% else %} 19 | 20 | {% endif %} 21 | 22 | -------------------------------------------------------------------------------- /backend/apps/testutils.py: -------------------------------------------------------------------------------- 1 | from django.test import LiveServerTestCase 2 | from selenium import webdriver 3 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 4 | 5 | class SeleniumTestCase(LiveServerTestCase): 6 | 7 | @classmethod 8 | def setUpClass(cls): 9 | super(SeleniumTestCase, cls).setUpClass() 10 | cls.driver = webdriver.Remote( 11 | command_executor='http://127.0.0.1:4444/wd/hub', 12 | desired_capabilities=DesiredCapabilities.CHROME) 13 | cls.driver.implicitly_wait(10) 14 | 15 | @classmethod 16 | def tearDownClass(cls): 17 | cls.driver.quit() 18 | super(SeleniumTestCase, cls).tearDownClass() -------------------------------------------------------------------------------- /backend/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | import os.path 3 | import json 4 | 5 | static_assets = None 6 | 7 | if not settings.WEBPACK_DEV_SERVER: 8 | assets_json_fname = os.path.join(settings.STATIC_ASSETS_JSON) 9 | try: 10 | with open(assets_json_fname, 'r') as f: 11 | static_assets = json.load(f)['main'] 12 | except Exception: 13 | raise Exception('failed to read %s. was frontend built?' % assets_json_fname) 14 | 15 | 16 | #use by base.html to figure out what statics to load 17 | def static_resources(request): 18 | return { 19 | "WEBPACK_DEV_SERVER": settings.WEBPACK_DEV_SERVER, 20 | "static_assets": static_assets 21 | } -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | django: 4 | build: ./backend 5 | volumes: 6 | - ./backend:/app 7 | - ./backend/media:/media 8 | - ./frontend/dist:/static 9 | - ./frontend/dist/assets.json:/app/assets.json 10 | command: ./bin/test.sh 11 | ports: 12 | - "8082:8082" 13 | environment: 14 | DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_TEST} 15 | network_mode: "service:selenium" 16 | depends_on: 17 | - db 18 | - selenium 19 | 20 | selenium: 21 | image: selenium/standalone-chrome-debug:2.53.0 22 | ports: 23 | - 5900:5900 -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | ports: 5 | - "9432:5432" 6 | frontend: 7 | image: node:5.10 8 | working_dir: /frontend 9 | command: npm run-script develop 10 | volumes: 11 | - ./frontend:/frontend 12 | ports: 13 | - "3000:3000" 14 | django: 15 | build: ./backend 16 | volumes: 17 | - ./backend:/app 18 | - ./backend/media:/media 19 | - ./frontend/dist:/static 20 | command: ./bin/develop.sh 21 | ports: 22 | - "8000:8000" 23 | environment: 24 | DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_DEV} 25 | depends_on: 26 | - db -------------------------------------------------------------------------------- /frontend/webpack/dev-server.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import WebpackDevServer from 'webpack-dev-server'; 3 | import config from './webpack.config.development'; 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | filename: config.output.filename, 8 | inline: true, 9 | hot: true, 10 | stats: false, 11 | historyApiFallback: true, 12 | headers: { 13 | 'Access-Control-Allow-Origin': 'http://localhost:8000', 14 | 'Access-Control-Allow-Headers': 'X-Requested-With' 15 | } 16 | }).listen(3000, '0.0.0.0', function (err) { 17 | if (err) { 18 | console.error(err); 19 | } else { 20 | console.log('webpack dev server listening on localhost:3000'); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /backend/conf/settings_prod.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | 4 | ALLOWED_HOSTS = ['*'] 5 | 6 | DEBUG = False 7 | TEMPLATE_DEBUG = False 8 | WEBPACK_DEV_SERVER = False 9 | 10 | STATIC_ASSETS_JSON = os.path.join(BASE_DIR, 'assets.json') 11 | 12 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 13 | 14 | LOGGING = { 15 | 'version': 1, 16 | 'disable_existing_loggers': False, 17 | 'handlers': { 18 | 'applogfile': { 19 | 'level': 'DEBUG', 20 | 'class': 'logging.FileHandler', 21 | 'filename': '/tmp/logs/app/app.log', 22 | }, 23 | }, 24 | 'loggers': { 25 | 'django': { 26 | 'handlers': ['applogfile'], 27 | 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), 28 | }, 29 | }, 30 | } -------------------------------------------------------------------------------- /frontend/webpack/webpack.config.development.js: -------------------------------------------------------------------------------- 1 | import base_config from './webpack.config.base'; 2 | import webpack from 'webpack'; 3 | 4 | export default { 5 | ...base_config, 6 | debug: true, 7 | output: { 8 | ...base_config.output, 9 | publicPath: 'http://localhost:3000' + base_config.output.publicPath, 10 | filename: 'bundle.js' 11 | }, 12 | entry: [ 13 | 'webpack-dev-server/client?http://0.0.0.0:3000', 14 | 'webpack/hot/dev-server', 15 | ...base_config.entry 16 | ], 17 | plugins: [ 18 | ...base_config.plugins, 19 | new webpack.HotModuleReplacementPlugin(), 20 | new webpack.DefinePlugin({ 21 | '__DEV__': true, 22 | 'process.env': JSON.stringify('development') 23 | }) 24 | ] 25 | }; -------------------------------------------------------------------------------- /bin/env.sh: -------------------------------------------------------------------------------- 1 | export DOCKER_CONFIG_PROD=${DOCKER_CONFIG_PROD:-docker-compose.production.yml} 2 | export DOCKER_CONFIG_DEV=${DOCKER_CONFIG_DEV:-docker-compose.override.yml} 3 | export DOCKER_CONFIG_TEST=${DOCKER_CONFIG_TEST:-docker-compose.test.yml} 4 | 5 | export DB_USER=${DB_USER:-django} 6 | export DB_NAME=${DB_NAME:-django} 7 | 8 | export DJANGO_SETTINGS_DEV=${DJANGO_SETTINGS_DEV:-conf.settings} 9 | export DJANGO_SETTINGS_PROD=${DJANGO_SETTINGS_MODULE:-conf.settings_prod} 10 | export DJANGO_SETTINGS_TEST=${DJANGO_SETTINGS_TEST:-conf.settings_test} 11 | 12 | dcdev() { 13 | docker-compose -f docker-compose.yml -f $DOCKER_CONFIG_DEV "$@" 14 | } 15 | 16 | dcprod() { 17 | docker-compose -f docker-compose.yml -f $DOCKER_CONFIG_PROD "$@" 18 | } 19 | 20 | dctest() { 21 | docker-compose -f docker-compose.yml -f $DOCKER_CONFIG_TEST "$@" 22 | } -------------------------------------------------------------------------------- /backend/urls.py: -------------------------------------------------------------------------------- 1 | """project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', admin.site.urls), 21 | url(r'^', include('apps.home.urls')) 22 | ] 23 | -------------------------------------------------------------------------------- /docker-compose.production.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | volumes: 5 | - "./logs/postgres:/var/log/postgresql" 6 | django: 7 | build: 8 | context: ./backend 9 | dockerfile: Dockerfile-production 10 | environment: 11 | DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_PROD} 12 | volumes: 13 | - "./logs/app:/tmp/logs/app" 14 | - "./logs/gunicorn:/tmp/logs/gunicorn" 15 | - "./backend/media:/media" 16 | depends_on: 17 | - db 18 | nginx: 19 | build: 20 | context: ./ 21 | dockerfile: ./nginx/Dockerfile 22 | depends_on: 23 | - django 24 | volumes: 25 | - "./backend/media:/media" 26 | - "./logs/nginx:/tmp/logs" 27 | ports: 28 | - "80:80" 29 | - "443:443" 30 | 31 | -------------------------------------------------------------------------------- /bin/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # restore a bacakup. arg is a filename that exitsts in backups dir 3 | 4 | source bin/env.sh 5 | 6 | BACKUP_FILE=$1 7 | 8 | if ! [ -f $BACKUP_FILE ]; then 9 | echo "file $BACKUP_FILE not found" 10 | exit 1 11 | fi 12 | 13 | if [ $(docker-compose -f docker-compose.yml -f $DOCKER_CONFIG_PROD -f docker-compose.db.yml ps | grep "_db_" | grep "Up" | wc -l) != 0 ]; then 14 | echo "database container running. please stop before trying to restore" 15 | exit 1 16 | fi 17 | 18 | echo "dropping database..." 19 | dcprod -f docker-compose.db.yml run --rm dbclient dropdb -h db -U $DB_USER $DB_NAME 20 | echo "creating database..." 21 | dcprod -f docker-compose.db.yml run --rm dbclient createdb -h db -U $DB_USER -O $DB_USER $DB_NAME 22 | echo "restoring database..." 23 | dcprod -f docker-compose.db.yml run --rm dbclient pg_restore -Fc -h db -U $DB_USER -d $DB_NAME $BACKUP_FILE 24 | echo "restore complete" -------------------------------------------------------------------------------- /frontend/webpack/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | 3 | export default { 4 | entry: [ 5 | '/frontend/src/js/index.js' 6 | ], 7 | devtool: 'source-map', 8 | output: { 9 | path: '/frontend/dist/', 10 | publicPath: '/static/', 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel', 18 | }, 19 | { 20 | test: /\.css$/, 21 | loader: 'style!css' 22 | }, 23 | { 24 | test: /\.styl$/, 25 | loader: 'style!css!stylus' 26 | }, 27 | { 28 | test: /\.(mp4|webm|mp3|ogg|wav|jpeg|jpg|bmp|ico|png|gif|ttf|otf|woff|eot)$/, 29 | loader: 'file?name=[path][name].[ext]?[hash]' 30 | } 31 | ] 32 | }, 33 | target: 'web', 34 | plugins: [ 35 | new webpack.NoErrorsPlugin() 36 | ] 37 | }; -------------------------------------------------------------------------------- /bin/init_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #initialize database if it hasn't been initialized yet 4 | 5 | DOCKER_INIT_DB_CONFIG=${DOCKER_INIT_DB_CONFIG:-$DOCKER_CONFIG_DEV} 6 | 7 | echo "inti db config: $DOCKER_INIT_DB_CONFIG" 8 | 9 | if [ $(docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG ps | grep db | wc -l) == 0 ]; then 10 | echo "initializing database" 11 | docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG up -d db 12 | DB_CONTAINER_ID=$(docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG ps -q db) 13 | for i in {30..0}; do 14 | echo "waiting for postgres to finish initializing..." 15 | if [ $(docker logs $DB_CONTAINER_ID 2>&1 | grep "database system is ready to accept connections" | wc -l) == 1 ]; then 16 | docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG stop db 17 | echo "db initialized" 18 | exit 0 19 | else 20 | sleep 1 21 | fi 22 | done 23 | if [ "$i" = 0 ]; then 24 | echo >&2 "db init failed" 25 | exit 1 26 | fi 27 | fi 28 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev-server": "babel-node webpack/dev-server.js", 8 | "npminstall": "npm i -q", 9 | "develop": "npm run npminstall && npm run dev-server", 10 | "build": "babel-node ./node_modules/webpack/bin/webpack --config webpack/webpack.config.production.js --progress --profile --color", 11 | "cleandist": "rm -rf /dist/*", 12 | "build-prod": "npm run npminstall && npm run cleandist && npm run build" 13 | }, 14 | "author": "domasx2@gmail.com", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "assets-webpack-plugin": "^3.4.0", 18 | "babel-cli": "^6.6.5", 19 | "babel-loader": "^6.2.4", 20 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 21 | "babel-preset-es2015": "^6.6.0", 22 | "css-loader": "^0.23.1", 23 | "extract-text-webpack-plugin": "^1.0.1", 24 | "file-loader": "^0.8.5", 25 | "style-loader": "^0.13.1", 26 | "stylus": "^0.54.2", 27 | "stylus-loader": "^2.0.0", 28 | "webpack": "^1.12.14", 29 | "webpack-dev-server": "^1.14.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/webpack/webpack.config.production.js: -------------------------------------------------------------------------------- 1 | import base_config from './webpack.config.base'; 2 | import webpack from 'webpack'; 3 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 4 | import AssetsWebpackPlugin from 'assets-webpack-plugin'; 5 | 6 | export default { 7 | ...base_config, 8 | 9 | output: { 10 | ...base_config.output, 11 | filename: '[hash]_bundle.js' 12 | }, 13 | 14 | module: { 15 | ...base_config.module, 16 | 17 | //wrap style loaders with extract text plugin 18 | loaders: base_config.module.loaders.map(function(conf) { 19 | return { 20 | ...conf, 21 | loader: conf.loader && conf.loader.includes('style!') ? ExtractTextPlugin.extract('style', conf.loader.replace('style!', '')) : conf.loader 22 | } 23 | }) 24 | }, 25 | 26 | plugins: [ 27 | new webpack.optimize.OccurenceOrderPlugin(), 28 | new webpack.DefinePlugin({ 29 | '__DEV__': false, 30 | 'process.env.NODE_ENV': JSON.stringify('production'), 31 | 'process.env': JSON.stringify('production') 32 | }), 33 | new webpack.optimize.UglifyJsPlugin({ 34 | compressor: { 35 | screw_ie8: true, 36 | warnings: false 37 | } 38 | }), 39 | new ExtractTextPlugin("[hash]_styles.css"), 40 | new AssetsWebpackPlugin({filename: 'dist/assets.json'}) 41 | ] 42 | }; -------------------------------------------------------------------------------- /nginx/nginx_nossl.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | user nobody nogroup; 4 | pid /tmp/nginx.pid; 5 | error_log /tmp/logs/nginx.error.log; 6 | 7 | events { 8 | worker_connections 1024; 9 | accept_mutex off; 10 | } 11 | 12 | http { 13 | include mime.types; 14 | default_type application/octet-stream; 15 | access_log /tmp/logs/nginx.access.log combined; 16 | sendfile on; 17 | 18 | server { 19 | listen 80 default; 20 | client_max_body_size 4G; 21 | server_name _; 22 | 23 | 24 | gzip on; 25 | gzip_disable "msie6"; 26 | gzip_vary on; 27 | gzip_proxied any; 28 | gzip_comp_level 6; 29 | gzip_buffers 16 8k; 30 | gzip_http_version 1.1; 31 | gzip_types application/javascript text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 32 | 33 | keepalive_timeout 5; 34 | 35 | location /static/ { 36 | alias /static/; 37 | } 38 | 39 | location /media/ { 40 | alias /media/; 41 | } 42 | 43 | location / { 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | proxy_set_header Host $http_host; 46 | 47 | # UNCOMMENT LINE BELOW IF THIS IS BEHIND A SSL PROXY 48 | #proxy_set_header X-Forwarded-Proto https; 49 | 50 | proxy_redirect off; 51 | proxy_pass http://django:8000; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /nginx/nginx_ssl.conf: -------------------------------------------------------------------------------- 1 | # to use ssl, add your cert and key to conf/ssl, 2 | # replace cert/key names with real names below, 3 | # and replace nginx.conf with this 4 | 5 | worker_processes 1; 6 | 7 | user nobody nogroup; 8 | pid /tmp/nginx.pid; 9 | error_log /tmp/logs/nginx.error.log; 10 | 11 | events { 12 | worker_connections 1024; 13 | accept_mutex off; 14 | } 15 | 16 | http { 17 | include "/etc/nginx/mime.types"; 18 | default_type application/octet-stream; 19 | sendfile on; 20 | 21 | server { 22 | listen 80 default; 23 | return 301 https://$host$request_uri; 24 | } 25 | 26 | server { 27 | listen 443; 28 | client_max_body_size 4G; 29 | server_name _; 30 | 31 | access_log /tmp/logs/nginx.access-ssl.log combined; 32 | 33 | ssl on; 34 | 35 | #rename with your key/cert here 36 | ssl_certificate /etc/nginx/ssl/server.crt; 37 | ssl_certificate_key /etc/nginx/ssl/server.key; 38 | 39 | ssl_prefer_server_ciphers On; 40 | ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; 41 | ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS; 42 | 43 | gzip on; 44 | gzip_disable "msie6"; 45 | gzip_vary on; 46 | gzip_proxied any; 47 | gzip_comp_level 6; 48 | gzip_buffers 16 8k; 49 | gzip_http_version 1.1; 50 | gzip_types application/javascript text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 51 | 52 | keepalive_timeout 5; 53 | 54 | location /static/ { 55 | alias /static/; 56 | } 57 | 58 | location /media/ { 59 | alias /media/; 60 | } 61 | 62 | location / { 63 | proxy_set_header Host $http_host; 64 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 65 | proxy_set_header X-Forwarded-Proto https; 66 | proxy_set_header X-Real-IP $remote_addr; 67 | proxy_set_header X-Scheme $scheme; 68 | proxy_redirect off; 69 | proxy_pass http://django:8000; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /backend/conf/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '^h85uo#*&q+5r=a1_l8i_&t$^$(sgsz-3*ijq8%22ayrbapxyk' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | #load frontend from webpack dev server rather than static 29 | WEBPACK_DEV_SERVER = True 30 | 31 | ALLOWED_HOSTS = [] 32 | 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = [ 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | 44 | 'apps.home' 45 | ] 46 | 47 | MIDDLEWARE_CLASSES = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': ['/app/templates/'], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | 'context_processors.static_resources' 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 86 | 'NAME': 'django', 87 | 'USER': 'django', 88 | 'HOST': 'db', 89 | 'PORT': 5432, 90 | } 91 | } 92 | 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 110 | }, 111 | ] 112 | 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 116 | 117 | LANGUAGE_CODE = 'en-us' 118 | 119 | TIME_ZONE = 'UTC' 120 | 121 | USE_I18N = True 122 | 123 | USE_L10N = True 124 | 125 | USE_TZ = True 126 | 127 | 128 | # Static files (CSS, JavaScript, Images) 129 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 130 | 131 | STATIC_URL = '/static/' 132 | STATIC_ROOT = '/static/' 133 | 134 | STATIC_ASSETS_JSON = os.path.join(STATIC_ROOT, 'assets.json') 135 | 136 | # user uploads 137 | MEDIA_URL = '/media/' 138 | MEDIA_ROOT = '/media/' 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-django-webpack-starter 2 | 3 | This is a starter project for a django app with webpack built frontend that uses docker for dev enironment. 4 | Docker and docker-compose is all you need to develop, build & deploy, run development or production mode with a single command. 5 | 6 | ## stack 7 | python 3.5 8 | node 5.10 9 | Postgres 9.5 10 | Django 1.10.5 11 | Webpack 12 | Stylus 13 | Nginx 14 | Gunicorn 15 | 16 | 17 | ## get started 18 | 19 | Get latest docker & docker-compose: 20 | https://www.docker.com/ 21 | https://docs.docker.com/compose/ 22 | 23 | Pull seed to your project: 24 | ```sh 25 | git init 26 | git remote add starter https://github.com/domasx2/docker-django-webpack-starter.git 27 | git pull starter master 28 | ``` 29 | 30 | Start dev server: 31 | ```sh 32 | ./bin/develop.sh 33 | ``` 34 | Wait for docker to set up container, then open [http://localhost:8000](http://localhost:8000) 35 | 36 | ## production mode 37 | 38 | ```sh 39 | # build production images, create db backup & start 40 | ./bin/deploy.sh 41 | 42 | # stop server 43 | ./bin/stop_production.sh 44 | 45 | # start srever 46 | ./bin/start_production.sh 47 | ``` 48 | 49 | In prod mode sources are added to docker image rather than mounted from host. Nginx serves static files, proxy pass to gunicorn for django app. Logs in `logs` dir. 50 | 51 | #### enable ssl 52 | Copy your .key and .crt files to `nginx/ssl` and run `./bin/deploy.sh`. 53 | 54 | ## install dependencies 55 | ```sh 56 | # frontend 57 | ./bin/npm.sh install [package] --save-dev 58 | 59 | # backend 60 | ./bin/pipinstall.sh [pacakge] #will also add entry to backend/requirements.txt 61 | ``` 62 | 63 | ## backup & restore database 64 | 65 | ```sh 66 | # create a backup in backups dir 67 | ./bin/backup.sh 68 | 69 | # restore from a backup in backups dir (server must be stopped) 70 | ./bin/restore.sh backups/somebackup.bak 71 | ``` 72 | 73 | ## run django management commands 74 | ```sh 75 | #dev mode 76 | ./bin/django.sh [command] 77 | 78 | #create migration 79 | ./bin/django.sh makemigrations myapp 80 | 81 | #prod mode 82 | ./bin/django_prod.sh [command] 83 | 84 | #start django shell in prod mode 85 | ./bin/django_prod.sh shell 86 | 87 | ``` 88 | 89 | ## layout 90 | 91 | ``` 92 | bin/ - various utility scripts 93 | 94 | docker-compose.yml - base docker compose config 95 | docker-compose.overrides.yml - development docker compose config 96 | docker-compose.production.yml - production docker compose config 97 | 98 | frontend/ - frontend stuff 99 | frontend/package.json - npm package file with frotnend dependencies 100 | frontend/src/js/ - javascript code 101 | frontend/src/js/index.js - js entry point. include other js deps here 102 | frontend/src/style/ - stylesheets 103 | frontend/src/style/index.styl - stylesheet entry point. include other styl files here 104 | 105 | backend/ - backend stuff 106 | backend/apps/ - django apps 107 | backend/conf/ - django settings files 108 | backend/conf/settings.py - default config 109 | backend/conf/settings_prod.py - production config 110 | backend/templates/ - django global templates 111 | backend/requirements.txt - python dependencies 112 | backend/gunicorn.conf.py - gunicorn conf for production 113 | backend/media/ - user uploads 114 | 115 | logs/ - in prod mode app, gunicorn, nginx, postgres logs go here 116 | nginx/ - nginx stuff for prod mode 117 | nginx/ssl/ - put key & cert here if you use ssl 118 | nginx/nginx_nossl.conf - nginx conf if no ssl is used 119 | nginx/nginx_ssl.conf - nginx conf for deploy with ssl 120 | ``` 121 | 122 | 123 | ## tests 124 | 125 | For e2e tests, use `app.testutils.SeleniumTestCase` class it comes with chrome driver configure at `self.driver`. See `apps.home.tests` for an example. 126 | See http://selenium-python.readthedocs.io/ for selenium driver api 127 | 128 | ```sh 129 | 130 | #run tests 131 | ./bin/test.sh 132 | 133 | # skip frontend build (eg, running tests repeatedly) 134 | ./bin/test.sh --skipbuild 135 | 136 | 137 | To debug tests it's possible to vnc into selenium container while its running at localhost:5900 and view the browser. Password is `secret`. 138 | 139 | ```sh 140 | sudo apt-get install vinagre # vnc client 141 | 142 | vinagre localhost:5900 143 | ``` --------------------------------------------------------------------------------