Skip to content

Commit

Permalink
Docker deployment with Nginx+Gunicorn example. manage.py script. Mult…
Browse files Browse the repository at this point in the history
…iple config files. More Flask extensions.
  • Loading branch information
MaxHalford committed Jul 19, 2016
1 parent 65ea777 commit 80c124c
Show file tree
Hide file tree
Showing 29 changed files with 398 additions and 166 deletions.
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.db
*.log
*.pyc
.git/
config.py
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.pyc
*.db
*webassets*
*.log
*.pyc
config.py
12 changes: 12 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[FORMAT]
max-line-length=100
indent-string=' '

[REPORTS]
files-output=no
reports=yes
evaluation=10 - ((float(5 * error + warning + refactor + convention) / statement) * 10)

[TYPECHECK]
ignored-modules=flask_sqlalchemy
ignored-classes=SQLObject,SQLAlchemy,Base
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
FROM phusion/baseimage:0.9.19

# Use baseimage-docker's init system.
CMD ["/sbin/my_init"]

ENV TERM=xterm-256color

# Set the locale
RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

# Install necessary packages
RUN apt-get update && apt-get install -y \
build-essential \
python3-pip

# Install Python requirements
RUN mkdir -p /usr/src/app
COPY requirements.txt /usr/src/app/
RUN pip3 install --upgrade pip
RUN pip3 install -r /usr/src/app/requirements.txt

# Copy the files from the host to the container
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN chmod 777 -R *

# Clean up
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Makefile

## Configuration

BUILD_TIME := $(shell date +%FT%T%z)
PROJECT := $(shell basename $(PWD))


## Install dependencies
.PHONY: install
install:
pip install -r requirements.txt

## Setup developpement environment
.PHONY: dev
dev:
cd app && ln -sf config_dev.py config.py

## Setup production environment
.PHONY: prod
prod:
cd app && ln -sf config_prod.py config.py
60 changes: 39 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ I didn't really like the Flask starter projects I found searching the web. I rea
- [ ] Static file bundling, automatic SCSS to CSS conversion and automatic minifying.
- [ ] Websockets (for example for live chatting)
- [x] Virtual environment example.
- [ ] Heroku deployment example.
- [x] Digital Ocean deployment example.
- [ ] Tests.
- [ ] Logging.
- [x] Logging.
- [ ] Language selection.
- [ ] Automatic API views.
- [ ] API key generator.
Expand All @@ -40,6 +39,10 @@ If you have any suggestions or want to help, feel free to drop me a line at <max
- [itsdangerous](http://pythonhosted.org/itsdangerous/) for generating random tokens for the confirmation emails.
- [Flask-Bcrypt](https://flask-bcrypt.readthedocs.org/en/latest/) for generating secret user passwords.
- [Flask-Admin](https://flask-admin.readthedocs.org/en/latest/) for building an administration interface.
- [Flask-Script](https://flask-script.readthedocs.io/en/latest/) for managing the app.
- [structlog](http://structlog.readthedocs.io/en/stable/) for logging.
- [Flask-DebugToolBar](https://flask-debugtoolbar.readthedocs.io/en/latest/) for adding a performance toolbar in development.
- [gunicorn](http://gunicorn.org/) for acting as a reverse-proxy for Nginx.

### Frontend

Expand All @@ -48,55 +51,70 @@ If you have any suggestions or want to help, feel free to drop me a line at <max

## Structure

I did what most people recommend for the application's structure. Basically, everything is contained in the ``app/`` folder.
I did what most people recommend for the application's structure. Basically, everything is contained in the `app/` folder.

- There you have the classic ``static/`` and ``templates/`` folders. The ``templates/`` folder contains macros, error views and a common layout.
- I added a ``views/`` folder to separate the user and the website logic, which could be extended to the the admin views.
- The same goes for the ``forms/`` folder, as the project grows it will be useful to split the WTForms code into separate files.
- The ``models.py`` script contains the SQLAlchemy code, for the while it only contains the logic for a ``users`` table.
- The ``toolbox/`` folder is a personal choice, in it I keep all the other code the application will need.
- There you have the classic `static/` and `templates/` folders. The `templates/` folder contains macros, error views and a common layout.
- I added a `views/` folder to separate the user and the website logic, which could be extended to the the admin views.
- The same goes for the `forms/` folder, as the project grows it will be useful to split the WTForms code into separate files.
- The `models.py` script contains the SQLAlchemy code, for the while it only contains the logic for a `users` table.
- The `toolbox/` folder is a personal choice, in it I keep all the other code the application will need.
- Management commands should be included in `manage.py`. Enter `python manage.py -?` to get a list of existing commands.
- I added a Makefile for setup tasks, it can be quite useful once a project grows.


## Setup

### Vanilla

- Install the required libraries.
- Install the requirements and setup the development environment.

``pip install -r requirements.txt``
`make install && make dev`

- Create the database.

``python createdb.py``
`python manage.py initdb`

- Run the application.

``python run.py``
`python manage.py runserver`

- Navigate to ``localhost:5000``.
- Navigate to `localhost:5000`.


### Virtual environment

```
``
pip install virtualenv
virtualenv venv
venv/bin/activate (venv\scripts\activate on Windows)
pip install -r requirements.txt
python createdb.py
python run.py
```
make install
make dev
python manage.py initdb
python manage.py runserver
``


## Deployment

- Heroku
- [Digital Ocean](deployment/Digital-Ocean.md)
The current application can be deployed with Docker [in a few commands](https://realpython.com/blog/python/dockerizing-flask-with-compose-and-machine-from-localhost-to-the-cloud/).

```sh
cd ~/path/to/application/
docker-machine create -d virtualbox --virtualbox-memory 512 --virtualbox-cpu-count 1 dev
docker-machine env dev
eval "$(docker-machine env dev)"
docker-compose build
docker-compose up -d
docker-compose run web make dev
docker-compose run web python3 manage.py initdb
```

Then access the IP address given by `docker-machine ip dev` et voilà. This is exactly how [OpenBikes's API is being deployed](https://github.com/OpenBikes/api.openbikes.co).


## Configuration

The goal is to keep most of the application's configuration in a single file called ``config.py``. The one I have included is basic and yet it covers most of the important stuff.
The goal is to keep most of the application's configuration in a single file called `config.py`. I added a `config_dev.py` and a `config_prod.py` who inherit from `config_common.py`. The trick is to symlink either of these to `config.py`. This is done in by running `make dev` or `make prod`.

I have included a working Gmail account to confirm user email addresses and reset user passwords, although in production you should't include the file if you push to GitHub because people can see it. The same goes for API keys, you should keep them secret. You can read more about secret configuration files [here](https://exploreflask.com/configuration.html).

Expand Down
6 changes: 0 additions & 6 deletions app.wsgi

This file was deleted.

38 changes: 11 additions & 27 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
app = Flask(__name__)

# Setup the app with the config.py file
app.config.from_object('config')
app.config.from_object('app.config')

# Setup the logger
from app.logger_setup import logger

# Setup the database
from flask.ext.sqlalchemy import SQLAlchemy
Expand All @@ -13,6 +16,12 @@
from flask.ext.mail import Mail
mail = Mail(app)

# Setup the debug toolbar
from flask_debugtoolbar import DebugToolbarExtension
app.config['DEBUG_TB_TEMPLATE_EDITOR_ENABLED'] = True
app.config['DEBUG_TB_PROFILER_ENABLED'] = True
toolbar = DebugToolbarExtension(app)

# Setup the password crypting
from flask.ext.bcrypt import Bcrypt
bcrypt = Bcrypt(app)
Expand All @@ -34,29 +43,4 @@
def load_user(email):
return User.query.filter(User.email == email).first()

# Setup the admin interface
from flask import request, Response
from werkzeug.exceptions import HTTPException
from flask_admin import Admin
from flask.ext.admin.contrib.sqla import ModelView
from flask.ext.login import LoginManager
from flask.ext.admin.contrib.fileadmin import FileAdmin
import os.path as op

admin = Admin(app, name='Admin', template_mode='bootstrap3')

class ModelView(ModelView):

def is_accessible(self):
auth = request.authorization or request.environ.get('REMOTE_USER') # workaround for Apache
if not auth or (auth.username, auth.password) != app.config['ADMIN_CREDENTIALS']:
raise HTTPException('', Response('You have to an administrator.', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'}
))
return True

# Users
admin.add_view(ModelView(User, db.session))
# Static files
path = op.join(op.dirname(__file__), 'static')
admin.add_view(FileAdmin(path, '/static/', name='Static'))
from app import admin
30 changes: 30 additions & 0 deletions app/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os.path as op

from flask import request, Response
from werkzeug.exceptions import HTTPException
from flask_admin import Admin
from flask.ext.admin.contrib.sqla import ModelView
from flask.ext.admin.contrib.fileadmin import FileAdmin

from app import app, db
from app.models import User


admin = Admin(app, name='Admin', template_mode='bootstrap3')

class ModelView(ModelView):

def is_accessible(self):
auth = request.authorization or request.environ.get('REMOTE_USER') # workaround for Apache
if not auth or (auth.username, auth.password) != app.config['ADMIN_CREDENTIALS']:
raise HTTPException('', Response('You have to an administrator.', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'}
))
return True

# Users
admin.add_view(ModelView(User, db.session))

# Static files
path = op.join(op.dirname(__file__), 'static')
admin.add_view(FileAdmin(path, '/static/', name='Static'))
8 changes: 6 additions & 2 deletions config.py → app/config_common.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# DEBUG has to be to False in a production enrironment for security reasons
DEBUG = True
TIMEZONE = 'Europe/Paris'

# Secret key for generating tokens
SECRET_KEY = 'houdini'

# Admin credentials
ADMIN_CREDENTIALS = ('admin', 'pa$$word')

# Database choice
SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True

# Configuration of a Gmail account for sending mails
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 465
Expand All @@ -15,5 +18,6 @@
MAIL_USERNAME = 'flask.boilerplate'
MAIL_PASSWORD = 'flaskboilerplate123'
ADMINS = ['[email protected]']

# Number of times a password is hashed
BCRYPT_LOG_ROUNDS = 12
32 changes: 32 additions & 0 deletions app/config_dev.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging

from app.config_common import *


# DEBUG can only be set to True in a development environment for security reasons
DEBUG = True

# Secret key for generating tokens
SECRET_KEY = 'houdini'

# Admin credentials
ADMIN_CREDENTIALS = ('admin', 'pa$$word')

# Database choice
SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True

# Configuration of a Gmail account for sending mails
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_USERNAME = 'flask.boilerplate'
MAIL_PASSWORD = 'flaskboilerplate123'
ADMINS = ['[email protected]']

# Number of times a password is hashed
BCRYPT_LOG_ROUNDS = 12

LOG_LEVEL = logging.DEBUG
LOG_FILENAME = 'activity.log'
32 changes: 32 additions & 0 deletions app/config_prod.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging

from app.config_common import *


# DEBUG has to be to False in a production environment for security reasons
DEBUG = False

# Secret key for generating tokens
SECRET_KEY = 'houdini'

# Admin credentials
ADMIN_CREDENTIALS = ('admin', 'pa$$word')

# Database choice
SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True

# Configuration of a Gmail account for sending mails
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_USERNAME = 'flask.boilerplate'
MAIL_PASSWORD = 'flaskboilerplate123'
ADMINS = ['[email protected]']

# Number of times a password is hashed
BCRYPT_LOG_ROUNDS = 12

LOG_LEVEL = logging.INFO
LOG_FILENAME = 'activity.log'
4 changes: 2 additions & 2 deletions app/forms/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ class SignUp(Form):

''' User sign up form. '''

name = TextField(validators=[Required(), Length(min=2)],
first_name = TextField(validators=[Required(), Length(min=2)],
description='Name')
surname = TextField(validators=[Required(), Length(min=2)],
last_name = TextField(validators=[Required(), Length(min=2)],
description='Surname')
phone = TextField(validators=[Required(), Length(min=6)],
description='Phone number')
Expand Down
Loading

0 comments on commit 80c124c

Please sign in to comment.