├── .env.dev-sample ├── .env.prod-sample ├── .env.prod.db-sample ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.prod.yml ├── docker-compose.yml └── services ├── nginx ├── Dockerfile └── nginx.conf └── web ├── Dockerfile ├── Dockerfile.prod ├── entrypoint.prod.sh ├── entrypoint.sh ├── manage.py ├── project ├── __init__.py ├── config.py ├── media │ └── .gitkeep └── static │ └── hello.txt └── requirements.txt /.env.dev-sample: -------------------------------------------------------------------------------- 1 | FLASK_APP=project/__init__.py 2 | FLASK_DEBUG=1 3 | DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_dev 4 | SQL_HOST=db 5 | SQL_PORT=5432 6 | DATABASE=postgres 7 | APP_FOLDER=/usr/src/app 8 | -------------------------------------------------------------------------------- /.env.prod-sample: -------------------------------------------------------------------------------- 1 | FLASK_APP=project/__init__.py 2 | FLASK_DEBUG=0 3 | DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_prod 4 | SQL_HOST=db 5 | SQL_PORT=5432 6 | DATABASE=postgres 7 | APP_FOLDER=/home/app/web 8 | -------------------------------------------------------------------------------- /.env.prod.db-sample: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=hello_flask 2 | POSTGRES_PASSWORD=hello_flask 3 | POSTGRES_DB=hello_flask_prod 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache 3 | .DS_Store 4 | .env.dev 5 | .env.prod 6 | .env.prod.db 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TestDriven.io 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dockerizing Flask with Postgres, Gunicorn, and Nginx 2 | 3 | ## Want to learn how to build this? 4 | 5 | Check out the [tutorial](https://testdriven.io/blog/dockerizing-flask-with-postgres-gunicorn-and-nginx). 6 | 7 | ## Want to use this project? 8 | 9 | ### Development 10 | 11 | Uses the default Flask development server. 12 | 13 | 1. Rename *.env.dev-sample* to *.env.dev*. 14 | 1. Update the environment variables in the *docker-compose.yml* and *.env.dev* files. 15 | - (M1 chip only) Remove `-slim-buster` from the Python dependency in `services/web/Dockerfile` to suppress an issue with installing psycopg2 16 | 1. Build the images and run the containers: 17 | 18 | ```sh 19 | $ docker-compose up -d --build 20 | ``` 21 | 22 | Test it out at [http://localhost:5001](http://localhost:5001). The "web" folder is mounted into the container and your code changes apply automatically. 23 | 24 | ### Production 25 | 26 | Uses gunicorn + nginx. 27 | 28 | 1. Rename *.env.prod-sample* to *.env.prod* and *.env.prod.db-sample* to *.env.prod.db*. Update the environment variables. 29 | 1. Build the images and run the containers: 30 | 31 | ```sh 32 | $ docker-compose -f docker-compose.prod.yml up -d --build 33 | ``` 34 | 35 | Test it out at [http://localhost:1337](http://localhost:1337). No mounted folders. To apply changes, the image must be re-built. 36 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web: 5 | build: 6 | context: ./services/web 7 | dockerfile: Dockerfile.prod 8 | command: gunicorn --bind 0.0.0.0:5000 manage:app 9 | volumes: 10 | - static_volume:/home/app/web/project/static 11 | - media_volume:/home/app/web/project/media 12 | expose: 13 | - 5000 14 | env_file: 15 | - ./.env.prod 16 | depends_on: 17 | - db 18 | db: 19 | image: postgres:13 20 | volumes: 21 | - postgres_data_prod:/var/lib/postgresql/data/ 22 | env_file: 23 | - ./.env.prod.db 24 | nginx: 25 | build: ./services/nginx 26 | volumes: 27 | - static_volume:/home/app/web/project/static 28 | - media_volume:/home/app/web/project/media 29 | ports: 30 | - 1337:80 31 | depends_on: 32 | - web 33 | 34 | volumes: 35 | postgres_data_prod: 36 | static_volume: 37 | media_volume: 38 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web: 5 | build: ./services/web 6 | command: python manage.py run -h 0.0.0.0 7 | volumes: 8 | - ./services/web/:/usr/src/app/ 9 | ports: 10 | - 5001:5000 11 | env_file: 12 | - ./.env.dev 13 | depends_on: 14 | - db 15 | db: 16 | image: postgres:13 17 | volumes: 18 | - postgres_data:/var/lib/postgresql/data/ 19 | environment: 20 | - POSTGRES_USER=hello_flask 21 | - POSTGRES_PASSWORD=hello_flask 22 | - POSTGRES_DB=hello_flask_dev 23 | 24 | volumes: 25 | postgres_data: 26 | -------------------------------------------------------------------------------- /services/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.25 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | COPY nginx.conf /etc/nginx/conf.d 5 | -------------------------------------------------------------------------------- /services/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream hello_flask { 2 | server web:5000; 3 | } 4 | 5 | server { 6 | 7 | listen 80; 8 | 9 | location / { 10 | proxy_pass http://hello_flask; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header Host $host; 13 | proxy_redirect off; 14 | } 15 | 16 | location /static/ { 17 | alias /home/app/web/project/static/; 18 | } 19 | 20 | location /media/ { 21 | alias /home/app/web/project/media/; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /services/web/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM python:3.11.3-slim-buster 3 | 4 | # set work directory 5 | WORKDIR /usr/src/app 6 | 7 | # set environment variables 8 | ENV PYTHONDONTWRITEBYTECODE 1 9 | ENV PYTHONUNBUFFERED 1 10 | 11 | # install system dependencies 12 | RUN apt-get update && apt-get install -y netcat 13 | 14 | # install dependencies 15 | RUN pip install --upgrade pip 16 | COPY ./requirements.txt /usr/src/app/requirements.txt 17 | RUN pip install -r requirements.txt 18 | 19 | # copy project 20 | COPY . /usr/src/app/ 21 | 22 | # run entrypoint.sh 23 | ENTRYPOINT ["/usr/src/app/entrypoint.sh"] 24 | -------------------------------------------------------------------------------- /services/web/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | ########### 2 | # BUILDER # 3 | ########### 4 | 5 | # pull official base image 6 | FROM python:3.11.3-slim-buster as builder 7 | 8 | # set work directory 9 | WORKDIR /usr/src/app 10 | 11 | # set environment variables 12 | ENV PYTHONDONTWRITEBYTECODE 1 13 | ENV PYTHONUNBUFFERED 1 14 | 15 | # install system dependencies 16 | RUN apt-get update && \ 17 | apt-get install -y --no-install-recommends gcc 18 | 19 | # lint 20 | RUN pip install --upgrade pip 21 | RUN pip install flake8==6.0.0 22 | COPY . /usr/src/app/ 23 | RUN flake8 --ignore=E501,F401 . 24 | 25 | # install python dependencies 26 | COPY ./requirements.txt . 27 | RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt 28 | 29 | 30 | ######### 31 | # FINAL # 32 | ######### 33 | 34 | # pull official base image 35 | FROM python:3.11.3-slim-buster 36 | 37 | # create directory for the app user 38 | RUN mkdir -p /home/app 39 | 40 | # create the app user 41 | RUN addgroup --system app && adduser --system --group app 42 | 43 | # create the appropriate directories 44 | ENV HOME=/home/app 45 | ENV APP_HOME=/home/app/web 46 | RUN mkdir $APP_HOME 47 | WORKDIR $APP_HOME 48 | 49 | # install dependencies 50 | RUN apt-get update && apt-get install -y --no-install-recommends netcat 51 | COPY --from=builder /usr/src/app/wheels /wheels 52 | COPY --from=builder /usr/src/app/requirements.txt . 53 | RUN pip install --upgrade pip 54 | RUN pip install --no-cache /wheels/* 55 | 56 | # copy entrypoint-prod.sh 57 | COPY ./entrypoint.prod.sh $APP_HOME 58 | 59 | # copy project 60 | COPY . $APP_HOME 61 | 62 | # chown all the files to the app user 63 | RUN chown -R app:app $APP_HOME 64 | 65 | # change to the app user 66 | USER app 67 | 68 | # run entrypoint.prod.sh 69 | ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"] 70 | -------------------------------------------------------------------------------- /services/web/entrypoint.prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$DATABASE" = "postgres" ] 4 | then 5 | echo "Waiting for postgres..." 6 | 7 | while ! nc -z $SQL_HOST $SQL_PORT; do 8 | sleep 0.1 9 | done 10 | 11 | echo "PostgreSQL started" 12 | fi 13 | 14 | exec "$@" 15 | -------------------------------------------------------------------------------- /services/web/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$DATABASE" = "postgres" ] 4 | then 5 | echo "Waiting for postgres..." 6 | 7 | while ! nc -z $SQL_HOST $SQL_PORT; do 8 | sleep 0.1 9 | done 10 | 11 | echo "PostgreSQL started" 12 | fi 13 | 14 | python manage.py create_db 15 | 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /services/web/manage.py: -------------------------------------------------------------------------------- 1 | from flask.cli import FlaskGroup 2 | 3 | from project import app, db, User 4 | 5 | 6 | cli = FlaskGroup(app) 7 | 8 | 9 | @cli.command("create_db") 10 | def create_db(): 11 | db.drop_all() 12 | db.create_all() 13 | db.session.commit() 14 | 15 | 16 | @cli.command("seed_db") 17 | def seed_db(): 18 | db.session.add(User(email="michael@mherman.org")) 19 | db.session.commit() 20 | 21 | 22 | if __name__ == "__main__": 23 | cli() 24 | -------------------------------------------------------------------------------- /services/web/project/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import ( 4 | Flask, 5 | jsonify, 6 | send_from_directory, 7 | request, 8 | ) 9 | from flask_sqlalchemy import SQLAlchemy 10 | from werkzeug.utils import secure_filename 11 | 12 | 13 | app = Flask(__name__) 14 | app.config.from_object("project.config.Config") 15 | db = SQLAlchemy(app) 16 | 17 | 18 | class User(db.Model): 19 | __tablename__ = "users" 20 | 21 | id = db.Column(db.Integer, primary_key=True) 22 | email = db.Column(db.String(128), unique=True, nullable=False) 23 | active = db.Column(db.Boolean(), default=True, nullable=False) 24 | 25 | def __init__(self, email): 26 | self.email = email 27 | 28 | 29 | @app.route("/") 30 | def hello_world(): 31 | return jsonify(hello="world") 32 | 33 | 34 | @app.route("/static/") 35 | def staticfiles(filename): 36 | return send_from_directory(app.config["STATIC_FOLDER"], filename) 37 | 38 | 39 | @app.route("/media/") 40 | def mediafiles(filename): 41 | return send_from_directory(app.config["MEDIA_FOLDER"], filename) 42 | 43 | 44 | @app.route("/upload", methods=["GET", "POST"]) 45 | def upload_file(): 46 | if request.method == "POST": 47 | file = request.files["file"] 48 | filename = secure_filename(file.filename) 49 | file.save(os.path.join(app.config["MEDIA_FOLDER"], filename)) 50 | return """ 51 | 52 | upload new File 53 |
54 |

55 |

56 | """ 57 | -------------------------------------------------------------------------------- /services/web/project/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class Config(object): 8 | SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite://") 9 | SQLALCHEMY_TRACK_MODIFICATIONS = False 10 | STATIC_FOLDER = f"{os.getenv('APP_FOLDER')}/project/static" 11 | MEDIA_FOLDER = f"{os.getenv('APP_FOLDER')}/project/media" 12 | -------------------------------------------------------------------------------- /services/web/project/media/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/flask-on-docker/c51257d182ad9c1e1b5f6addb633f74a358c0f71/services/web/project/media/.gitkeep -------------------------------------------------------------------------------- /services/web/project/static/hello.txt: -------------------------------------------------------------------------------- 1 | hi! 2 | -------------------------------------------------------------------------------- /services/web/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.2 2 | Flask-SQLAlchemy==3.0.3 3 | gunicorn==20.1.0 4 | psycopg2-binary==2.9.6 5 | --------------------------------------------------------------------------------