From 0ed150b02a53024577d8c2d94d912204ef90cb9f Mon Sep 17 00:00:00 2001
From: James Curtin <jameswcurtin@gmail.com>
Date: Tue, 21 May 2019 14:27:41 -0400
Subject: [PATCH] Implement docker environment

---
 cookiecutter.json                             |  6 +-
 tasks.py                                      |  1 +
 {{cookiecutter.app_name}}/.env.example        |  7 ++-
 {{cookiecutter.app_name}}/.travis.yml         |  5 +-
 {{cookiecutter.app_name}}/Dockerfile          | 57 +++++++++++++++++++
 {{cookiecutter.app_name}}/Pipfile             | 12 +++-
 {{cookiecutter.app_name}}/README.rst          | 28 +++++++++
 {{cookiecutter.app_name}}/docker-compose.yml  | 54 ++++++++++++++++++
 {{cookiecutter.app_name}}/package.json        |  6 +-
 .../requirements/dev.txt                      |  1 +
 .../requirements/prod.txt                     |  2 +
 .../shell_scripts/auto_pipenv.sh              |  7 +++
 .../shell_scripts/supervisord_entrypoint.sh   | 15 +++++
 {{cookiecutter.app_name}}/supervisord.conf    | 21 +++++++
 .../supervisord_programs/gunicorn.conf        | 18 ++++++
 {{cookiecutter.app_name}}/webpack.config.js   |  2 +-
 16 files changed, 231 insertions(+), 11 deletions(-)
 create mode 100644 {{cookiecutter.app_name}}/Dockerfile
 create mode 100644 {{cookiecutter.app_name}}/docker-compose.yml
 create mode 100644 {{cookiecutter.app_name}}/shell_scripts/auto_pipenv.sh
 create mode 100644 {{cookiecutter.app_name}}/shell_scripts/supervisord_entrypoint.sh
 create mode 100644 {{cookiecutter.app_name}}/supervisord.conf
 create mode 100644 {{cookiecutter.app_name}}/supervisord_programs/gunicorn.conf

diff --git a/cookiecutter.json b/cookiecutter.json
index c4a689fa..600313b3 100644
--- a/cookiecutter.json
+++ b/cookiecutter.json
@@ -3,7 +3,9 @@
 	"email": "sloria1@gmail.com",
 	"github_username": "sloria",
 	"project_name": "My Flask App",
-	"app_name": "myflaskapp",
+	"app_name": "{{cookiecutter.project_name.lower().replace('-', '_').replace(' ', '_')}}",
 	"project_short_description": "A flasky app.",
-	"use_pipenv": ["no", "yes"]
+	"use_pipenv": ["no", "yes"],
+	"python_version": ["3.7", "3.6", "3.5"],
+	"node_version": ["12", "11", "10", "8"]
 }
diff --git a/tasks.py b/tasks.py
index 9d8d5dfb..0a57d8cd 100644
--- a/tasks.py
+++ b/tasks.py
@@ -12,6 +12,7 @@ HERE = os.path.abspath(os.path.dirname(__file__))
 with open(os.path.join(HERE, 'cookiecutter.json'), 'r') as fp:
     COOKIECUTTER_SETTINGS = json.load(fp)
 # Match default value of app_name from cookiecutter.json
+COOKIECUTTER_SETTINGS["app_name"] = 'my_flask_app'
 COOKIE = os.path.join(HERE, COOKIECUTTER_SETTINGS['app_name'])
 REQUIREMENTS = os.path.join(COOKIE, 'requirements', 'dev.txt')
 
diff --git a/{{cookiecutter.app_name}}/.env.example b/{{cookiecutter.app_name}}/.env.example
index 72367469..3ab1c437 100644
--- a/{{cookiecutter.app_name}}/.env.example
+++ b/{{cookiecutter.app_name}}/.env.example
@@ -1,5 +1,8 @@
 # Environment variable overrides for local development
 FLASK_APP=autoapp.py
+FLASK_DEBUG=1
 FLASK_ENV=development
-DATABASE_URL="sqlite:////tmp/dev.db"
-SECRET_KEY="not-so-secret"
+DATABASE_URL=sqlite:////tmp/dev.db
+GUNICORN_WORKERS=1
+LOG_LEVEL=debug
+SECRET_KEY=not-so-secret
diff --git a/{{cookiecutter.app_name}}/.travis.yml b/{{cookiecutter.app_name}}/.travis.yml
index 64deca9b..6e25e707 100644
--- a/{{cookiecutter.app_name}}/.travis.yml
+++ b/{{cookiecutter.app_name}}/.travis.yml
@@ -7,10 +7,11 @@ python:
   - 3.4
   - 3.5
   - 3.6
+  - 3.7
 install:
   - pip install -r requirements/dev.txt
-  - nvm install 6.10
-  - nvm use 6.10
+  - nvm install {{cookiecutter.node_version}}
+  - nvm use {{cookiecutter.node_version}}
   - npm install
 before_script:
   - npm run lint
diff --git a/{{cookiecutter.app_name}}/Dockerfile b/{{cookiecutter.app_name}}/Dockerfile
new file mode 100644
index 00000000..1d6572a7
--- /dev/null
+++ b/{{cookiecutter.app_name}}/Dockerfile
@@ -0,0 +1,57 @@
+# ==================================== BASE ====================================
+ARG INSTALL_PYTHON_VERSION=${INSTALL_PYTHON_VERSION:-3.7}
+FROM python:${INSTALL_PYTHON_VERSION}-slim-stretch AS base
+
+RUN apt-get update
+RUN apt-get install -y \
+    curl
+
+ARG INSTALL_NODE_VERSION=${INSTALL_NODE_VERSION:-12}
+RUN curl -sL https://deb.nodesource.com/setup_${INSTALL_NODE_VERSION}.x | bash -
+RUN apt-get install -y \
+    nodejs \
+    && apt-get -y autoclean
+
+WORKDIR /app
+{%- if cookiecutter.use_pipenv == "yes" %}
+COPY ["Pipfile", "shell_scripts/auto_pipenv.sh", "./"]
+RUN pip install pipenv
+{%- else %}
+COPY requirements requirements
+{%- endif %}
+
+COPY [ "assets", "package.json", "webpack.config.js", "./" ]
+RUN npm install
+
+# ================================= DEVELOPMENT ================================
+FROM base AS development
+{%- if cookiecutter.use_pipenv == "yes" %}
+RUN pipenv install --dev
+{%- else %}
+RUN pip install -r requirements/dev.txt
+{%- endif %}
+EXPOSE 2992
+EXPOSE 5000
+CMD [ {% if cookiecutter.use_pipenv == 'yes' %}"pipenv", "run", {% endif %}"npm", "start" ]
+
+# ================================= PRODUCTION =================================
+FROM base AS production
+{%- if cookiecutter.use_pipenv == "yes" %}
+RUN pipenv install
+{%- else %}
+RUN pip install -r requirements/prod.txt
+{%- endif %}
+COPY supervisord.conf /etc/supervisor/supervisord.conf
+COPY supervisord_programs /etc/supervisor/conf.d
+EXPOSE 5000
+ENTRYPOINT ["/bin/bash", "shell_scripts/supervisord_entrypoint.sh"]
+CMD ["-c", "/etc/supervisor/supervisord.conf"]
+
+# =================================== MANAGE ===================================
+FROM base AS manage
+{%- if cookiecutter.use_pipenv == "yes" %}
+COPY --from=development /root/.local/share/virtualenvs/ /root/.local/share/virtualenvs/
+{%- else %}
+RUN pip install -r requirements/dev.txt
+{%- endif %}
+ENTRYPOINT [ {% if cookiecutter.use_pipenv == 'yes' %}"pipenv", "run", {% endif %}"flask" ]
diff --git a/{{cookiecutter.app_name}}/Pipfile b/{{cookiecutter.app_name}}/Pipfile
index 342880c2..26eb0bac 100644
--- a/{{cookiecutter.app_name}}/Pipfile
+++ b/{{cookiecutter.app_name}}/Pipfile
@@ -14,7 +14,7 @@ click = ">=5.0"
 
 # Database
 Flask-SQLAlchemy = "==2.4.0"
-psycopg2 = "==2.8.2"
+psycopg2-binary = "==2.8.2"
 SQLAlchemy = "==1.3.4"
 
 # Migrations
@@ -25,7 +25,9 @@ Flask-WTF = "==0.14.2"
 WTForms = "==2.2.1"
 
 # Deployment
+gevent = "==1.4.0"
 gunicorn = ">=19.1.1"
+supervisor = "==4.0.2"
 
 # Webpack
 flask-webpack = "==0.1.0"
@@ -47,7 +49,12 @@ environs = "==4.1.3"
 # Testing
 pytest = "==4.5.0"
 WebTest = "==2.0.33"
+<<<<<<< HEAD
 factory-boy = "==2.12.*"
+=======
+factory-boy = "==2.11.*"
+pdbpp = "==0.10.0"
+>>>>>>> ae5c375... Implement docker environment
 
 # Lint and code style
 flake8 = "==3.7.7"
@@ -58,3 +65,6 @@ flake8-isort = "==2.7.0"
 flake8-quotes = "==2.0.1"
 isort = "==4.3.20"
 pep8-naming = "==0.8.2"
+
+[requires]
+python_version = "{{cookiecutter.python_version}}"
diff --git a/{{cookiecutter.app_name}}/README.rst b/{{cookiecutter.app_name}}/README.rst
index 0a895306..ce9b0e40 100644
--- a/{{cookiecutter.app_name}}/README.rst
+++ b/{{cookiecutter.app_name}}/README.rst
@@ -81,6 +81,34 @@ To apply the migration.
 For a full migration command reference, run ``flask db --help``.
 
 
+Docker
+------
+
+This app can be run completely using ``Docker`` and ``docker-compose``. Before starting, make sure to create a new copy of ``.env.example`` called ``.env``. You will need to start the development version of the app at least once before running other Docker commands, as starting the dev app bootstraps a necessary file, ``webpack/manifest.json``.
+
+There are three main services:
+
+To run the development version of the app ::
+
+    docker-compose up flask-dev
+
+To run the production version of the app ::
+
+    docker-compose up flask-prod
+
+The list of ``environment:`` variables in the ``docker-compose.yml`` file takes precedence over any variables specified in ``.env``.
+
+To run any commands using the ``Flask CLI`` ::
+
+    docker-compose run --rm manage <<COMMAND>>
+
+Therefore, to initialize a database you would run ::
+
+    docker-compose run --rm manage db init
+
+A docker volume ``node-modules`` is created to store NPM packages and is reused across the dev and prod versions of the application. For the purposes of DB testing with ``sqlite``, the file ``dev.db`` is mounted to all containers. This volume mount should be removed from ``docker-compose.yml`` if a production DB server is used.
+
+
 Asset Management
 ----------------
 
diff --git a/{{cookiecutter.app_name}}/docker-compose.yml b/{{cookiecutter.app_name}}/docker-compose.yml
new file mode 100644
index 00000000..bce64afa
--- /dev/null
+++ b/{{cookiecutter.app_name}}/docker-compose.yml
@@ -0,0 +1,54 @@
+version: '3.6'
+
+x-build-args: &build_args
+  INSTALL_PYTHON_VERSION: {{cookiecutter.python_version}}
+  INSTALL_NODE_VERSION: {{cookiecutter.node_version}}
+
+x-default-volumes: &default_volumes
+  volumes:
+    - ./:/app
+    - node-modules:/app/node_modules
+    - ./dev.db:/tmp/dev.db
+
+services:
+  flask-dev:
+    build:
+      context: .
+      target: development
+      args:
+        <<: *build_args
+    image: "{{cookiecutter.app_name}}-development"
+    ports:
+      - "5000:5000"
+      - "2992:2992"
+    <<: *default_volumes
+
+  flask-prod:
+    build:
+      context: .
+      target: production
+      args:
+        <<: *build_args
+    image: "{{cookiecutter.app_name}}-production"
+    ports:
+      - "5000:5000"
+    environment:
+      FLASK_ENV: production
+      FLASK_DEBUG: 0
+      LOG_LEVEL: info
+      GUNICORN_WORKERS: 4
+    <<: *default_volumes
+
+  manage:
+    build:
+      context: .
+      target: manage
+    image: "{{cookiecutter.app_name}}-manage"
+    stdin_open: true
+    tty: true
+    <<: *default_volumes
+
+volumes:
+  node-modules:
+  static-build:
+  dev-db:
diff --git a/{{cookiecutter.app_name}}/package.json b/{{cookiecutter.app_name}}/package.json
index 600a9e5c..33cabedf 100644
--- a/{{cookiecutter.app_name}}/package.json
+++ b/{{cookiecutter.app_name}}/package.json
@@ -5,8 +5,8 @@
   "scripts": {
     "build": "NODE_ENV=production webpack --progress --colors -p",
     "start": "concurrently -n \"WEBPACK,FLASK\" -c \"bgBlue.bold,bgMagenta.bold\" \"npm run webpack-dev-server\" \"npm run flask-server\"",
-    "webpack-dev-server": "NODE_ENV=debug webpack-dev-server --port 2992 --hot --inline",
-    "flask-server": "{% if cookiecutter.use_pipenv == 'yes' %}pipenv run {% endif %}flask run",
+    "webpack-dev-server": "NODE_ENV=debug webpack-dev-server --host=0.0.0.0 --port 2992 --hot --inline",
+    "flask-server": "{% if cookiecutter.use_pipenv == 'yes' %}pipenv run {% endif %}flask run --host=0.0.0.0",
     "lint": "eslint \"assets/js/*.js\""
   },
   "repository": {
@@ -15,7 +15,7 @@
   },
   "author": "{{cookiecutter.full_name}}",
   "license": "MIT",
-  "engines": { "node" : ">=4" },
+  "engines": { "node" : ">={{cookiecutter.node_version}}" },
   "bugs": {
     "url": "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.app_name}}/issues"
   },
diff --git a/{{cookiecutter.app_name}}/requirements/dev.txt b/{{cookiecutter.app_name}}/requirements/dev.txt
index 8c26898a..3417eb94 100644
--- a/{{cookiecutter.app_name}}/requirements/dev.txt
+++ b/{{cookiecutter.app_name}}/requirements/dev.txt
@@ -5,6 +5,7 @@
 pytest==3.7.4
 WebTest==2.0.30
 factory-boy==2.11.1
+pdbpp==0.10.0
 
 # Lint and code style
 flake8==3.5.0
diff --git a/{{cookiecutter.app_name}}/requirements/prod.txt b/{{cookiecutter.app_name}}/requirements/prod.txt
index 9fae9b94..69ede966 100644
--- a/{{cookiecutter.app_name}}/requirements/prod.txt
+++ b/{{cookiecutter.app_name}}/requirements/prod.txt
@@ -21,7 +21,9 @@ Flask-WTF==0.14.2
 WTForms==2.2.1
 
 # Deployment
+gevent==1.4.0
 gunicorn>=19.1.1
+supervisor==4.0.2
 
 # Webpack
 flask-webpack==0.1.0
diff --git a/{{cookiecutter.app_name}}/shell_scripts/auto_pipenv.sh b/{{cookiecutter.app_name}}/shell_scripts/auto_pipenv.sh
new file mode 100644
index 00000000..ec3e964e
--- /dev/null
+++ b/{{cookiecutter.app_name}}/shell_scripts/auto_pipenv.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env sh
+
+function auto_pipenv_shell {
+    if [ -f "Pipfile" ] ; then
+        source "$(pipenv --venv)/bin/activate"
+    fi
+}
diff --git a/{{cookiecutter.app_name}}/shell_scripts/supervisord_entrypoint.sh b/{{cookiecutter.app_name}}/shell_scripts/supervisord_entrypoint.sh
new file mode 100644
index 00000000..494e1e88
--- /dev/null
+++ b/{{cookiecutter.app_name}}/shell_scripts/supervisord_entrypoint.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env sh
+set -e
+
+npm run build
+
+{%- if cookiecutter.use_pipenv == "yes" %}
+source ./shell_scripts/auto_pipenv.sh
+auto_pipenv_shell
+{%- endif %}
+
+if [ $# -eq 0 ] || [ "${1#-}" != "$1" ]; then
+  set -- supervisord "$@"
+fi
+
+exec "$@"
diff --git a/{{cookiecutter.app_name}}/supervisord.conf b/{{cookiecutter.app_name}}/supervisord.conf
new file mode 100644
index 00000000..adf2d5c4
--- /dev/null
+++ b/{{cookiecutter.app_name}}/supervisord.conf
@@ -0,0 +1,21 @@
+[unix_http_server]
+file=/tmp/supervisor.sock                       ; path to your socket file
+
+[supervisord]
+logfile=/tmp/supervisord.log                    ; supervisord log file
+logfile_maxbytes=50MB                           ; maximum size of logfile before rotation
+logfile_backups=10                              ; number of backed up logfiles
+loglevel=%(ENV_LOG_LEVEL)s                      ; info, debug, warn, trace
+pidfile=/tmp/supervisord.pid                    ; pidfile location
+nodaemon=true                                   ; run supervisord as a daemon
+minfds=1024                                     ; number of startup file descriptors
+minprocs=200                                    ; number of process descriptors
+
+[rpcinterface:supervisor]
+supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
+
+[supervisorctl]
+serverurl=unix:///tmp/supervisor.sock         ; use a unix:// URL for a unix socket
+
+[include]
+files = /etc/supervisor/conf.d/*.conf
diff --git a/{{cookiecutter.app_name}}/supervisord_programs/gunicorn.conf b/{{cookiecutter.app_name}}/supervisord_programs/gunicorn.conf
new file mode 100644
index 00000000..752bcbc1
--- /dev/null
+++ b/{{cookiecutter.app_name}}/supervisord_programs/gunicorn.conf
@@ -0,0 +1,18 @@
+[program:gunicorn]
+directory=/app
+command=gunicorn
+    {{cookiecutter.app_name}}.app:create_app()
+    -b :5000
+    -w %(ENV_GUNICORN_WORKERS)s
+    -k gevent
+    --max-requests=5000
+    --max-requests-jitter=500
+    --log-level=%(ENV_LOG_LEVEL)s
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+autostart=true
+autorestart=true
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
diff --git a/{{cookiecutter.app_name}}/webpack.config.js b/{{cookiecutter.app_name}}/webpack.config.js
index 7c6bd3f2..a2bff4ee 100644
--- a/{{cookiecutter.app_name}}/webpack.config.js
+++ b/{{cookiecutter.app_name}}/webpack.config.js
@@ -11,7 +11,7 @@ const ManifestRevisionPlugin = require('manifest-revision-webpack-plugin');
 const debug = (process.env.NODE_ENV !== 'production');
 
 // Development asset host (webpack dev server)
-const publicHost = debug ? 'http://localhost:2992' : '';
+const publicHost = debug ? 'http://0.0.0.0:2992' : '';
 
 const rootAssetPath = path.join(__dirname, 'assets');
 
-- 
GitLab