Implement docker environment

master
James Curtin 6 years ago
parent 4f40e61d84
commit 0ed150b02a
  1. 6
      cookiecutter.json
  2. 1
      tasks.py
  3. 7
      {{cookiecutter.app_name}}/.env.example
  4. 5
      {{cookiecutter.app_name}}/.travis.yml
  5. 57
      {{cookiecutter.app_name}}/Dockerfile
  6. 12
      {{cookiecutter.app_name}}/Pipfile
  7. 28
      {{cookiecutter.app_name}}/README.rst
  8. 54
      {{cookiecutter.app_name}}/docker-compose.yml
  9. 6
      {{cookiecutter.app_name}}/package.json
  10. 1
      {{cookiecutter.app_name}}/requirements/dev.txt
  11. 2
      {{cookiecutter.app_name}}/requirements/prod.txt
  12. 7
      {{cookiecutter.app_name}}/shell_scripts/auto_pipenv.sh
  13. 15
      {{cookiecutter.app_name}}/shell_scripts/supervisord_entrypoint.sh
  14. 21
      {{cookiecutter.app_name}}/supervisord.conf
  15. 18
      {{cookiecutter.app_name}}/supervisord_programs/gunicorn.conf
  16. 2
      {{cookiecutter.app_name}}/webpack.config.js

@ -3,7 +3,9 @@
"email": "sloria1@gmail.com", "email": "sloria1@gmail.com",
"github_username": "sloria", "github_username": "sloria",
"project_name": "My Flask App", "project_name": "My Flask App",
"app_name": "myflaskapp", "app_name": "{{cookiecutter.project_name.lower().replace('-', '_').replace(' ', '_')}}",
"project_short_description": "A flasky app.", "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"]
} }

@ -12,6 +12,7 @@ HERE = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(HERE, 'cookiecutter.json'), 'r') as fp: with open(os.path.join(HERE, 'cookiecutter.json'), 'r') as fp:
COOKIECUTTER_SETTINGS = json.load(fp) COOKIECUTTER_SETTINGS = json.load(fp)
# Match default value of app_name from cookiecutter.json # 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']) COOKIE = os.path.join(HERE, COOKIECUTTER_SETTINGS['app_name'])
REQUIREMENTS = os.path.join(COOKIE, 'requirements', 'dev.txt') REQUIREMENTS = os.path.join(COOKIE, 'requirements', 'dev.txt')

@ -1,5 +1,8 @@
# Environment variable overrides for local development # Environment variable overrides for local development
FLASK_APP=autoapp.py FLASK_APP=autoapp.py
FLASK_DEBUG=1
FLASK_ENV=development FLASK_ENV=development
DATABASE_URL="sqlite:////tmp/dev.db" DATABASE_URL=sqlite:////tmp/dev.db
SECRET_KEY="not-so-secret" GUNICORN_WORKERS=1
LOG_LEVEL=debug
SECRET_KEY=not-so-secret

@ -7,10 +7,11 @@ python:
- 3.4 - 3.4
- 3.5 - 3.5
- 3.6 - 3.6
- 3.7
install: install:
- pip install -r requirements/dev.txt - pip install -r requirements/dev.txt
- nvm install 6.10 - nvm install {{cookiecutter.node_version}}
- nvm use 6.10 - nvm use {{cookiecutter.node_version}}
- npm install - npm install
before_script: before_script:
- npm run lint - npm run lint

@ -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" ]

@ -14,7 +14,7 @@ click = ">=5.0"
# Database # Database
Flask-SQLAlchemy = "==2.4.0" Flask-SQLAlchemy = "==2.4.0"
psycopg2 = "==2.8.2" psycopg2-binary = "==2.8.2"
SQLAlchemy = "==1.3.4" SQLAlchemy = "==1.3.4"
# Migrations # Migrations
@ -25,7 +25,9 @@ Flask-WTF = "==0.14.2"
WTForms = "==2.2.1" WTForms = "==2.2.1"
# Deployment # Deployment
gevent = "==1.4.0"
gunicorn = ">=19.1.1" gunicorn = ">=19.1.1"
supervisor = "==4.0.2"
# Webpack # Webpack
flask-webpack = "==0.1.0" flask-webpack = "==0.1.0"
@ -47,7 +49,12 @@ environs = "==4.1.3"
# Testing # Testing
pytest = "==4.5.0" pytest = "==4.5.0"
WebTest = "==2.0.33" WebTest = "==2.0.33"
<<<<<<< HEAD
factory-boy = "==2.12.*" factory-boy = "==2.12.*"
=======
factory-boy = "==2.11.*"
pdbpp = "==0.10.0"
>>>>>>> ae5c375... Implement docker environment
# Lint and code style # Lint and code style
flake8 = "==3.7.7" flake8 = "==3.7.7"
@ -58,3 +65,6 @@ flake8-isort = "==2.7.0"
flake8-quotes = "==2.0.1" flake8-quotes = "==2.0.1"
isort = "==4.3.20" isort = "==4.3.20"
pep8-naming = "==0.8.2" pep8-naming = "==0.8.2"
[requires]
python_version = "{{cookiecutter.python_version}}"

@ -81,6 +81,34 @@ To apply the migration.
For a full migration command reference, run ``flask db --help``. 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 Asset Management
---------------- ----------------

@ -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:

@ -5,8 +5,8 @@
"scripts": { "scripts": {
"build": "NODE_ENV=production webpack --progress --colors -p", "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\"", "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", "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", "flask-server": "{% if cookiecutter.use_pipenv == 'yes' %}pipenv run {% endif %}flask run --host=0.0.0.0",
"lint": "eslint \"assets/js/*.js\"" "lint": "eslint \"assets/js/*.js\""
}, },
"repository": { "repository": {
@ -15,7 +15,7 @@
}, },
"author": "{{cookiecutter.full_name}}", "author": "{{cookiecutter.full_name}}",
"license": "MIT", "license": "MIT",
"engines": { "node" : ">=4" }, "engines": { "node" : ">={{cookiecutter.node_version}}" },
"bugs": { "bugs": {
"url": "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.app_name}}/issues" "url": "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.app_name}}/issues"
}, },

@ -5,6 +5,7 @@
pytest==3.7.4 pytest==3.7.4
WebTest==2.0.30 WebTest==2.0.30
factory-boy==2.11.1 factory-boy==2.11.1
pdbpp==0.10.0
# Lint and code style # Lint and code style
flake8==3.5.0 flake8==3.5.0

@ -21,7 +21,9 @@ Flask-WTF==0.14.2
WTForms==2.2.1 WTForms==2.2.1
# Deployment # Deployment
gevent==1.4.0
gunicorn>=19.1.1 gunicorn>=19.1.1
supervisor==4.0.2
# Webpack # Webpack
flask-webpack==0.1.0 flask-webpack==0.1.0

@ -0,0 +1,7 @@
#!/usr/bin/env sh
function auto_pipenv_shell {
if [ -f "Pipfile" ] ; then
source "$(pipenv --venv)/bin/activate"
fi
}

@ -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 "$@"

@ -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

@ -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

@ -11,7 +11,7 @@ const ManifestRevisionPlugin = require('manifest-revision-webpack-plugin');
const debug = (process.env.NODE_ENV !== 'production'); const debug = (process.env.NODE_ENV !== 'production');
// Development asset host (webpack dev server) // 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'); const rootAssetPath = path.join(__dirname, 'assets');

Loading…
Cancel
Save