From 0ed150b02a53024577d8c2d94d912204ef90cb9f Mon Sep 17 00:00:00 2001 From: James Curtin 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 c4a689f..600313b 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 9d8d5df..0a57d8c 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 7236746..3ab1c43 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 64deca9..6e25e70 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 0000000..1d6572a --- /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 342880c..26eb0ba 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 0a89530..ce9b0e4 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 <> + +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 0000000..bce64af --- /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 600a9e5..33cabed 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 8c26898..3417eb9 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 9fae9b9..69ede96 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 0000000..ec3e964 --- /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 0000000..494e1e8 --- /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 0000000..adf2d5c --- /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 0000000..752bcbc --- /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 7c6bd3f..a2bff4e 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');