Selaa lähdekoodia

Docker deployment with Nginx+Gunicorn example. manage.py script. Multiple config files. More Flask extensions.

MaxHalford 9 vuotta sitten
vanhempi
commit
80c124cc30

+ 5 - 0
.dockerignore

@@ -0,0 +1,5 @@
+*.db
+*.log
+*.pyc
+.git/
+config.py

+ 3 - 2
.gitignore

@@ -1,3 +1,4 @@
-*.pyc
 *.db
-*webassets*
+*.log
+*.pyc
+config.py

+ 12 - 0
.pylintrc

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

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

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

+ 39 - 21
README.md

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

+ 0 - 6
app.wsgi

@@ -1,6 +0,0 @@
-import sys, os, logging
-logging.basicConfig(stream=sys.stderr)
-sys.path.insert(0, '/var/www/Flask-Boilerplate')
-os.chdir('/var/www/Flask-Boilerplate')
-from run import app
-application = app

+ 11 - 27
app/__init__.py

@@ -3,7 +3,10 @@ from flask import Flask
 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
@@ -13,6 +16,12 @@ db = SQLAlchemy(app)
 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)
@@ -34,29 +43,4 @@ login_manager.login_view = 'userbp.signin'
 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 - 0
app/admin.py

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

+ 6 - 2
config.py → app/config_common.py

@@ -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
@@ -15,5 +18,6 @@ MAIL_USE_SSL = True
 MAIL_USERNAME = 'flask.boilerplate'
 MAIL_PASSWORD = 'flaskboilerplate123'
 ADMINS = ['flask.boilerplate@gmail.com']
+
 # Number of times a password is hashed
 BCRYPT_LOG_ROUNDS = 12

+ 32 - 0
app/config_dev.py

@@ -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 = ['flask.boilerplate@gmail.com']
+
+# Number of times a password is hashed
+BCRYPT_LOG_ROUNDS = 12
+
+LOG_LEVEL = logging.DEBUG
+LOG_FILENAME = 'activity.log'

+ 32 - 0
app/config_prod.py

@@ -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 = ['flask.boilerplate@gmail.com']
+
+# Number of times a password is hashed
+BCRYPT_LOG_ROUNDS = 12
+
+LOG_LEVEL = logging.INFO
+LOG_FILENAME = 'activity.log'

+ 2 - 2
app/forms/user.py

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

+ 75 - 0
app/logger_setup.py

@@ -0,0 +1,75 @@
+'''
+logger_setup.py customizes the app's logging module. Each time an event is
+logged the logger checks the level of the event (eg. debug, warning, info...).
+If the event is above the approved threshold then it goes through. The handlers
+do the same thing; they output to a file/shell if the event level is above their
+threshold.
+:Example:
+        >>> from website import logger
+        >>> logger.info('event', foo='bar')
+**Levels**:
+        - logger.debug('For debugging purposes')
+        - logger.info('An event occured, for example a database update')
+        - logger.warning('Rare situation')
+        - logger.error('Something went wrong')
+        - logger.critical('Very very bad')
+You can build a log incrementally as so:
+        >>> log = logger.new(date='now')
+        >>> log = log.bind(weather='rainy')
+        >>> log.info('user logged in', user='John')
+'''
+
+import datetime as dt
+import logging
+from logging.handlers import RotatingFileHandler
+import pytz
+
+from flask import request, session
+from structlog import wrap_logger
+from structlog.processors import JSONRenderer
+
+from app import app
+
+# Set the logging level
+app.logger.setLevel(app.config['LOG_LEVEL'])
+
+# Remove the stdout handler
+app.logger.removeHandler(app.logger.handlers[0])
+
+TZ = pytz.timezone(app.config['TIMEZONE'])
+
+def add_fields(_, level, event_dict):
+    ''' Add custom fields to each record. '''
+    now = dt.datetime.now()
+    event_dict['timestamp'] = TZ.localize(now, True).astimezone(pytz.utc).isoformat()
+    event_dict['level'] = level
+
+    if session:
+        event_dict['session_id'] = session.get('session_id')
+
+    if request:
+        try:
+            event_dict['ip_address'] = request.headers['X-Forwarded-For'].split(',')[0].strip()
+        except:
+            event_dict['ip_address'] = 'unknown'
+
+    return event_dict
+
+# Add a handler to write log messages to a file
+if app.config.get('LOG_FILE'):
+    file_handler = RotatingFileHandler(app.config['LOG_FILENAME'],
+                                       app.config['LOG_MAXBYTES'],
+                                       app.config['LOG_BACKUPS'],
+                                       'a',
+                                       encoding='utf-8')
+    file_handler.setLevel(logging.DEBUG)
+    app.logger.addHandler(file_handler)
+
+# Wrap the application logger with structlog to format the output
+logger = wrap_logger(
+    app.logger,
+    processors=[
+        add_fields,
+        JSONRenderer(indent=None)
+    ]
+)

+ 10 - 4
app/models.py

@@ -1,20 +1,26 @@
-from app import db, bcrypt
 from sqlalchemy.ext.hybrid import hybrid_property
 from flask.ext.login import UserMixin
 
+from app import db, bcrypt
+
 
 class User(db.Model, UserMixin):
 
-    ''' A website user. '''
+    ''' A user who has an account on the website. '''
 
     __tablename__ = 'users'
-    name = db.Column(db.String)
-    surname = db.Column(db.String)
+
+    first_name = db.Column(db.String)
+    last_name = db.Column(db.String)
     phone = db.Column(db.String)
     email = db.Column(db.String, primary_key=True)
     confirmation = db.Column(db.Boolean)
     _password = db.Column(db.String)
 
+    @property
+    def full_name(self):
+        return '{} {}'.format(self.first_name, self.last_name)
+
     @hybrid_property
     def password(self):
         return self._password

+ 5 - 6
app/templates/layout.html

@@ -1,8 +1,7 @@
-<!-- HTML macros -->
-{% import "macros.html" as m %}
 <!DOCTYPE html>
+{% import "macros.html" as m %}
 <html lang="en">
-	<div id="container">
+	<body id="container">
    		<div id="header">
    			{% block head %}
 				<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no'/>
@@ -31,7 +30,7 @@
 				 		<!-- User is logged in -->
 				      	{% if current_user.is_authenticated %}
 				      		<div class="item">
-				        		<a class="ui primary button" href="/user/account">{{ current_user.name }} {{ current_user.surname }}</a>
+				        		<a class="ui primary button" href="/user/account">{{ current_user.full_name }}</a>
 				    		</div>
 				    		<div class="item">
 				        		<a class="ui default button" href="/user/signout">Sign out</a>
@@ -74,5 +73,5 @@
 	    		</div>
 	  		</div>
 		</div>
-	</div>
-</html>
+	</body>
+</html>

+ 2 - 2
app/templates/user/account.html

@@ -6,6 +6,6 @@
 
 {% block content %}
 
-    <h1 class="ui header">Hi {{ current_user.name }} {{ current_user.surname }}!</h1>
+    <h1 class="ui header">Hi {{ current_user.full_name }}!</h1>
 
-{% endblock %}
+{% endblock %}

+ 3 - 3
app/templates/user/signup.html

@@ -5,8 +5,8 @@
 	<div class="ui form-user center raised very padded text container segment">
 		<form class="ui form" action="/user/signup" method="post">
 			<h4 class="ui dividing header">Sign up</h4>
-	    	{{ m.render_field(form.name) }}
-	    	{{ m.render_field(form.surname) }}
+	    	{{ m.render_field(form.first_name) }}
+	    	{{ m.render_field(form.last_name) }}
 	    	{{ m.render_field(form.phone) }}
 	    	{{ m.render_field(form.email) }}
 	    	{{ m.render_field(form.password) }}
@@ -16,4 +16,4 @@
 		</form>
 	</div>
 
-{% endblock %}
+{% endblock %}

+ 2 - 2
app/views/user.py

@@ -19,8 +19,8 @@ def signup():
     if form.validate_on_submit():
         # Create a user who hasn't validated his email address
         user = models.User(
-            name=form.name.data,
-            surname=form.surname.data,
+            first_name=form.first_name.data,
+            last_name=form.last_name.data,
             phone=form.phone.data,
             email=form.email.data,
             confirmation=False,

+ 0 - 3
createdb.py

@@ -1,3 +0,0 @@
-from app import db
-from app import models
-db.create_all()

+ 0 - 58
deployment/Digital-Ocean.md

@@ -1,58 +0,0 @@
-# Digital Ocean deployment
-
-The following script sets everything up so that the application can be deployed on a [Digital Ocean](https://www.digitalocean.com/) droplet with Ubuntu 14.04 and the Apache web server. Of course you should go through it and modify the parts that are unique to your application (the username and the application GitHub URL/folder). You will also have to change the ``app.wsgi`` and the ``Flask-Boilerplate.conf`` files, just open the files and replace the obvious stuff.
-
-```sh
-# Login and change password
-ssh root@<IP>
-
-# Add a new user "MAX" and give him a password
-adduser MAX
-# Give "MAX" sudo rights
-gpasswd -a MAX sudo
-
-# Change "PermitRootLogin yes" to "PermitRootLogin no"
-nano /etc/ssh/sshd_config
-
-# Restart SSH
-service ssh restart
-
-# Switch to user "MAX"
-sudo su MAX
-
-# Configure the timezone
-sudo dpkg-reconfigure tzdata
-# Configure NTP Synchronization
-sudo apt-get update
-sudo apt-get install ntp
-# Create a Swap File
-sudo fallocate -l 4G /swapfile
-sudo chmod 600 /swapfile
-sudo mkswap /swapfile
-sudo swapon /swapfile
-sudo sh -c 'echo "/swapfile none swap sw 0 0" >> /etc/fstab'
-
-# Setup Python and Apache
-sudo apt-get update
-sudo apt-get install apache2
-sudo apt-get install python3-pip python3-dev libapache2-mod-wsgi-py3
-
-# Clone the repository containing the code
-cd /var/www
-sudo apt-get install git
-sudo git clone https://github.com/MaxHalford/Flask-Boilerplate
-sudo chmod -R 777 Flask-Boilerplate
-cd Flask-Boilerplate
-
-# Install the necessary Python libraries (takes some time)
-sudo pip3 install -r setup/requirements.txt
-
-# Configure and enable a virtual host
-sudo cp deployment/Flask-Boilerplate.conf /etc/apache2/sites-available/
-sudo a2ensite Flask-Boilerplate
-sudo service apache2 reload
-sudo service apache2 restart
-
-# Reboot the server and you should be done!
-sudo reboot
-```

+ 0 - 17
deployment/Flask-Boilerplate.conf

@@ -1,17 +0,0 @@
-<VirtualHost *:80>
-		ServerName openbikes.co
-		ServerAdmin maxhalford25@gmail.com
-		WSGIScriptAlias / /var/www/Flask-Boilerplate/app.wsgi
-		<Directory /var/www/OpenBikes/>
-			Order allow,deny
-			Allow from all
-		</Directory>
-		Alias /static /var/www/Flask-Boilerplate/static
-		<Directory /var/www/Flask-Boilerplate/static/>
-			Order allow,deny
-			Allow from all
-		</Directory>
-		ErrorLog ${APACHE_LOG_DIR}/error.log
-		LogLevel warn
-		CustomLog ${APACHE_LOG_DIR}/access.log combined
-</VirtualHost>

+ 0 - 0
deployment/Heroku.md


+ 14 - 0
docker-compose.yml

@@ -0,0 +1,14 @@
+web:
+  restart: always
+  build: ./
+  expose:
+    - "8000"
+  command: /usr/local/bin/gunicorn -w 2 -b :8000 app:app
+
+nginx:
+  restart: always
+  build: ./nginx/
+  ports:
+    - "80:80"
+  links:
+    - web:web

+ 33 - 0
manage.py

@@ -0,0 +1,33 @@
+from flask.ext.script import Manager, prompt_bool, Shell, Server
+from termcolor import colored
+
+from app import app, db, models
+
+
+manager = Manager(app)
+
+
+def make_shell_context():
+    return dict(app=app)
+
+
+@manager.command
+def initdb():
+    ''' Create the SQL database. '''
+    db.create_all()
+    print(colored('The SQL database has been created', 'green'))
+
+
+@manager.command
+def dropdb():
+    ''' Delete the SQL database. '''
+    if prompt_bool('Are you sure you want to lose all your SQL data?'):
+        db.drop_all()
+        print(colored('The SQL database has been deleted', 'green'))
+
+
+manager.add_command('runserver', Server())
+manager.add_command('shell', Shell(make_context=make_shell_context))
+
+if __name__ == '__main__':
+    manager.run()

+ 3 - 0
nginx/Dockerfile

@@ -0,0 +1,3 @@
+FROM tutum/nginx
+RUN rm /etc/nginx/sites-enabled/default
+ADD sites-enabled/ /etc/nginx/sites-enabled

+ 13 - 0
nginx/sites-enabled/api.openbikes.co

@@ -0,0 +1,13 @@
+server {
+
+    listen 80;
+    server_name openbikes.co;
+    charset utf-8;
+
+    location / {
+        proxy_pass http://web:8000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+}

+ 13 - 9
requirements.txt

@@ -1,9 +1,13 @@
-flask
-flask-sqlalchemy
-flask-wtf
-wtforms
-flask-login
-flask-mail
-itsdangerous
-flask-bcrypt
-flask-admin
+Flask==0.10.1
+Flask-Admin==1.3.0
+Flask-Bcrypt==0.7.1
+Flask-DebugToolbar==0.10.0
+Flask-Login==0.3.2
+Flask-Mail==0.9.1
+Flask-Script==2.0.5
+Flask-SQLAlchemy==2.1
+Flask-WTF==0.12
+gunicorn==19.4.5
+itsdangerous==0.24
+termcolor==1.1.0
+WTForms==2.1

+ 0 - 2
run.py

@@ -1,2 +0,0 @@
-from app import app
-app.run()