├── app ├── migrations │ ├── README │ ├── script.py.mako │ ├── versions │ │ ├── 32b48058cd02_.py │ │ └── 791cd7d80402_.py │ ├── alembic.ini │ └── env.py ├── templates │ ├── guest_confirmation.html │ ├── guest_list.html │ ├── base.html │ └── guest_registration.html ├── models.py └── app.py ├── Dockerfile ├── requirements.txt ├── LICENSE ├── .gitignore └── README.md /app/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uwsgi-nginx-flask:python3.6 2 | 3 | COPY requirements.txt / 4 | 5 | WORKDIR / 6 | 7 | RUN pip install -r ./requirements.txt --no-cache-dir 8 | 9 | COPY app/ /app/ 10 | 11 | WORKDIR /app 12 | 13 | ENV FLASK_APP=app.py 14 | CMD flask db upgrade && flask run -h 0.0.0.0 -p 5000 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.9.1 2 | appdirs==1.4.3 3 | click==6.7 4 | Flask==0.12.1 5 | Flask-Migrate==2.0.3 6 | Flask-Script==2.0.5 7 | Flask-SQLAlchemy==2.2 8 | itsdangerous==0.24 9 | Jinja2==2.9.6 10 | Mako==1.0.6 11 | MarkupSafe==1.0 12 | packaging==16.8 13 | psycopg2==2.7.1 14 | pyparsing==2.2.0 15 | python-editor==1.0.3 16 | six==1.10.0 17 | SQLAlchemy==1.1.9 18 | Werkzeug==0.12.1 19 | -------------------------------------------------------------------------------- /app/templates/guest_confirmation.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Guest Registration Confirmed{% endblock %} 3 | {% block head %} 4 | {{ super() }} 5 | {% endblock %} 6 | {% block content %} 7 |

You are confirmed!

8 |

9 | Name: {{ name }}
10 | Email: {{ email }}
11 | Number of attendees: {{ partysize }} 12 |

13 | View all attendees 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from app import DB 2 | 3 | class Guest(DB.Model): 4 | """Simple database model to track event attendees.""" 5 | 6 | __tablename__ = 'guests' 7 | id = DB.Column(DB.Integer, primary_key=True) 8 | name = DB.Column(DB.String(80)) 9 | email = DB.Column(DB.String(120)) 10 | partysize = DB.Column(DB.Integer, default=1) 11 | 12 | def __init__(self, name=None, email=None, partysize=1): 13 | self.name = name 14 | self.email = email 15 | self.partysize = partysize 16 | -------------------------------------------------------------------------------- /app/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /app/templates/guest_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Registered Guests{% endblock %} 3 | {% block head %} 4 | {{ super() }} 5 | {% endblock %} 6 | {% block content %} 7 |

Registered Guests

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for guest in guests %} 18 | 19 | 20 | 21 | 22 | 23 | {% endfor %} 24 |
NameEmailAttendees
{{ guest.name }}{{ guest.email }}{% if guest.partysize %}{{ guest.partysize }}{% else %}1{% endif %}
25 | 28 | {% endblock %} -------------------------------------------------------------------------------- /app/migrations/versions/32b48058cd02_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 32b48058cd02 4 | Revises: 791cd7d80402 5 | Create Date: 2017-05-02 17:00:45.486682 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '32b48058cd02' 14 | down_revision = '791cd7d80402' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('guests', sa.Column('partysize', sa.Integer(), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('guests', 'partysize') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /app/migrations/versions/791cd7d80402_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 791cd7d80402 4 | Revises: 5 | Create Date: 2017-05-02 16:01:24.803337 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '791cd7d80402' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('guests', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(length=80), nullable=True), 24 | sa.Column('email', sa.String(length=120), nullable=True), 25 | sa.PrimaryKeyConstraint('id') 26 | ) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_table('guests') 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /app/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | {% block title %}{% endblock %} 6 | 8 | 10 | 13 | {% endblock %} 14 | 15 | 16 |
{% block content %}{% endblock %}
17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /app/templates/guest_registration.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Guest Registration{% endblock %} 3 | {% block head %} 4 | {{ super() }} 5 | {% endblock %} 6 | {% block content %} 7 |

Guest Registration

8 |
9 |
10 | 11 | 12 | Let us know who you are. 13 |
14 |
15 | 16 | 17 | Your email address in case we need to contact you. 18 |
19 |
20 | 21 | 22 | Tell us how many people will be attending. 23 |
24 | 25 |
26 | {% endblock %} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask, request, render_template 4 | from flask_migrate import Migrate 5 | from flask_sqlalchemy import SQLAlchemy 6 | 7 | APP = Flask(__name__) 8 | APP.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 9 | 10 | APP.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql+psycopg2://%s:%s@%s/%s' % ( 11 | # ARGS.dbuser, ARGS.dbpass, ARGS.dbhost, ARGS.dbname 12 | os.environ['DBUSER'], os.environ['DBPASS'], os.environ['DBHOST'], os.environ['DBNAME'] 13 | ) 14 | 15 | # initialize the database connection 16 | DB = SQLAlchemy(APP) 17 | 18 | # initialize database migration management 19 | MIGRATE = Migrate(APP, DB) 20 | 21 | from models import * 22 | 23 | 24 | @APP.route('/') 25 | def view_registered_guests(): 26 | guests = Guest.query.all() 27 | return render_template('guest_list.html', guests=guests) 28 | 29 | 30 | @APP.route('/register', methods = ['GET']) 31 | def view_registration_form(): 32 | return render_template('guest_registration.html') 33 | 34 | 35 | @APP.route('/register', methods = ['POST']) 36 | def register_guest(): 37 | name = request.form.get('name') 38 | email = request.form.get('email') 39 | partysize = request.form.get('partysize') 40 | if not partysize or partysize=='': 41 | partysize = 1 42 | 43 | guest = Guest(name, email, partysize) 44 | DB.session.add(guest) 45 | DB.session.commit() 46 | 47 | return render_template('guest_confirmation.html', 48 | name=name, email=email, partysize=partysize) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask on Docker with external PostgreSQL database 2 | 3 | A simple Python Flask application running in a Docker container and connecting via SQLAlchemy to a PostgreSQL database. 4 | 5 | The database connection information is specified via environment variables `DBHOST`, `DBPASS`, `DBUSER`, and `DBNAME`. This app always uses the default PostgreSQL port. 6 | 7 | There are two [releases](https://github.com/Azure-Samples/docker-flask-postgres/releases) of this app. Version [`0.1-initialapp`](https://github.com/Azure-Samples/docker-flask-postgres/releases/tag/0.1-initialapp) demonstrates a complete app, whereas version [`0.2-migration`](https://github.com/Azure-Samples/docker-flask-postgres/releases/tag/0.2-migration) introduces model changes and a database migration. 8 | 9 | Download one of the releases then build and run in Docker locally via: 10 | 11 | ``` 12 | docker build -t docker-flask-sample . 13 | docker run -it --env DBPASS="" --env DBHOST="" --env DBUSER="" --env DBNAME="" -p 5000:5000 docker-flask-sample 14 | ``` 15 | The app can be reached in your browser at `http://127.0.0.1:5000`. 16 | 17 | # Contributing 18 | 19 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 20 | -------------------------------------------------------------------------------- /app/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | import logging 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | logger = logging.getLogger('alembic.env') 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | from flask import current_app 21 | config.set_main_option('sqlalchemy.url', 22 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 23 | target_metadata = current_app.extensions['migrate'].db.metadata 24 | 25 | # other values from the config, defined by the needs of env.py, 26 | # can be acquired: 27 | # my_important_option = config.get_main_option("my_important_option") 28 | # ... etc. 29 | 30 | 31 | def run_migrations_offline(): 32 | """Run migrations in 'offline' mode. 33 | 34 | This configures the context with just a URL 35 | and not an Engine, though an Engine is acceptable 36 | here as well. By skipping the Engine creation 37 | we don't even need a DBAPI to be available. 38 | 39 | Calls to context.execute() here emit the given string to the 40 | script output. 41 | 42 | """ 43 | url = config.get_main_option("sqlalchemy.url") 44 | context.configure(url=url) 45 | 46 | with context.begin_transaction(): 47 | context.run_migrations() 48 | 49 | 50 | def run_migrations_online(): 51 | """Run migrations in 'online' mode. 52 | 53 | In this scenario we need to create an Engine 54 | and associate a connection with the context. 55 | 56 | """ 57 | 58 | # this callback is used to prevent an auto-migration from being generated 59 | # when there are no changes to the schema 60 | # reference: http://alembic.readthedocs.org/en/latest/cookbook.html 61 | def process_revision_directives(context, revision, directives): 62 | if getattr(config.cmd_opts, 'autogenerate', False): 63 | script = directives[0] 64 | if script.upgrade_ops.is_empty(): 65 | directives[:] = [] 66 | logger.info('No changes in schema detected.') 67 | 68 | engine = engine_from_config(config.get_section(config.config_ini_section), 69 | prefix='sqlalchemy.', 70 | poolclass=pool.NullPool) 71 | 72 | connection = engine.connect() 73 | context.configure(connection=connection, 74 | target_metadata=target_metadata, 75 | process_revision_directives=process_revision_directives, 76 | **current_app.extensions['migrate'].configure_args) 77 | 78 | try: 79 | with context.begin_transaction(): 80 | context.run_migrations() 81 | finally: 82 | connection.close() 83 | 84 | if context.is_offline_mode(): 85 | run_migrations_offline() 86 | else: 87 | run_migrations_online() 88 | --------------------------------------------------------------------------------