├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── app ├── __init__.py ├── admin │ ├── __init__.py │ └── views.py ├── app.py ├── database.py ├── extensions.py ├── manage.py ├── requirements.txt ├── run_celery.py ├── settings │ ├── __init__.py │ ├── base.py │ └── dev.py ├── tasks.py ├── templates │ ├── admin │ │ └── index.html │ ├── base.html │ ├── emails │ │ ├── confirmation.html │ │ └── recover.html │ ├── errors │ │ ├── forbidden_page.html │ │ ├── page_not_found.html │ │ └── server_error.html │ ├── index.html │ └── users │ │ ├── login.html │ │ ├── reset.html │ │ ├── reset_with_token.html │ │ ├── settings.html │ │ └── signup.html ├── tests │ ├── __init__.py │ └── test_base.py ├── users │ ├── __init__.py │ ├── decorators.py │ ├── forms.py │ ├── models.py │ └── views.py ├── uwsgi.ini └── views.py ├── docker-compose.prod.yml ├── docker-compose.yml ├── dockerfiles └── README.md ├── nginx ├── .env ├── nginx.conf └── sites-enabled │ └── flask_app.conf ├── postgresql ├── .env └── postgresql.conf ├── rabbitmq ├── .env └── rabbitmq.conf ├── redis ├── .env └── redis.conf └── supervisor ├── conf.d ├── DEVELOPMENT │ ├── celery_app.conf │ └── flask_app.conf └── PRODUCTION │ ├── celery_app.conf │ └── flask_app.conf └── supervisord.conf /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | app/migrations 3 | app/settings/prod.py 4 | app/static 5 | dockerfiles 6 | *.pyc 7 | *.md 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/migrations 2 | app/settings/prod.py 3 | app/static 4 | dockerfiles/logs 5 | dockerfiles/pgdata 6 | dockerfiles/redis 7 | dockerfiles/rabbitmq 8 | dockerfiles/mail 9 | *.pyc 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.11-slim 2 | 3 | MAINTAINER Ivan Gorbachev 4 | 5 | RUN apt-get update && apt-get install -qq -y --no-install-recommends \ 6 | build-essential \ 7 | libffi-dev \ 8 | libpq-dev \ 9 | libxml2-dev \ 10 | libxslt-dev \ 11 | && apt-get purge -y --auto-remove \ 12 | -o APT::AutoRemove::RecommendsImportant=false \ 13 | -o APT::AutoRemove::SuggestsImportant=false $buildDeps \ 14 | && apt-get clean \ 15 | && rm -rf /var/lib/apt/lists/* \ 16 | && easy_install pip 17 | 18 | COPY ./app/requirements.txt ./requirements.txt 19 | RUN pip install -U -r requirements.txt 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask-docker-compose 2 | 3 | ## About 4 | 5 | Docker-Flask is about Flask project organization and running it in a 6 | docker-compose containers. 7 | Application has a basic user model with authentication(passwords hashing), 8 | database migrations, 9 | administration interface, celery asynchronous tasks, manage script, 10 | debug toolbar, bootstrap starter templates. 11 | 12 | ## Application scheme 13 | 14 | ### Including the followings 15 | 16 | - Docker and Docker-Compose for managing project 17 | 18 | - [Flask](https://github.com/mitsuhiko/flask) application running on UWSGI 19 | with next extensions: 20 | 21 | - [Flask-Admin](https://github.com/flask-admin/flask-admin) 22 | - [Flask-Bcrypt](https://github.com/maxcountryman/flask-bcrypt) 23 | - [Flask-DebugToolbar](https://github.com/mgood/flask-debugtoolbar) 24 | - [Flask-Login](https://github.com/maxcountryman/flask-login) 25 | - [Flask-Mail](https://github.com/mattupstate/flask-mail) 26 | - [Flask-Migrate](https://github.com/miguelgrinberg/Flask-Migrate) 27 | - [Flask-Script](https://github.com/smurfix/flask-script) 28 | - [Flask-SQLAlchemy](https://github.com/mitsuhiko/flask-sqlalchemy) 29 | - [Flask-Testing](https://github.com/jarus/flask-testing) 30 | - [Flask-WTF](https://github.com/lepture/flask-wtf) 31 | 32 | 33 | - [Celery](http://www.celeryproject.org/install/) asynchronous tasks 34 | application 35 | 36 | 37 | - [Supervisor](http://supervisord.org/) initialize system for managing python 38 | applications 39 | 40 | 41 | - [PostgreSQL](http://www.postgresql.org/) object-relational database 42 | 43 | 44 | - [Nginx](http://nginx.org/) frontend web server for production 45 | mode 46 | 47 | 48 | - [Redis](http://redis.io/) key-value storage server 49 | 50 | 51 | - [RabbitMQ](http://www.rabbitmq.com/) AMPQ server(for production only) 52 | 53 | ## Pre-Build 54 | 55 | - install docker [https://github.com/docker/docker](https://github.com/docker/docker) 56 | - install docker-compose [https://docs.docker.com/compose/install](https://docs.docker.com/compose/install) 57 | 58 | ## Usage 59 | 60 | ### Pull images 61 | 62 | - ```docker-compose pull``` 63 | 64 | ### Build an image 65 | 66 | - ```docker-compose build flaskapp``` 67 | 68 | ### Start a cluster 69 | 70 | To start applications with development environment: 71 | 72 | - ```docker-compose up -d``` 73 | 74 | To start applications with production environment 75 | (first copy configuration file and edit it) 76 | 77 | - ```cp app/settings/prod.py.repl app/settings/prod.py``` 78 | - ```docker-compose --file docker-compose.prod.yml up -d``` 79 | 80 | To initialize, create migration and upgrade your database: 81 | 82 | - ```docker exec -it dockerflask_flaskapp_1 bash -c "python manage.py create_db"``` 83 | 84 | To run ipython debug flaskapp shell: 85 | 86 | - ```docker exec -it dockerflask_flaskapp_1 bash -c "python manage.py shell"``` 87 | 88 | To create admin user: 89 | 90 | - ```docker exec -it dockerflask_flaskapp_1 bash -c "python manage.py create_user -a"``` 91 | 92 | ### Migrations 93 | 94 | To initialize migrations: 95 | 96 | - ```docker exec -it dockerflask_flaskapp_1 bash -c "python manage.py db init"``` 97 | 98 | To create a migration: 99 | 100 | - ```docker exec -it dockerflask_flaskapp_1 bash -c "python manage.py db migrate"``` 101 | 102 | To upgrade your database with migration: 103 | 104 | - ```docker exec -it dockerflask_flaskapp_1 bash -c "python manage.py db upgrade"``` 105 | 106 | ### Stop and destroy a cluster 107 | 108 | - ```docker-compose stop && docker-compose rm -f``` 109 | 110 | ### Logs and troubleshooting 111 | 112 | To check standard logs: 113 | 114 | - ```docker-compose logs``` 115 | 116 | Access the application containers shell: 117 | 118 | - ```docker exec -it dockerflask_flaskapp_1 bash``` 119 | 120 | ### Running tests 121 | 122 | - ```docker exec -it dockerflask_flaskapp_1 bash -c "python manage.py runtests"``` 123 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from app import create_app 4 | -------------------------------------------------------------------------------- /app/admin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/app/admin/__init__.py -------------------------------------------------------------------------------- /app/admin/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import request, redirect, url_for 4 | from flask_admin.contrib.sqla import ModelView 5 | from flask_login import current_user 6 | 7 | 8 | class AdminUserView(ModelView): 9 | """User model view for admin.""" 10 | can_delete = False 11 | column_list = ('username', 'email', 'password', 'last_login', 'created_at') 12 | column_searchable_list = ('username', 'email') 13 | 14 | def is_accessible(self): 15 | return current_user.is_authenticated 16 | 17 | def inaccessible_callback(self, name, **kwargs): 18 | return redirect(url_for('users.login', next=request.url)) 19 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Import Stdlibs 4 | import os 5 | 6 | # Import Flask app, modules and extensions 7 | from flask import Flask, render_template 8 | from flask_admin import Admin 9 | 10 | # Import local modules 11 | from admin.views import AdminUserView 12 | from database import db 13 | from extensions import ( 14 | debug_toolbar, 15 | login_manager, 16 | mail 17 | ) 18 | from users.models import User 19 | from users.views import users as users_blueprint 20 | from views import main_blueprint 21 | 22 | # For import * 23 | __all__ = ['create_app'] 24 | 25 | DEFAULT_APP_NAME = 'flaskapp' 26 | 27 | 28 | def create_app(package_name, 29 | package_path, 30 | settings_override=None, 31 | register_security_blueprint=True): 32 | """Flask app factory.""" 33 | app = Flask(package_name, instance_relative_config=False) 34 | 35 | configure_app(app, settings_override) 36 | 37 | configure_logging(app) 38 | 39 | register_database(app) 40 | register_error_handlers(app) 41 | register_admin(app) 42 | register_extensions(app) 43 | register_blueprints(app) 44 | 45 | return app 46 | 47 | 48 | def configure_app(app, config=None): 49 | """Configure application.""" 50 | app.config.from_object('settings.base') 51 | if not app.config['TESTING']: 52 | app.config.from_envvar('FLASK_SETTINGS') 53 | else: 54 | app.config.from_object('settings.testing') 55 | 56 | 57 | def register_admin(app): 58 | """Register admin application.""" 59 | admin = Admin( 60 | app, 61 | template_mode='bootstrap3' 62 | ) 63 | admin.add_view(AdminUserView(User, db.session)) 64 | 65 | 66 | def register_database(app): 67 | """Register database.""" 68 | db.init_app(app) 69 | 70 | 71 | def register_extensions(app): 72 | """Register all extensions.""" 73 | login_manager.init_app(app) 74 | debug_toolbar.init_app(app) 75 | mail.init_app(app) 76 | 77 | 78 | def register_blueprints(app): 79 | """Register all blueprints.""" 80 | app.register_blueprint(main_blueprint) 81 | app.register_blueprint(users_blueprint, url_prefix='/users') 82 | 83 | 84 | def configure_logging(app): 85 | """Configure file(info) and email(error) logging.""" 86 | 87 | import logging 88 | from logging.handlers import SMTPHandler 89 | 90 | # Set info level on logger, which might be overwritten by handers. 91 | # Suppress DEBUG messages. 92 | app.logger.setLevel(logging.INFO) 93 | 94 | info_log = os.path.join(app.config['LOG_FOLDER'], 'info.log') 95 | info_file_handler = logging.handlers.RotatingFileHandler( 96 | info_log, maxBytes=100000, backupCount=10) 97 | info_file_handler.setLevel(logging.INFO) 98 | info_file_handler.setFormatter(logging.Formatter( 99 | '%(asctime)s %(levelname)s: %(message)s ' 100 | '[in %(pathname)s:%(lineno)d]') 101 | ) 102 | app.logger.addHandler(info_file_handler) 103 | if not app.config['DEBUG']: 104 | mail_handler = SMTPHandler(app.config['MAIL_SERVER'], 105 | app.config['MAIL_USERNAME'], 106 | app.config['ADMINS'], 107 | 'O_ops... %s failed!' % app.config[ 108 | 'PROJECT'], 109 | (app.config['MAIL_USERNAME'], 110 | app.config['MAIL_PASSWORD'])) 111 | mail_handler.setLevel(logging.ERROR) 112 | mail_handler.setFormatter(logging.Formatter( 113 | '%(asctime)s %(levelname)s: %(message)s ' 114 | '[in %(pathname)s:%(lineno)d]') 115 | ) 116 | app.logger.addHandler(mail_handler) 117 | 118 | 119 | def register_error_handlers(app): 120 | 121 | @app.errorhandler(403) 122 | def forbidden_page(error): 123 | return render_template("errors/forbidden_page.html"), 403 124 | 125 | @app.errorhandler(404) 126 | def page_not_found(error): 127 | return render_template("errors/page_not_found.html"), 404 128 | 129 | @app.errorhandler(500) 130 | def server_error_page(error): 131 | return render_template("errors/server_error.html"), 500 132 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from sqlalchemy import func 4 | from sqlalchemy.ext.declarative import declared_attr 5 | 6 | from flask_sqlalchemy import SQLAlchemy 7 | 8 | 9 | db = SQLAlchemy() 10 | 11 | 12 | class TimestampMixin(object): 13 | @declared_attr 14 | def created_at(cls): 15 | return db.Column(db.DateTime, default=func.now()) 16 | -------------------------------------------------------------------------------- /app/extensions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask_debugtoolbar import DebugToolbarExtension 4 | from flask_login import LoginManager 5 | from flask_mail import Mail 6 | 7 | from database import db 8 | from users.models import User 9 | 10 | 11 | # Create a tolbar object 12 | debug_toolbar = DebugToolbarExtension() 13 | 14 | 15 | # Create and setup a logger manager object 16 | login_manager = LoginManager() 17 | login_manager.login_view = 'users.views.login_view' 18 | 19 | 20 | @login_manager.user_loader 21 | def load_user(userid): 22 | return db.session.query(User).get(userid) 23 | 24 | 25 | # Create a flask-mail object 26 | mail = Mail() 27 | -------------------------------------------------------------------------------- /app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import unittest 7 | 8 | import coverage 9 | 10 | from flask_migrate import Migrate, MigrateCommand 11 | from flask_script import Manager, Server, prompt, prompt_bool, prompt_pass 12 | from flask_script.commands import Clean, ShowUrls 13 | 14 | from app import create_app 15 | from database import db 16 | from users.models import User 17 | 18 | app = create_app('manageapp', os.path.dirname(__file__)) 19 | 20 | manager = Manager(app) 21 | 22 | manager.add_command("show-urls", ShowUrls()) 23 | manager.add_command("clean", Clean()) 24 | 25 | migrate = Migrate(app, db) 26 | manager.add_command('db', MigrateCommand) 27 | 28 | manager.add_command("runserver", 29 | Server(host='0.0.0.0', 30 | port=5000, 31 | use_debugger=True)) 32 | 33 | COV = coverage.coverage( 34 | branch=True, 35 | include='project/*', 36 | omit=[ 37 | 'project/tests/*', 38 | 'project/server/config.py', 39 | 'project/server/*/__init__.py' 40 | ] 41 | ) 42 | COV.start() 43 | 44 | 45 | @manager.shell 46 | def make_shell_context(): 47 | """ Create a python REPL with several default imports 48 | in the context of the app 49 | """ 50 | return dict(app=app, db=db, User=User) 51 | 52 | 53 | @manager.command 54 | def create_db(): 55 | """Create database migrations and upgrade it""" 56 | db.create_all() 57 | 58 | 59 | @manager.command 60 | def drop_db(): 61 | """Drop all database""" 62 | if prompt_bool( 63 | "Are you sure you want to lose all your data"): 64 | db.drop_all() 65 | 66 | 67 | @manager.command 68 | def create_user(admin=False): 69 | """Creates an user in database""" 70 | username = prompt("Enter username") 71 | email = prompt("Enter email") 72 | password1 = prompt_pass("Enter password") 73 | password2 = prompt_pass("Re-type password") 74 | if password1 == password2: 75 | new_user = User( 76 | username=username, 77 | password=password1, 78 | email=email 79 | ) 80 | new_user.is_admin = admin 81 | db.session.add(new_user) 82 | db.session.commit() 83 | print('User {0} successfully created'.format(username)) 84 | else: 85 | print("Error: Passwords don't match") 86 | 87 | 88 | @manager.command 89 | def install_secret_key(app, filename='secret_key'): 90 | """Configure the SECRET_KEY from a file 91 | in the instance directory. 92 | 93 | If the file does not exist, print instructions 94 | to create it from a shell with a random key, 95 | then exit. 96 | """ 97 | filename = os.path.join(app.instance_path, filename) 98 | 99 | try: 100 | app.config['SECRET_KEY'] = open(filename, 'rb').read() 101 | except IOError: 102 | print('Error: No secret key. Create it with:') 103 | full_path = os.path.dirname(filename) 104 | if not os.path.isdir(full_path): 105 | print('mkdir -p {filename}'.format(filename=full_path)) 106 | print('head -c 24 /dev/urandom > {filename}'.format(filename=filename)) 107 | sys.exit(1) 108 | 109 | 110 | @manager.command 111 | def test(): 112 | """Runs the unit tests without coverage.""" 113 | tests = unittest.TestLoader().discover('project/tests', pattern='test*.py') 114 | result = unittest.TextTestRunner(verbosity=2).run(tests) 115 | if result.wasSuccessful(): 116 | return 0 117 | else: 118 | return 1 119 | 120 | 121 | @manager.command 122 | def cov(): 123 | """Runs the unit tests with coverage.""" 124 | tests = unittest.TestLoader().discover('project/tests') 125 | result = unittest.TextTestRunner(verbosity=2).run(tests) 126 | if result.wasSuccessful(): 127 | COV.stop() 128 | COV.save() 129 | print('Coverage Summary:') 130 | COV.report() 131 | basedir = os.path.abspath(os.path.dirname(__file__)) 132 | covdir = os.path.join(basedir, 'tmp/coverage') 133 | COV.html_report(directory=covdir) 134 | print('HTML version: file://%s/index.html' % covdir) 135 | COV.erase() 136 | return 0 137 | else: 138 | return 1 139 | 140 | 141 | if __name__ == '__main__': 142 | manager.run() 143 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | # Flask and Flask extensions 2 | Flask 3 | Flask-Admin 4 | Flask-Babel 5 | Flask-Bcrypt 6 | Flask-DebugToolbar 7 | Flask-Login 8 | Flask-Mail 9 | Flask-Migrate 10 | Flask-Script 11 | Flask-SQLAlchemy 12 | Flask-Testing 13 | Flask-WTF 14 | # Database and System 15 | celery 16 | coverage 17 | bpython 18 | gunicorn 19 | psycopg2 20 | redis 21 | psutil 22 | supervisor 23 | wtforms>=3.0.0a1 # not directly required, pinned by Snyk to avoid a vulnerability 24 | setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability 25 | -------------------------------------------------------------------------------- /app/run_celery.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | from celery import Celery 6 | 7 | from app import create_app 8 | 9 | 10 | def make_celery(app=None): 11 | app = app or create_app('celeryapp', os.path.dirname(__file__)) 12 | celery = Celery(__name__, broker=app.config['CELERY_BROKER_URL']) 13 | celery.conf.update(app.config) 14 | TaskBase = celery.Task 15 | 16 | class ContextTask(TaskBase): 17 | abstract = True 18 | 19 | def __call__(self, *args, **kwargs): 20 | with app.app_context(): 21 | return TaskBase.__call__(self, *args, **kwargs) 22 | 23 | celery.Task = ContextTask 24 | 25 | return celery 26 | -------------------------------------------------------------------------------- /app/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/app/settings/__init__.py -------------------------------------------------------------------------------- /app/settings/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | DEBUG = False 4 | TESTING = False 5 | DATABASE_URI = '' 6 | -------------------------------------------------------------------------------- /app/settings/dev.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | DEBUG = True 4 | DEBUG_TB_INTERCEPT_REDIRECTS = False 5 | SECRET_KEY = '' 6 | HOST = '0.0.0.0' 7 | PORT = 5000 8 | 9 | LOG_FOLDER = '/tmp/' 10 | 11 | SQLALCHEMY_DATABASE_URI = 'postgresql://flask:flask@postgresql/flask' 12 | SQLALCHEMY_ECHO = True 13 | SQLALCHEMY_TRACK_MODIFICATIONS = True 14 | 15 | CELERY_TIMEZONE = 'Europe/Moscow' 16 | CELERY_ENABLE_UTC = False 17 | CELERY_BROKER_URL = 'redis://redis:6379/0' 18 | CELERY_RESULT_BACKEND = 'redis://redis:6379/0' 19 | CELERY_TRACK_STARTED = True 20 | CELERY_RESULT_PERSISTENT = True 21 | CELERYD_POOL_RESTARTS = True 22 | CELERY_ACCEPT_CONTENT = ['pickle', 'json', 'msgpack', 'yaml'] 23 | 24 | MAIL_SERVER = 'postfix' 25 | MAIL_PORT = 25 26 | MAIL_USE_TLS = False 27 | MAIL_USERNAME = 'postfix' 28 | MAIL_PASSWORD = 'postfix' 29 | MAIL_DEFAULT_SENDER = 'support@postfix' 30 | -------------------------------------------------------------------------------- /app/tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask_mail import Message 4 | 5 | from extensions import mail 6 | from run_celery import make_celery 7 | 8 | 9 | celeryapp = make_celery() 10 | 11 | 12 | @celeryapp.task 13 | def send_email(email, theme, message): 14 | msg = Message(theme, recipients=[email]) 15 | msg.body = message + u"\n" 16 | return mail.send(msg) 17 | -------------------------------------------------------------------------------- /app/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'admin/master.html' %} 3 | 4 | {% block body %} 5 |
Hello, {{current_user.username}}
6 |
Last logged in: {{current_user.last_login}}
7 |
8 | {{memory_sysinfo}} 9 |
10 |
11 | {{cpu_sysinfo}} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} 8 | 9 | 10 | 11 | {% block content %}{% endblock %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/templates/emails/confirmation.html: -------------------------------------------------------------------------------- 1 | 2 | Your account was successfully created. Please click the link below
3 | to confirm your email address and activate your account: 4 | 5 |

6 | {{ confirm_url }} 7 |

8 | 9 |

10 | --
11 | Any questions? Comments? Email support@z-btc.com. 12 |

13 | -------------------------------------------------------------------------------- /app/templates/emails/recover.html: -------------------------------------------------------------------------------- 1 | 2 | We got a request to reset your password.
3 | If you ignore this message, your password won't be changed.
4 | Please click the link below to reset password:
5 | 6 |

7 | {{ recover_url }} 8 |

9 | 10 |

11 | --
12 | Any questions? Comments? Email support@z-btc.com. 13 |

14 | -------------------------------------------------------------------------------- /app/templates/errors/forbidden_page.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/app/templates/errors/forbidden_page.html -------------------------------------------------------------------------------- /app/templates/errors/page_not_found.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/app/templates/errors/page_not_found.html -------------------------------------------------------------------------------- /app/templates/errors/server_error.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/app/templates/errors/server_error.html -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | {% block title %}{{page}}{% endblock %} 5 | {% block content %} 6 |
7 |
{{current_user}}
8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /app/templates/users/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | {% block content %} 5 |
6 |
7 |
8 | 9 | 66 |
67 |
68 |
69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /app/templates/users/reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |

Reset your password

10 | {{form.hidden_tag()}} 11 |

12 | {{form.email}} 13 |

14 | {{ form.csrf_token }} 15 |

16 |

17 | {% with messages = get_flashed_messages() %} 18 | {% if messages %} 19 |

    20 | {% for message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 | {% endif %} 25 | {% endwith %} 26 |

27 |
28 |
29 |
30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /app/templates/users/reset_with_token.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |

Please sign in

10 | {{form.hidden_tag()}} 11 |

12 | {{orm.password}} 13 |

14 |

15 | {{orm.confirm}} 16 |

17 | {{ form.csrf_token }} 18 |

19 |
20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /app/templates/users/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | {% block content %} 5 |
6 |
7 |
8 |

User settings

9 |
10 | {{form.hidden_tag()}} 11 |

12 | {{form.email}} 13 |

14 |

15 | {{form.phone}} 16 |

17 |

18 | {{form.use2factor_auth}} 19 |

20 |

21 | {{form.password}} 22 |

23 |

24 | {{form.new_password}} 25 |

26 |

27 | {{form.confirm}} 28 |

29 |

30 | {{form.recaptcha(class="form-control")}} 31 | {% with messages = get_flashed_messages() %} 32 | {% if messages %} 33 |

    34 | {% for message in messages %} 35 |
  • {{ message }}
  • 36 | {% endfor %} 37 |
38 | {% endif %} 39 | {% endwith %} 40 |

41 | {{ form.csrf_token }} 42 |

43 |
44 |
45 |
46 |
47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /app/templates/users/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | {% block content %} 5 |
6 |
7 |
8 | 35 |
36 |
37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/app/tests/__init__.py -------------------------------------------------------------------------------- /app/tests/test_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from flask_testing import TestCase 5 | 6 | from app import app, db 7 | from users.models import User 8 | 9 | 10 | class BaseTestCase(TestCase): 11 | """A base test case""" 12 | 13 | def create_app(self): 14 | app.config.from_object('settings.testing') 15 | return app 16 | 17 | def setUp(self): 18 | db.create_all() 19 | 20 | def tearDown(self): 21 | db.session.remove() 22 | db.drop_all() 23 | 24 | 25 | class UserTestCase(BaseTestCase): 26 | """A user test case""" 27 | 28 | def test_user(self): 29 | user = User("admin", "ad@min.com", "admin") 30 | db.session.add(user) 31 | db.session.commit() 32 | assert user in db.session 33 | 34 | def test_make_unique_nickname(self): 35 | u = User("john", "john@example.com", "john") 36 | db.session.add(u) 37 | db.session.commit() 38 | nickname = User.make_unique_nickname("john") 39 | assert nickname != "john" 40 | u = User(nickname, "susan@example.com") 41 | db.session.add(u) 42 | db.session.commit() 43 | nickname2 = User.make_unique_nickname("john") 44 | assert nickname2 != "john" 45 | assert nickname2 != nickname 46 | 47 | 48 | class PageTestCase(BaseTestCase): 49 | """A pages test case""" 50 | 51 | def test_index_page(self): 52 | response = self.client.get("/") 53 | self.assert200(response) 54 | 55 | def test_secret_page(self): 56 | response = self.client.get("/secret") 57 | self.assert401(response) 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /app/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/app/users/__init__.py -------------------------------------------------------------------------------- /app/users/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from functools import wraps 4 | 5 | from flask import abort, redirect, request, url_for 6 | from flask_login import current_user 7 | 8 | 9 | def requires_login(f): 10 | @wraps(f) 11 | def decorated_function(*args, **kwargs): 12 | if current_user is None: 13 | return redirect(url_for('.login', next=request.path)) 14 | return f(*args, **kwargs) 15 | return decorated_function 16 | 17 | 18 | def admin_required(f): 19 | @wraps(f) 20 | def decorated_function(*args, **kwargs): 21 | if current_user.is_anonymous or not current_user.is_admin: 22 | abort(403) 23 | return f(*args, **kwargs) 24 | return decorated_function 25 | -------------------------------------------------------------------------------- /app/users/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Users forms.""" 4 | from wtforms import TextField, PasswordField, BooleanField 5 | from wtforms.validators import DataRequired, EqualTo, Email, Length 6 | 7 | from flask_wtf import FlaskForm, RecaptchaField 8 | 9 | from database import db 10 | from users.models import User 11 | 12 | 13 | class EmailForm(FlaskForm): 14 | """Email form for reset password.""" 15 | email = TextField( 16 | u'Email', 17 | validators=[DataRequired(), Email(), Length(max=128)] 18 | ) 19 | 20 | 21 | class PasswordForm(FlaskForm): 22 | """Password form.""" 23 | password = PasswordField( 24 | u'Password', 25 | validators=[DataRequired()] 26 | ) 27 | confirm = PasswordField( 28 | u'Repeat password', 29 | validators=[ 30 | DataRequired(), 31 | EqualTo('password', message=u'Passwords must match.') 32 | ] 33 | ) 34 | 35 | 36 | class LoginForm(FlaskForm): 37 | """Login form.""" 38 | email = TextField( 39 | u'Email', 40 | validators=[DataRequired(), Email(), Length(max=128)] 41 | ) 42 | password = PasswordField( 43 | u'Password', 44 | validators=[DataRequired()] 45 | ) 46 | remember_me = BooleanField( 47 | u'Remember me', 48 | default=False 49 | ) 50 | 51 | def validate(self): 52 | """Validate the form.""" 53 | rv = FlaskForm.validate(self) 54 | if not rv: 55 | return False 56 | 57 | user = db.session.query(User).filter_by( 58 | email=self.email.data).first() 59 | 60 | if user is None: 61 | self.email.errors.append(u'Unknown email.') 62 | return False 63 | 64 | if not user.check_password(self.password.data): 65 | self.password.errors.append(u'Invalid password.') 66 | return False 67 | 68 | self.user = user 69 | 70 | return True 71 | 72 | 73 | class SignUpForm(FlaskForm): 74 | """Registration user form.""" 75 | email = TextField( 76 | u'Email', 77 | validators=[DataRequired(), Email(), Length(max=128)] 78 | ) 79 | password = PasswordField( 80 | u'Password', 81 | validators=[DataRequired()] 82 | ) 83 | confirm = PasswordField( 84 | u'Repeat password', 85 | validators=[ 86 | DataRequired(), 87 | EqualTo('password', message=u'Passwords must match.') 88 | ] 89 | ) 90 | recaptcha = RecaptchaField() 91 | 92 | def validate(self): 93 | """Validate the form.""" 94 | rv = FlaskForm.validate(self) 95 | if not rv: 96 | return False 97 | 98 | user = db.session.query(User).filter_by( 99 | email=self.email.data).first() 100 | if user: 101 | self.email.errors.append('Email already registered.') 102 | return False 103 | 104 | return True 105 | 106 | 107 | class SettingsForm(FlaskForm): 108 | """User settings form.""" 109 | email = TextField( 110 | u'Email', 111 | validators=[DataRequired(), Email(), Length(max=128)] 112 | ) 113 | phone = TextField( 114 | u'Phone', 115 | validators=[Length(max=16)] 116 | ) 117 | use2factor_auth = BooleanField( 118 | u'Use 2-factor auth', 119 | default=False 120 | ) 121 | send_sms_always = BooleanField( 122 | u'Send sms always', 123 | default=False 124 | ) 125 | password = PasswordField( 126 | u'Password', 127 | validators=[DataRequired()] 128 | ) 129 | new_password = PasswordField( 130 | u'New password' 131 | ) 132 | confirm = PasswordField( 133 | u'Repeat new password', 134 | validators=[ 135 | EqualTo('new_password', message=u'Passwords must match.') 136 | ] 137 | ) 138 | recaptcha = RecaptchaField() 139 | 140 | def validate(self): 141 | """Validate the form.""" 142 | rv = FlaskForm.validate(self) 143 | if not rv: 144 | return False 145 | 146 | return True 147 | -------------------------------------------------------------------------------- /app/users/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Exchange models.""" 4 | # Import Stdlibs 5 | import bcrypt 6 | 7 | # Import local modules 8 | from database import TimestampMixin, db 9 | 10 | 11 | class User(TimestampMixin, db.Model): 12 | __tablename__ = 'client' 13 | 14 | id = db.Column(db.Integer, primary_key=True, nullable=False) 15 | username = db.Column(db.String(255), nullable=True, unique=True) 16 | email = db.Column(db.String(255), nullable=True, unique=True) 17 | phone = db.Column(db.String(64), nullable=True, unique=True) 18 | active = db.Column(db.Boolean(), nullable=False, default=False) 19 | admin = db.Column(db.Boolean(), nullable=False, default=False) 20 | use2factor_auth = db.Column(db.Boolean(), nullable=False, default=False) 21 | send_sms_always = db.Column(db.Boolean(), nullable=False, default=False) 22 | last_date = db.Column(db.DateTime, nullable=True) 23 | confirmed_date = db.Column(db.DateTime, nullable=True) 24 | first_ip = db.Column(db.String(40), nullable=True) 25 | last_ip = db.Column(db.String(40), nullable=True) 26 | password_hash = db.Column(db.String(128), nullable=True) 27 | 28 | def __init__(self, email=None, password=None): 29 | if email is not None and password is not None: 30 | self.email = email 31 | self.set_password(password) 32 | else: 33 | if not (email is None and password is None): 34 | raise ValueError 35 | 36 | def __repr__(self): 37 | return "{0} {1} {2} {3}".format(self.id, self.email, self.is_active, self.is_admin) 38 | 39 | def check_password(self, given_password): 40 | return bcrypt.checkpw(given_password, self.password_hash) 41 | 42 | def set_password(self, new_password): 43 | self.password_hash = bcrypt.hashpw(new_password, bcrypt.gensalt()) 44 | 45 | @property 46 | def is_authenticated(self): 47 | return True 48 | 49 | # The methods below are required by flask-login 50 | @property 51 | def is_active(self): 52 | return self.active 53 | 54 | @property 55 | def is_admin(self): 56 | return self.admin 57 | 58 | @property 59 | def is_anonymous(self): 60 | return False 61 | 62 | def get_id(self): 63 | return str(self.id) 64 | 65 | # Serialize method for REST-API 66 | @property 67 | def serialize(self): 68 | return { 69 | 'id': self.id, 70 | 'email': self.email, 71 | 'phone': self.phone, 72 | 'active': u'Да' if self.active else u'Нет', 73 | 'use2factor_auth': u'Да' if self.use2factor_auth else u'Нет', 74 | 'first_ip': self.first_ip, 75 | 'last_ip': self.last_ip, 76 | 'created_at': self.created_at, 77 | 'confirmed_date': self.confirmed_date, 78 | 'last_date': self.last_date 79 | } 80 | -------------------------------------------------------------------------------- /app/users/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Users views.""" 4 | import logging 5 | from datetime import datetime 6 | from itsdangerous import URLSafeTimedSerializer 7 | 8 | from flask import (Blueprint, abort, flash, redirect, 9 | render_template, request, session, url_for) 10 | from flask_login import current_user, login_user, logout_user 11 | 12 | from database import db 13 | from users.models import User 14 | 15 | from .decorators import requires_login 16 | from .forms import (EmailForm, LoginForm, PasswordForm, 17 | SettingsForm, SignUpForm) 18 | 19 | try: 20 | import run_celery 21 | except ImportError: 22 | # migrations hack 23 | pass 24 | 25 | 26 | users = Blueprint('users', __name__) 27 | 28 | 29 | @users.route('/login', methods=['GET', 'POST']) 30 | def login(): 31 | error = None 32 | form = LoginForm() 33 | if request.method == 'POST' and form.validate_on_submit(): 34 | user = db.session.query(User).get(form.user.id) 35 | if user.check_password(form.password.data): 36 | login_user(user) 37 | session['client_id'] = user.id 38 | return redirect( 39 | request.args.get('next') or url_for('default_index')) 40 | else: 41 | logging.debug("Login failed.") 42 | flash(u"Login failed.", 'error') 43 | return redirect(url_for('users.login')) 44 | return render_template('users/login.html', form=form, error=error) 45 | 46 | 47 | @users.route('/logout') 48 | @requires_login 49 | def logout(): 50 | logout_user() 51 | session.pop('client_id', None) 52 | flash(u"You were logged out", 'success') 53 | return redirect(url_for('users.login')) 54 | 55 | 56 | @users.route('/signup', methods=('GET', 'POST')) 57 | def signup(): 58 | form = SignUpForm() 59 | session.pop('client_id', None) 60 | if request.method == 'POST' and form.validate_on_submit(): 61 | logging.debug("Email: {0}".format(form.email.data)) 62 | check_user = User.query.filter_by(email=form.email.data).first() 63 | print(check_user) 64 | if check_user: 65 | logging.debug( 66 | "Email {0} already exist in the database.".format( 67 | form.email.data)) 68 | msg = u""" 69 | User with email {0} already exist. 70 | """ 71 | flash(msg, 'error') 72 | return redirect(url_for('users.signup')) 73 | user = User(form.email.data, form.password.data) 74 | user.email = form.email.data 75 | user.set_password(form.password.data) 76 | if request.headers.getlist("X-Forwarded-For"): 77 | ip = request.headers.getlist("X-Forwarded-For")[0] 78 | elif request.headers.get("X-Real-IP"): 79 | ip = request.headers.get("X-Real-IP") 80 | else: 81 | ip = request.remote_addr 82 | user.first_ip = ip 83 | db.session.add(user) 84 | # Now we'll send the email confirmation link 85 | try: 86 | ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) 87 | token = ts.dumps(user.email, salt='email-confirm-key') 88 | logging.debug("Confirm token: {0}".format(token)) 89 | except Exception as e: 90 | logging.error(e) 91 | abort(404) 92 | subject = u"Confirm your email" 93 | confirm_url = url_for( 94 | 'users.confirm_email', token=token, _external=True) 95 | html = render_template( 96 | 'emails/confirmation.html', confirm_url=confirm_url) 97 | run_celery.send_email.apply_async( 98 | (user.email, subject, html) 99 | ) 100 | msg = u""" 101 | Account was successfully created. 102 | Check your email to confirm account. 103 | """ 104 | logging.debug("New account was successfully created.") 105 | flash(msg, 'success') 106 | db.session.commit() 107 | return redirect(url_for('users.login')) 108 | return render_template('users/signup.html', form=form) 109 | 110 | 111 | @users.route('/settings', methods=('GET', 'POST')) 112 | @requires_login 113 | def settings(): 114 | form = SettingsForm() 115 | try: 116 | user = db.session.query(User).get(current_user.get_id()) 117 | except TypeError: 118 | abort(404) 119 | if request.method == 'POST' and form.validate_on_submit(): 120 | if user.check_password(form.password.data): 121 | error = False 122 | if not(user.email == form.email.data) and \ 123 | not User.query.filter_by(email=form.email.data).scalar(): 124 | flash(u"This email already exist.", 'error') 125 | error = True 126 | if not(user.phone == form.phone.data) and \ 127 | User.query.filter_by(phone=form.phone.data).scalar(): 128 | flash(u"This phone already exist.", 'error') 129 | error = True 130 | user.email = form.email.data 131 | user.phone = form.phone.data 132 | new_password = form.new_password.data 133 | confirm = form.confirm.data 134 | if new_password and confirm and new_password == confirm: 135 | user.set_password(new_password) 136 | elif new_password and confirm: 137 | flash(u"Passwords don't match.", 'error') 138 | error = True 139 | if not error: 140 | db.session.add(user) 141 | db.session.commit() 142 | flash(u"Your changes have been saved.", 'success') 143 | return redirect(url_for('users.settings')) 144 | else: 145 | flash(u"Please, check password again.", 'error') 146 | return redirect(url_for('users.settings')) 147 | else: 148 | form.email.data = user.email 149 | form.phone.data = user.phone 150 | return render_template('users/settings.html', form=form) 151 | 152 | 153 | @users.route('/confirm/') 154 | def confirm_email(token): 155 | try: 156 | ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) 157 | email = ts.loads(token, salt="email-confirm-key", max_age=86400) 158 | except Exception as e: 159 | logging.error(e) 160 | abort(404) 161 | logging.debug("Token: {0} email: {1}".format(token, email)) 162 | user = User.query.filter_by(email=email).first_or_404() 163 | user.active = True 164 | user.confirmed_date = datetime.utcnow() 165 | db.session.add(user) 166 | db.session.commit() 167 | msg = u""" 168 | Thanks! Your email address was confirmed. 169 | Your account is active now. Please, login. 170 | """ 171 | flash(msg, 'success') 172 | logging.debug("Account {0} is active now.".format(email)) 173 | return redirect(url_for('users.login')) 174 | 175 | 176 | @users.route('/reset', methods=["GET", "POST"]) 177 | def reset(): 178 | form = EmailForm() 179 | if request.method == 'POST' and form.validate_on_submit(): 180 | user = User.query.filter_by(email=form.email.data).first_or_404() 181 | logging.debug( 182 | "Password reset request from {0}".format( 183 | user.email)) 184 | subject = "Password reset requested" 185 | # Here we use the URLSafeTimedSerializer we created in `util` at the 186 | # beginning of the chapter 187 | try: 188 | ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) 189 | token = ts.dumps(user.email, salt='recover-key') 190 | except Exception as e: 191 | logging.error(e) 192 | abort(404) 193 | recover_url = url_for( 194 | 'users.reset_with_token', 195 | token=token, 196 | _external=True) 197 | html = render_template( 198 | 'emails/recover.html', 199 | recover_url=recover_url) 200 | run_celery.send_email.apply_async( 201 | (user.email, subject, html) 202 | ) 203 | msg = u""" 204 | Please, check your email. 205 | """ 206 | flash(msg, 'error') 207 | return redirect(url_for('users.login')) 208 | return render_template('users/reset.html', form=form) 209 | 210 | 211 | @users.route('/reset/', methods=["GET", "POST"]) 212 | def reset_with_token(token): 213 | try: 214 | ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) 215 | email = ts.loads(token, salt="recover-key", max_age=86400) 216 | except Exception as e: 217 | logging.error(e) 218 | abort(404) 219 | form = PasswordForm() 220 | if request.method == 'POST' and form.validate_on_submit(): 221 | user = User.query.filter_by(email=email).first_or_404() 222 | user.set_password(form.password.data) 223 | db.session.add(user) 224 | db.session.commit() 225 | return redirect(url_for('users.login')) 226 | return render_template( 227 | 'users/reset_with_token.html', form=form, token=token) 228 | -------------------------------------------------------------------------------- /app/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | base = /var/www/app 3 | module = app 4 | callable = app 5 | socket = /var/www/app/run/uwsgi/flask_app.sock 6 | chmod-socket = 666 7 | logto = /var/www/app/log/uwsgi.log 8 | -------------------------------------------------------------------------------- /app/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import Blueprint, render_template 4 | 5 | from users.decorators import requires_login 6 | 7 | 8 | main_blueprint = Blueprint('', __name__) 9 | 10 | 11 | @main_blueprint.route('/') 12 | def index(): 13 | return render_template('index.html', page=u"It's working!") 14 | 15 | 16 | @main_blueprint.route('/secret') 17 | @requires_login 18 | def secret(): 19 | return render_template('index.html', page=u"Secret page!") 20 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | flaskapp: 2 | build: ./ 3 | working_dir: /var/www/app 4 | command: bash -c "supervisord -c /etc/supervisord.conf" 5 | volumes: 6 | - ./app:/var/www/app:rw 7 | - ./app/log:/var/www/app/log:rw 8 | - ./supervisor/supervisord.conf:/etc/supervisord.conf:ro 9 | - ./supervisor/conf.d:/etc/supervisor/conf.d:ro 10 | links: 11 | - postgresql 12 | - rabbitmq 13 | - redis 14 | environment: 15 | - APP_ENVIRONMENT=PRODUCTION 16 | - FLASK_SETTINGS=settings/prod.py 17 | 18 | postgresql: 19 | restart: always 20 | image: postgres:latest 21 | env_file: postgresql/.env 22 | volumes: 23 | - ./dockerfiles/pgdata:/var/lib/postgresql/data/pgdata:rw 24 | ports: 25 | - "5432" 26 | 27 | rabbitmq: 28 | restart: always 29 | image: rabbitmq:latest 30 | env_file: rabbitmq/.env 31 | ports: 32 | - "5672" 33 | 34 | redis: 35 | restart: always 36 | image: redis:latest 37 | env_file: redis/.env 38 | volumes: 39 | - ./redis/redis.conf:/data/redis.conf:ro 40 | command: redis-server /data/redis.conf 41 | ports: 42 | - "6379" 43 | 44 | nginx: 45 | restart: always 46 | image: nginx:latest 47 | env_file: nginx/.env 48 | volumes: 49 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro 50 | - ./nginx/flask_app.conf:/etc/nginx/sites-enabled/flask_app.conf:ro 51 | - ./app/run/uwsgi:/var/run/uwsgi:ro 52 | ports: 53 | - "80:80" 54 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | flaskapp: 2 | build: ./ 3 | working_dir: /var/www/app 4 | command: bash -c "supervisord -c /etc/supervisord.conf" 5 | volumes: 6 | - ./app:/var/www/app:rw 7 | - ./supervisor/supervisord.conf:/etc/supervisord.conf:ro 8 | - ./supervisor/conf.d:/etc/supervisor/conf.d:ro 9 | links: 10 | - postgresql 11 | - redis 12 | environment: 13 | - APP_ENVIRONMENT=DEVELOPMENT 14 | - FLASK_SETTINGS=settings/dev.py 15 | ports: 16 | - "5000:5000" 17 | 18 | postgresql: 19 | restart: always 20 | image: postgres:latest 21 | env_file: postgresql/.env 22 | volumes: 23 | - ./dockerfiles/pgdata:/var/lib/postgresql/data/pgdata:rw 24 | ports: 25 | - "5432" 26 | 27 | redis: 28 | restart: always 29 | image: redis:latest 30 | env_file: redis/.env 31 | ports: 32 | - "6379" 33 | -------------------------------------------------------------------------------- /dockerfiles/README.md: -------------------------------------------------------------------------------- 1 | all database files should be here 2 | -------------------------------------------------------------------------------- /nginx/.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/nginx/.env -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 2; 2 | user www-data; 3 | 4 | error_log /var/log/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | use epoll; 10 | accept_mutex off; 11 | } 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | proxy_set_header X-Real-IP $remote_addr; 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | 18 | default_type application/octet-stream; 19 | 20 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 21 | '$status $body_bytes_sent "$http_referer" ' 22 | '"$http_user_agent" "$http_x_forwarded_for"'; 23 | 24 | access_log /var/log/access.log main; 25 | 26 | sendfile on; 27 | 28 | keepalive_timeout 65; 29 | 30 | client_max_body_size 300m; 31 | client_body_buffer_size 128k; 32 | 33 | gzip on; 34 | gzip_http_version 1.0; 35 | gzip_comp_level 6; 36 | gzip_min_length 0; 37 | gzip_buffers 16 8k; 38 | gzip_proxied any; 39 | gzip_types text/plain text/css text/xml text/javascript application/xml application/xml+rss application/javascript application/json; 40 | gzip_disable "MSIE [1-6]\."; 41 | gzip_vary on; 42 | 43 | include /etc/nginx/sites-enabled/*.conf; 44 | } 45 | -------------------------------------------------------------------------------- /nginx/sites-enabled/flask_app.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | client_max_body_size 50M; 5 | 6 | location / { 7 | include uwsgi_params; 8 | uwsgi_pass unix://var/run/uwsgi/flask_app.sock; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /postgresql/.env: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=flask 2 | POSTGRES_DB=flask 3 | POSTGRES_PASSWORD=flask 4 | POSTGRES_HOST=postgresql 5 | POSTGRES_PORT=5432 6 | PGDATA=/var/lib/postgresql/data/pgdata 7 | -------------------------------------------------------------------------------- /postgresql/postgresql.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/postgresql/postgresql.conf -------------------------------------------------------------------------------- /rabbitmq/.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/rabbitmq/.env -------------------------------------------------------------------------------- /rabbitmq/rabbitmq.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/rabbitmq/rabbitmq.conf -------------------------------------------------------------------------------- /redis/.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ip0000h/flask-docker-compose/ac19e89c8774076bcb15f58cc2697c6aeca794f1/redis/.env -------------------------------------------------------------------------------- /redis/redis.conf: -------------------------------------------------------------------------------- 1 | # Redis configuration file example 2 | 3 | # Note on units: when memory size is needed, it is possible to specifiy 4 | # it in the usual form of 1k 5GB 4M and so forth: 5 | # 6 | # 1k => 1000 bytes 7 | # 1kb => 1024 bytes 8 | # 1m => 1000000 bytes 9 | # 1mb => 1024*1024 bytes 10 | # 1g => 1000000000 bytes 11 | # 1gb => 1024*1024*1024 bytes 12 | # 13 | # units are case insensitive so 1GB 1Gb 1gB are all the same. 14 | 15 | # By default Redis does not run as a daemon. Use 'yes' if you need it. 16 | # Note that Redis will write a pid file in /var/run/redis.pid when daemonized. 17 | daemonize yes 18 | 19 | # When running daemonized, Redis writes a pid file in /var/run/redis.pid by 20 | # default. You can specify a custom pid file location here. 21 | pidfile /tmp/redis-server.pid 22 | 23 | # Accept connections on the specified port, default is 6379. 24 | # If port 0 is specified Redis will not listen on a TCP socket. 25 | port 6379 26 | 27 | # If you want you can bind a single interface, if the bind option is not 28 | # specified all the interfaces will listen for incoming connections. 29 | # 30 | bind 127.0.0.1 31 | 32 | # Specify the path for the unix socket that will be used to listen for 33 | # incoming connections. There is no default, so Redis will not listen 34 | # on a unix socket when not specified. 35 | # 36 | # unixsocket /var/run/redis/redis.sock 37 | # unixsocketperm 755 38 | 39 | # Close the connection after a client is idle for N seconds (0 to disable) 40 | timeout 0 41 | 42 | # Set server verbosity to 'debug' 43 | # it can be one of: 44 | # debug (a lot of information, useful for development/testing) 45 | # verbose (many rarely useful info, but not a mess like the debug level) 46 | # notice (moderately verbose, what you want in production probably) 47 | # warning (only very important / critical messages are logged) 48 | loglevel notice 49 | 50 | # Specify the log file name. Also 'stdout' can be used to force 51 | # Redis to log on the standard output. Note that if you use standard 52 | # output for logging but daemonize, logs will be sent to /dev/null 53 | logfile /tmp/redis-server.log 54 | 55 | # To enable logging to the system logger, just set 'syslog-enabled' to yes, 56 | # and optionally update the other syslog parameters to suit your needs. 57 | # syslog-enabled no 58 | 59 | # Specify the syslog identity. 60 | # syslog-ident redis 61 | 62 | # Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7. 63 | # syslog-facility local0 64 | 65 | # Set the number of databases. The default database is DB 0, you can select 66 | # a different one on a per-connection basis using SELECT where 67 | # dbid is a number between 0 and 'databases'-1 68 | databases 16 69 | 70 | ################################ SNAPSHOTTING ################################# 71 | # 72 | # Save the DB on disk: 73 | # 74 | # save 75 | # 76 | # Will save the DB if both the given number of seconds and the given 77 | # number of write operations against the DB occurred. 78 | # 79 | # In the example below the behaviour will be to save: 80 | # after 900 sec (15 min) if at least 1 key changed 81 | # after 300 sec (5 min) if at least 10 keys changed 82 | # after 60 sec if at least 10000 keys changed 83 | # 84 | # Note: you can disable saving at all commenting all the "save" lines. 85 | 86 | save 900 1 87 | save 300 10 88 | save 60 10000 89 | 90 | # Compress string objects using LZF when dump .rdb databases? 91 | # For default that's set to 'yes' as it's almost always a win. 92 | # If you want to save some CPU in the saving child set it to 'no' but 93 | # the dataset will likely be bigger if you have compressible values or keys. 94 | rdbcompression yes 95 | 96 | # The filename where to dump the DB 97 | dbfilename dump.rdb 98 | 99 | # The working directory. 100 | # 101 | # The DB will be written inside this directory, with the filename specified 102 | # above using the 'dbfilename' configuration directive. 103 | # 104 | # Also the Append Only File will be created inside this directory. 105 | # 106 | # Note that you must specify a directory here, not a file name. 107 | # dir /var/lib/redis 108 | 109 | ################################# REPLICATION ################################# 110 | 111 | # Master-Slave replication. Use slaveof to make a Redis instance a copy of 112 | # another Redis server. Note that the configuration is local to the slave 113 | # so for example it is possible to configure the slave to save the DB with a 114 | # different interval, or to listen to another port, and so on. 115 | # 116 | # slaveof 117 | 118 | # If the master is password protected (using the "requirepass" configuration 119 | # directive below) it is possible to tell the slave to authenticate before 120 | # starting the replication synchronization process, otherwise the master will 121 | # refuse the slave request. 122 | # 123 | # masterauth 124 | 125 | # When a slave lost the connection with the master, or when the replication 126 | # is still in progress, the slave can act in two different ways: 127 | # 128 | # 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will 129 | # still reply to client requests, possibly with out of data data, or the 130 | # data set may just be empty if this is the first synchronization. 131 | # 132 | # 2) if slave-serve-stale data is set to 'no' the slave will reply with 133 | # an error "SYNC with master in progress" to all the kind of commands 134 | # but to INFO and SLAVEOF. 135 | # 136 | slave-serve-stale-data yes 137 | 138 | # Slaves send PINGs to server in a predefined interval. It's possible to change 139 | # this interval with the repl_ping_slave_period option. The default value is 10 140 | # seconds. 141 | # 142 | # repl-ping-slave-period 10 143 | 144 | # The following option sets a timeout for both Bulk transfer I/O timeout and 145 | # master data or ping response timeout. The default value is 60 seconds. 146 | # 147 | # It is important to make sure that this value is greater than the value 148 | # specified for repl-ping-slave-period otherwise a timeout will be detected 149 | # every time there is low traffic between the master and the slave. 150 | # 151 | # repl-timeout 60 152 | 153 | ################################## SECURITY ################################### 154 | 155 | # Require clients to issue AUTH before processing any other 156 | # commands. This might be useful in environments in which you do not trust 157 | # others with access to the host running redis-server. 158 | # 159 | # This should stay commented out for backward compatibility and because most 160 | # people do not need auth (e.g. they run their own servers). 161 | # 162 | # Warning: since Redis is pretty fast an outside user can try up to 163 | # 150k passwords per second against a good box. This means that you should 164 | # use a very strong password otherwise it will be very easy to break. 165 | # 166 | # requirepass foobared 167 | 168 | # Command renaming. 169 | # 170 | # It is possilbe to change the name of dangerous commands in a shared 171 | # environment. For instance the CONFIG command may be renamed into something 172 | # of hard to guess so that it will be still available for internal-use 173 | # tools but not available for general clients. 174 | # 175 | # Example: 176 | # 177 | # rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 178 | # 179 | # It is also possilbe to completely kill a command renaming it into 180 | # an empty string: 181 | # 182 | # rename-command CONFIG "" 183 | 184 | ################################### LIMITS #################################### 185 | 186 | # Set the max number of connected clients at the same time. By default there 187 | # is no limit, and it's up to the number of file descriptors the Redis process 188 | # is able to open. The special value '0' means no limits. 189 | # Once the limit is reached Redis will close all the new connections sending 190 | # an error 'max number of clients reached'. 191 | # 192 | # maxclients 128 193 | 194 | # Don't use more memory than the specified amount of bytes. 195 | # When the memory limit is reached Redis will try to remove keys 196 | # accordingly to the eviction policy selected (see maxmemmory-policy). 197 | # 198 | # If Redis can't remove keys according to the policy, or if the policy is 199 | # set to 'noeviction', Redis will start to reply with errors to commands 200 | # that would use more memory, like SET, LPUSH, and so on, and will continue 201 | # to reply to read-only commands like GET. 202 | # 203 | # This option is usually useful when using Redis as an LRU cache, or to set 204 | # an hard memory limit for an instance (using the 'noeviction' policy). 205 | # 206 | # WARNING: If you have slaves attached to an instance with maxmemory on, 207 | # the size of the output buffers needed to feed the slaves are subtracted 208 | # from the used memory count, so that network problems / resyncs will 209 | # not trigger a loop where keys are evicted, and in turn the output 210 | # buffer of slaves is full with DELs of keys evicted triggering the deletion 211 | # of more keys, and so forth until the database is completely emptied. 212 | # 213 | # In short... if you have slaves attached it is suggested that you set a lower 214 | # limit for maxmemory so that there is some free RAM on the system for slave 215 | # output buffers (but this is not needed if the policy is 'noeviction'). 216 | # 217 | # maxmemory 218 | 219 | # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory 220 | # is reached? You can select among five behavior: 221 | # 222 | # volatile-lru -> remove the key with an expire set using an LRU algorithm 223 | # allkeys-lru -> remove any key accordingly to the LRU algorithm 224 | # volatile-random -> remove a random key with an expire set 225 | # allkeys->random -> remove a random key, any key 226 | # volatile-ttl -> remove the key with the nearest expire time (minor TTL) 227 | # noeviction -> don't expire at all, just return an error on write operations 228 | # 229 | # Note: with all the kind of policies, Redis will return an error on write 230 | # operations, when there are not suitable keys for eviction. 231 | # 232 | # At the date of writing this commands are: set setnx setex append 233 | # incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd 234 | # sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby 235 | # zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby 236 | # getset mset msetnx exec sort 237 | # 238 | # The default is: 239 | # 240 | # maxmemory-policy volatile-lru 241 | 242 | # LRU and minimal TTL algorithms are not precise algorithms but approximated 243 | # algorithms (in order to save memory), so you can select as well the sample 244 | # size to check. For instance for default Redis will check three keys and 245 | # pick the one that was used less recently, you can change the sample size 246 | # using the following configuration directive. 247 | # 248 | # maxmemory-samples 3 249 | 250 | ############################## APPEND ONLY MODE ############################### 251 | 252 | # By default Redis asynchronously dumps the dataset on disk. If you can live 253 | # with the idea that the latest records will be lost if something like a crash 254 | # happens this is the preferred way to run Redis. If instead you care a lot 255 | # about your data and don't want to that a single record can get lost you should 256 | # enable the append only mode: when this mode is enabled Redis will append 257 | # every write operation received in the file appendonly.aof. This file will 258 | # be read on startup in order to rebuild the full dataset in memory. 259 | # 260 | # Note that you can have both the async dumps and the append only file if you 261 | # like (you have to comment the "save" statements above to disable the dumps). 262 | # Still if append only mode is enabled Redis will load the data from the 263 | # log file at startup ignoring the dump.rdb file. 264 | # 265 | # IMPORTANT: Check the BGREWRITEAOF to check how to rewrite the append 266 | # log file in background when it gets too big. 267 | 268 | appendonly no 269 | 270 | # The name of the append only file (default: "appendonly.aof") 271 | # appendfilename appendonly.aof 272 | 273 | # The fsync() call tells the Operating System to actually write data on disk 274 | # instead to wait for more data in the output buffer. Some OS will really flush 275 | # data on disk, some other OS will just try to do it ASAP. 276 | # 277 | # Redis supports three different modes: 278 | # 279 | # no: don't fsync, just let the OS flush the data when it wants. Faster. 280 | # always: fsync after every write to the append only log . Slow, Safest. 281 | # everysec: fsync only if one second passed since the last fsync. Compromise. 282 | # 283 | # The default is "everysec" that's usually the right compromise between 284 | # speed and data safety. It's up to you to understand if you can relax this to 285 | # "no" that will will let the operating system flush the output buffer when 286 | # it wants, for better performances (but if you can live with the idea of 287 | # some data loss consider the default persistence mode that's snapshotting), 288 | # or on the contrary, use "always" that's very slow but a bit safer than 289 | # everysec. 290 | # 291 | # If unsure, use "everysec". 292 | 293 | # appendfsync always 294 | appendfsync everysec 295 | # appendfsync no 296 | 297 | # When the AOF fsync policy is set to always or everysec, and a background 298 | # saving process (a background save or AOF log background rewriting) is 299 | # performing a lot of I/O against the disk, in some Linux configurations 300 | # Redis may block too long on the fsync() call. Note that there is no fix for 301 | # this currently, as even performing fsync in a different thread will block 302 | # our synchronous write(2) call. 303 | # 304 | # In order to mitigate this problem it's possible to use the following option 305 | # that will prevent fsync() from being called in the main process while a 306 | # BGSAVE or BGREWRITEAOF is in progress. 307 | # 308 | # This means that while another child is saving the durability of Redis is 309 | # the same as "appendfsync none", that in pratical terms means that it is 310 | # possible to lost up to 30 seconds of log in the worst scenario (with the 311 | # default Linux settings). 312 | # 313 | # If you have latency problems turn this to "yes". Otherwise leave it as 314 | # "no" that is the safest pick from the point of view of durability. 315 | no-appendfsync-on-rewrite no 316 | 317 | # Automatic rewrite of the append only file. 318 | # Redis is able to automatically rewrite the log file implicitly calling 319 | # BGREWRITEAOF when the AOF log size will growth by the specified percentage. 320 | # 321 | # This is how it works: Redis remembers the size of the AOF file after the 322 | # latest rewrite (or if no rewrite happened since the restart, the size of 323 | # the AOF at startup is used). 324 | # 325 | # This base size is compared to the current size. If the current size is 326 | # bigger than the specified percentage, the rewrite is triggered. Also 327 | # you need to specify a minimal size for the AOF file to be rewritten, this 328 | # is useful to avoid rewriting the AOF file even if the percentage increase 329 | # is reached but it is still pretty small. 330 | # 331 | # Specify a precentage of zero in order to disable the automatic AOF 332 | # rewrite feature. 333 | 334 | auto-aof-rewrite-percentage 100 335 | auto-aof-rewrite-min-size 64mb 336 | 337 | ################################## SLOW LOG ################################### 338 | 339 | # The Redis Slow Log is a system to log queries that exceeded a specified 340 | # execution time. The execution time does not include the I/O operations 341 | # like talking with the client, sending the reply and so forth, 342 | # but just the time needed to actually execute the command (this is the only 343 | # stage of command execution where the thread is blocked and can not serve 344 | # other requests in the meantime). 345 | # 346 | # You can configure the slow log with two parameters: one tells Redis 347 | # what is the execution time, in microseconds, to exceed in order for the 348 | # command to get logged, and the other parameter is the length of the 349 | # slow log. When a new command is logged the oldest one is removed from the 350 | # queue of logged commands. 351 | 352 | # The following time is expressed in microseconds, so 1000000 is equivalent 353 | # to one second. Note that a negative number disables the slow log, while 354 | # a value of zero forces the logging of every command. 355 | slowlog-log-slower-than 10000 356 | 357 | # There is no limit to this length. Just be aware that it will consume memory. 358 | # You can reclaim memory used by the slow log with SLOWLOG RESET. 359 | slowlog-max-len 128 360 | 361 | ################################ VIRTUAL MEMORY ############################### 362 | 363 | ### WARNING! Virtual Memory is deprecated in Redis 2.4 364 | ### The use of Virtual Memory is strongly discouraged. 365 | 366 | # Virtual Memory allows Redis to work with datasets bigger than the actual 367 | # amount of RAM needed to hold the whole dataset in memory. 368 | # In order to do so very used keys are taken in memory while the other keys 369 | # are swapped into a swap file, similarly to what operating systems do 370 | # with memory pages. 371 | # 372 | # To enable VM just set 'vm-enabled' to yes, and set the following three 373 | # VM parameters accordingly to your needs. 374 | 375 | vm-enabled no 376 | # vm-enabled yes 377 | 378 | # This is the path of the Redis swap file. As you can guess, swap files 379 | # can't be shared by different Redis instances, so make sure to use a swap 380 | # file for every redis process you are running. Redis will complain if the 381 | # swap file is already in use. 382 | # 383 | # The best kind of storage for the Redis swap file (that's accessed at random) 384 | # is a Solid State Disk (SSD). 385 | # 386 | # *** WARNING *** if you are using a shared hosting the default of putting 387 | # the swap file under /tmp is not secure. Create a dir with access granted 388 | # only to Redis user and configure Redis to create the swap file there. 389 | vm-swap-file /tmp/redis.swap 390 | 391 | # vm-max-memory configures the VM to use at max the specified amount of 392 | # RAM. Everything that deos not fit will be swapped on disk *if* possible, that 393 | # is, if there is still enough contiguous space in the swap file. 394 | # 395 | # With vm-max-memory 0 the system will swap everything it can. Not a good 396 | # default, just specify the max amount of RAM you can in bytes, but it's 397 | # better to leave some margin. For instance specify an amount of RAM 398 | # that's more or less between 60 and 80% of your free RAM. 399 | vm-max-memory 0 400 | 401 | # Redis swap files is split into pages. An object can be saved using multiple 402 | # contiguous pages, but pages can't be shared between different objects. 403 | # So if your page is too big, small objects swapped out on disk will waste 404 | # a lot of space. If you page is too small, there is less space in the swap 405 | # file (assuming you configured the same number of total swap file pages). 406 | # 407 | # If you use a lot of small objects, use a page size of 64 or 32 bytes. 408 | # If you use a lot of big objects, use a bigger page size. 409 | # If unsure, use the default :) 410 | vm-page-size 32 411 | 412 | # Number of total memory pages in the swap file. 413 | # Given that the page table (a bitmap of free/used pages) is taken in memory, 414 | # every 8 pages on disk will consume 1 byte of RAM. 415 | # 416 | # The total swap size is vm-page-size * vm-pages 417 | # 418 | # With the default of 32-bytes memory pages and 134217728 pages Redis will 419 | # use a 4 GB swap file, that will use 16 MB of RAM for the page table. 420 | # 421 | # It's better to use the smallest acceptable value for your application, 422 | # but the default is large in order to work in most conditions. 423 | vm-pages 134217728 424 | 425 | # Max number of VM I/O threads running at the same time. 426 | # This threads are used to read/write data from/to swap file, since they 427 | # also encode and decode objects from disk to memory or the reverse, a bigger 428 | # number of threads can help with big objects even if they can't help with 429 | # I/O itself as the physical device may not be able to couple with many 430 | # reads/writes operations at the same time. 431 | # 432 | # The special value of 0 turn off threaded I/O and enables the blocking 433 | # Virtual Memory implementation. 434 | vm-max-threads 4 435 | 436 | ############################### ADVANCED CONFIG ############################### 437 | 438 | # Hashes are encoded in a special way (much more memory efficient) when they 439 | # have at max a given numer of elements, and the biggest element does not 440 | # exceed a given threshold. You can configure this limits with the following 441 | # configuration directives. 442 | hash-max-zipmap-entries 512 443 | hash-max-zipmap-value 64 444 | 445 | # Similarly to hashes, small lists are also encoded in a special way in order 446 | # to save a lot of space. The special representation is only used when 447 | # you are under the following limits: 448 | list-max-ziplist-entries 512 449 | list-max-ziplist-value 64 450 | 451 | # Sets have a special encoding in just one case: when a set is composed 452 | # of just strings that happens to be integers in radix 10 in the range 453 | # of 64 bit signed integers. 454 | # The following configuration setting sets the limit in the size of the 455 | # set in order to use this special memory saving encoding. 456 | set-max-intset-entries 512 457 | 458 | # Similarly to hashes and lists, sorted sets are also specially encoded in 459 | # order to save a lot of space. This encoding is only used when the length and 460 | # elements of a sorted set are below the following limits: 461 | zset-max-ziplist-entries 128 462 | zset-max-ziplist-value 64 463 | 464 | # Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in 465 | # order to help rehashing the main Redis hash table (the one mapping top-level 466 | # keys to values). The hash table implementation redis uses (see dict.c) 467 | # performs a lazy rehashing: the more operation you run into an hash table 468 | # that is rhashing, the more rehashing "steps" are performed, so if the 469 | # server is idle the rehashing is never complete and some more memory is used 470 | # by the hash table. 471 | # 472 | # The default is to use this millisecond 10 times every second in order to 473 | # active rehashing the main dictionaries, freeing memory when possible. 474 | # 475 | # If unsure: 476 | # use "activerehashing no" if you have hard latency requirements and it is 477 | # not a good thing in your environment that Redis can reply form time to time 478 | # to queries with 2 milliseconds delay. 479 | # 480 | # use "activerehashing yes" if you don't have such hard requirements but 481 | # want to free memory asap when possible. 482 | activerehashing yes 483 | 484 | ################################## INCLUDES ################################### 485 | 486 | # Include one or more other config files here. This is useful if you 487 | # have a standard template that goes to all redis server but also need 488 | # to customize a few per-server settings. Include files can include 489 | # other files, so use this wisely. 490 | # 491 | # include /path/to/local.conf 492 | # include /path/to/other.conf 493 | -------------------------------------------------------------------------------- /supervisor/conf.d/DEVELOPMENT/celery_app.conf: -------------------------------------------------------------------------------- 1 | [program:celery_app] 2 | command = celery worker -A tasks.celeryapp -B -s /tmp/celerybeat-schedule --loglevel=INFO 3 | directory = /var/www/app 4 | autostart = true 5 | autorestart = true 6 | startretries= 1 7 | stopsignal=TERM 8 | stopwaitsecs=10 9 | stopasgroup=false 10 | killasgroup=false 11 | redirect_stderr = true 12 | stdout_logfile = /tmp/celery_app.log 13 | -------------------------------------------------------------------------------- /supervisor/conf.d/DEVELOPMENT/flask_app.conf: -------------------------------------------------------------------------------- 1 | [program:flask_app] 2 | command = python manage.py runserver 3 | directory = /var/www/app 4 | autostart = true 5 | autorestart = true 6 | startretries= 10 7 | stopsignal=TERM 8 | stopwaitsecs=10 9 | stopasgroup=false 10 | killasgroup=false 11 | redirect_stderr = true 12 | stdout_logfile = /tmp/flask_app.log 13 | -------------------------------------------------------------------------------- /supervisor/conf.d/PRODUCTION/celery_app.conf: -------------------------------------------------------------------------------- 1 | [program:celery_app] 2 | command = celery worker -A tasks.celeryapp -B --loglevel=WAARNING 3 | directory = /var/www/app 4 | autostart = true 5 | autorestart = true 6 | startretries = 1 7 | redirect_stderr = true 8 | stdout_logfile = /var/www/app/log/celery_app.log 9 | -------------------------------------------------------------------------------- /supervisor/conf.d/PRODUCTION/flask_app.conf: -------------------------------------------------------------------------------- 1 | [program:flask_app] 2 | command = uwsgi --ini uwsgi.ini 3 | directory = /var/www/app 4 | autostart = true 5 | autorestart = true 6 | startretries = 1 7 | redirect_stderr = true 8 | stdout_logfile = /var/www/app/log/flask_app.log 9 | -------------------------------------------------------------------------------- /supervisor/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | user=www-data 3 | logfile = /tmp/supervisord.log 4 | pidfile = /tmp/supervisord.pid 5 | logfile_maxbytes = 50MB 6 | logfile_backups=10 7 | loglevel = info 8 | nodaemon = true 9 | 10 | [supervisorctl] 11 | serverurl=unix:///tmp/supervisord.sock 12 | 13 | [rpcinterface:supervisor] 14 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 15 | 16 | [unix_http_server] 17 | file=/tmp/supervisord.sock 18 | chmod=0755 19 | chown=www-data:www-data 20 | 21 | [include] 22 | files=/etc/supervisor/conf.d/%(ENV_APP_ENVIRONMENT)s/*.conf 23 | --------------------------------------------------------------------------------