From e4e986820adfcc4c4b30599e5de0c88e34f49b97 Mon Sep 17 00:00:00 2001 From: ameye Date: Fri, 6 Dec 2024 06:57:12 -0500 Subject: [PATCH] first commit --- .env | 7 +++ .env.dev-sample | 7 +++ .env.prod-sample | 7 +++ .env.prod.db-sample | 3 ++ .gitignore | 6 +++ LICENSE | 21 ++++++++ README.md | 47 ++++++++++++++++++ docker-compose.prod.yml | 37 ++++++++++++++ docker-compose.yml | 25 ++++++++++ env.dev | 7 +++ services/nginx/Dockerfile | 4 ++ services/nginx/nginx.conf | 24 ++++++++++ services/web/Dockerfile | 23 +++++++++ services/web/Dockerfile.prod | 69 +++++++++++++++++++++++++++ services/web/entrypoint.prod.sh | 14 ++++++ services/web/entrypoint.sh | 16 +++++++ services/web/manage.py | 23 +++++++++ services/web/project/__init__.py | 56 ++++++++++++++++++++++ services/web/project/config.py | 11 +++++ services/web/project/media/.gitkeep | 0 services/web/project/static/hello.txt | 1 + services/web/requirements.txt | 4 ++ 22 files changed, 412 insertions(+) create mode 100644 .env create mode 100644 .env.dev-sample create mode 100644 .env.prod-sample create mode 100644 .env.prod.db-sample create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100644 env.dev create mode 100644 services/nginx/Dockerfile create mode 100644 services/nginx/nginx.conf create mode 100644 services/web/Dockerfile create mode 100644 services/web/Dockerfile.prod create mode 100755 services/web/entrypoint.prod.sh create mode 100755 services/web/entrypoint.sh create mode 100644 services/web/manage.py create mode 100644 services/web/project/__init__.py create mode 100644 services/web/project/config.py create mode 100644 services/web/project/media/.gitkeep create mode 100644 services/web/project/static/hello.txt create mode 100644 services/web/requirements.txt diff --git a/.env b/.env new file mode 100644 index 0000000..789ff86 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +FLASK_APP=project/__init__.py +FLASK_DEBUG=1 +DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_dev +SQL_HOST=db +SQL_PORT=5432 +DATABASE=postgres +APP_FOLDER=/usr/src/app \ No newline at end of file diff --git a/.env.dev-sample b/.env.dev-sample new file mode 100644 index 0000000..396ab02 --- /dev/null +++ b/.env.dev-sample @@ -0,0 +1,7 @@ +FLASK_APP=project/__init__.py +FLASK_DEBUG=1 +DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_dev +SQL_HOST=db +SQL_PORT=5432 +DATABASE=postgres +APP_FOLDER=/usr/src/app diff --git a/.env.prod-sample b/.env.prod-sample new file mode 100644 index 0000000..451cf9e --- /dev/null +++ b/.env.prod-sample @@ -0,0 +1,7 @@ +FLASK_APP=project/__init__.py +FLASK_DEBUG=0 +DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_prod +SQL_HOST=db +SQL_PORT=5432 +DATABASE=postgres +APP_FOLDER=/home/app/web diff --git a/.env.prod.db-sample b/.env.prod.db-sample new file mode 100644 index 0000000..3acb993 --- /dev/null +++ b/.env.prod.db-sample @@ -0,0 +1,3 @@ +POSTGRES_USER=hello_flask +POSTGRES_PASSWORD=hello_flask +POSTGRES_DB=hello_flask_prod diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0747bae --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.pyc +__pycache +.DS_Store +.env.dev +.env.prod +.env.prod.db diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29812bb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 TestDriven.io + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c48883 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Dockerizing Flask with Postgres, Gunicorn, and Nginx + +## Want to learn how to build this? + +Check out the [tutorial](https://testdriven.io/blog/dockerizing-flask-with-postgres-gunicorn-and-nginx). + +## Want to use this project? + +### Development + +Uses the default Flask development server. + +1. Rename *.env.dev-sample* to *.env.dev*. +1. Update the environment variables in the *docker-compose.yml* and *.env.dev* files. + - (M1 chip only) Remove `-slim-buster` from the Python dependency in `services/web/Dockerfile` to suppress an issue with installing psycopg2 +1. Build the images and run the containers: + + ```sh + $ docker-compose up -d --build + ``` + + Test it out at [http://localhost:5001](http://localhost:5001). The "web" folder is mounted into the container and your code changes apply automatically. + +### Production + +Uses gunicorn + nginx. + +1. Rename *.env.prod-sample* to *.env.prod* and *.env.prod.db-sample* to *.env.prod.db*. Update the environment variables. +1. Build the images and run the containers: + + ```sh + $ docker-compose -f docker-compose.prod.yml up -d --build + ``` + + Test it out at [http://localhost:1337](http://localhost:1337). No mounted folders. To apply changes, the image must be re-built. + + + +git init + +git add . + +git commit -m "first commit" + +git remote add origin https://gitlab.chiefsoft.net/MERMS/MermsCoreBackendFlask.git + +git push -u origin master diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..fdb3aae --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + web: + build: + context: ./services/web + dockerfile: Dockerfile.prod + command: gunicorn --bind 0.0.0.0:5000 manage:app + volumes: + - static_volume:/home/app/web/project/static + - media_volume:/home/app/web/project/media + expose: + - 5000 + env_file: + - ./.env.prod + depends_on: + - db + db: + image: postgres:13 + volumes: + - postgres_data_prod:/var/lib/postgresql/data/ + env_file: + - ./.env.prod.db + nginx: + build: ./services/nginx + volumes: + - static_volume:/home/app/web/project/static + - media_volume:/home/app/web/project/media + ports: + - 1337:80 + depends_on: + - web + +volumes: + postgres_data_prod: + static_volume: + media_volume: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a357095 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3.8' + +services: + web: + build: ./services/web + command: python manage.py run -h 0.0.0.0 + volumes: + - ./services/web/:/usr/src/app/ + ports: + - 5001:5000 + env_file: + - ./.env.dev + depends_on: + - db + db: + image: postgres:13 + volumes: + - postgres_data:/var/lib/postgresql/data/ + environment: + - POSTGRES_USER=hello_flask + - POSTGRES_PASSWORD=hello_flask + - POSTGRES_DB=hello_flask_dev + +volumes: + postgres_data: \ No newline at end of file diff --git a/env.dev b/env.dev new file mode 100644 index 0000000..789ff86 --- /dev/null +++ b/env.dev @@ -0,0 +1,7 @@ +FLASK_APP=project/__init__.py +FLASK_DEBUG=1 +DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_dev +SQL_HOST=db +SQL_PORT=5432 +DATABASE=postgres +APP_FOLDER=/usr/src/app \ No newline at end of file diff --git a/services/nginx/Dockerfile b/services/nginx/Dockerfile new file mode 100644 index 0000000..8328a0e --- /dev/null +++ b/services/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.25 + +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d diff --git a/services/nginx/nginx.conf b/services/nginx/nginx.conf new file mode 100644 index 0000000..02689b8 --- /dev/null +++ b/services/nginx/nginx.conf @@ -0,0 +1,24 @@ +upstream hello_flask { + server web:5000; +} + +server { + + listen 80; + + location / { + proxy_pass http://hello_flask; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + } + + location /static/ { + alias /home/app/web/project/static/; + } + + location /media/ { + alias /home/app/web/project/media/; + } + +} diff --git a/services/web/Dockerfile b/services/web/Dockerfile new file mode 100644 index 0000000..54a20c6 --- /dev/null +++ b/services/web/Dockerfile @@ -0,0 +1,23 @@ +# pull official base image +FROM python:3.11.3-slim-buster + +# set work directory +WORKDIR /usr/src/app + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install system dependencies +RUN apt-get update && apt-get install -y netcat + +# install dependencies +RUN pip install --upgrade pip +COPY ./requirements.txt /usr/src/app/requirements.txt +RUN pip install -r requirements.txt + +# copy project +COPY . /usr/src/app/ + +# run entrypoint.sh +ENTRYPOINT ["/usr/src/app/entrypoint.sh"] diff --git a/services/web/Dockerfile.prod b/services/web/Dockerfile.prod new file mode 100644 index 0000000..4974c67 --- /dev/null +++ b/services/web/Dockerfile.prod @@ -0,0 +1,69 @@ +########### +# BUILDER # +########### + +# pull official base image +FROM python:3.11.3-slim-buster as builder + +# set work directory +WORKDIR /usr/src/app + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install system dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends gcc + +# lint +RUN pip install --upgrade pip +RUN pip install flake8==6.0.0 +COPY . /usr/src/app/ +RUN flake8 --ignore=E501,F401 . + +# install python dependencies +COPY ./requirements.txt . +RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt + + +######### +# FINAL # +######### + +# pull official base image +FROM python:3.11.3-slim-buster + +# create directory for the app user +RUN mkdir -p /home/app + +# create the app user +RUN addgroup --system app && adduser --system --group app + +# create the appropriate directories +ENV HOME=/home/app +ENV APP_HOME=/home/app/web +RUN mkdir $APP_HOME +WORKDIR $APP_HOME + +# install dependencies +RUN apt-get update && apt-get install -y --no-install-recommends netcat +COPY --from=builder /usr/src/app/wheels /wheels +COPY --from=builder /usr/src/app/requirements.txt . +RUN pip install --upgrade pip +RUN pip install --no-cache /wheels/* + +# copy entrypoint-prod.sh +COPY ./entrypoint.prod.sh $APP_HOME + +# copy project +COPY . $APP_HOME + +# chown all the files to the app user +RUN chown -R app:app $APP_HOME + +# change to the app user +USER app + +# run entrypoint.prod.sh +ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"] diff --git a/services/web/entrypoint.prod.sh b/services/web/entrypoint.prod.sh new file mode 100755 index 0000000..37fa201 --- /dev/null +++ b/services/web/entrypoint.prod.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +if [ "$DATABASE" = "postgres" ] +then + echo "Waiting for postgres..." + + while ! nc -z $SQL_HOST $SQL_PORT; do + sleep 0.1 + done + + echo "PostgreSQL started" +fi + +exec "$@" diff --git a/services/web/entrypoint.sh b/services/web/entrypoint.sh new file mode 100755 index 0000000..3069d16 --- /dev/null +++ b/services/web/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +if [ "$DATABASE" = "postgres" ] +then + echo "Waiting for postgres..." + + while ! nc -z $SQL_HOST $SQL_PORT; do + sleep 0.1 + done + + echo "PostgreSQL started" +fi + +python manage.py create_db + +exec "$@" diff --git a/services/web/manage.py b/services/web/manage.py new file mode 100644 index 0000000..d9c3f04 --- /dev/null +++ b/services/web/manage.py @@ -0,0 +1,23 @@ +from flask.cli import FlaskGroup + +from project import app, db, User + + +cli = FlaskGroup(app) + + +@cli.command("create_db") +def create_db(): + db.drop_all() + db.create_all() + db.session.commit() + + +@cli.command("seed_db") +def seed_db(): + db.session.add(User(email="michael@mherman.org")) + db.session.commit() + + +if __name__ == "__main__": + cli() diff --git a/services/web/project/__init__.py b/services/web/project/__init__.py new file mode 100644 index 0000000..ddde1b0 --- /dev/null +++ b/services/web/project/__init__.py @@ -0,0 +1,56 @@ +import os + +from flask import ( + Flask, + jsonify, + send_from_directory, + request, +) +from flask_sqlalchemy import SQLAlchemy +from werkzeug.utils import secure_filename + + +app = Flask(__name__) +app.config.from_object("project.config.Config") +db = SQLAlchemy(app) + + +class User(db.Model): + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(128), unique=True, nullable=False) + active = db.Column(db.Boolean(), default=True, nullable=False) + + def __init__(self, email): + self.email = email + + +@app.route("/") +def hello_world(): + return jsonify(hello="ameye world") + + +@app.route("/static/") +def staticfiles(filename): + return send_from_directory(app.config["STATIC_FOLDER"], filename) + + +@app.route("/media/") +def mediafiles(filename): + return send_from_directory(app.config["MEDIA_FOLDER"], filename) + + +@app.route("/upload", methods=["GET", "POST"]) +def upload_file(): + if request.method == "POST": + file = request.files["file"] + filename = secure_filename(file.filename) + file.save(os.path.join(app.config["MEDIA_FOLDER"], filename)) + return """ + + upload new File +
+

+

+ """ diff --git a/services/web/project/config.py b/services/web/project/config.py new file mode 100644 index 0000000..90eba5d --- /dev/null +++ b/services/web/project/config.py @@ -0,0 +1,11 @@ +import os + + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Config(object): + SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite://") + SQLALCHEMY_TRACK_MODIFICATIONS = False + STATIC_FOLDER = f"{os.getenv('APP_FOLDER')}/project/static" + MEDIA_FOLDER = f"{os.getenv('APP_FOLDER')}/project/media" diff --git a/services/web/project/media/.gitkeep b/services/web/project/media/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/web/project/static/hello.txt b/services/web/project/static/hello.txt new file mode 100644 index 0000000..32aad8c --- /dev/null +++ b/services/web/project/static/hello.txt @@ -0,0 +1 @@ +hi! diff --git a/services/web/requirements.txt b/services/web/requirements.txt new file mode 100644 index 0000000..d7edd53 --- /dev/null +++ b/services/web/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.3.2 +Flask-SQLAlchemy==3.0.3 +gunicorn==20.1.0 +psycopg2-binary==2.9.6