Home > Enterprise >  Dockerizing flask-sockerio app with nginx gunicorn postgres "Application object must be cal
Dockerizing flask-sockerio app with nginx gunicorn postgres "Application object must be cal

Time:09-12

I have a flask application that I want to host on my VPS. I already have a domain name that is pointed to the VPS's IP.

My project structure is like this:

 -nginx
|   \nginx.conf
 -main.py
 -models.py
 -wsgi.py
 -requirements.txt
 -Dockerfile
 -docker-compose.yml

nginx\nginx.conf:

server {
    listen 80;
    server_name mydomain.com;
    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://0.0.0.0:5000;
    }
    location /socket.io {
        include proxy_params;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://0.0.0.0:5000/socket.io;
    }
}

Dockerfile:

FROM python:3.8.3
WORKDIR /
COPY . /
RUN pip install --no-cache-dir -r requirements.txt

docker-compose.yml:

version: '3.8'
services:
    web:
        build: .
        command: gunicorn --bind 0.0.0.0:5000 -k gevent -w 1 wsgi:socket_
        ports:
            - 5000:5000
        environment:
            - FLASK_APP=main.py
        env_file:
            - .env
        depends_on:
            - postgres

    postgres:
        image: postgres
        environment:
            - POSTGRES_USER=myuser
            - POSTGRES_PASSWORD=mypassword
            - POSTGRES_DB=mydb
        volumes:
            - flask_polls_data:/var/lib/postgresql/data

    pgadmin:
        image: dpage/pgadmin4
        environment:
            - PGADMIN_DEFAULT_EMAIL=mymaiil
            - PGADMIN_DEFAULT_PASSWORD=mypass
            - PGADMIN_LISTEN_PORT=80
        ports:
            - 15432:80
        volumes:
            - pgadmin:/var/lib/pgadmin
        depends_on:
            - postgres

    nginx:
        image: nginx:1.19-alpine
        ports:
            - 1337:80
        depends_on:
            - web


volumes:
    flask_polls_data:
    postgres:
    pgadmin:

wsgi.py:

from main import app, socket_

if __name__ == "__main__":
    socket_.run(app, debug=False)

The URI for the database is in .env:

URI=postgresql://myuser:mypassword@postgres:5432/mydb

I set the app config for URI like:

app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("URI", None)

I get this error when I run docker-compose up --build

web_1 | Application object must be callable.

models.py:

from flask_sqlalchemy import SQLAlchemy


db = SQLAlchemy()


class User(db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text, unique=True, nullable=False)

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"<User name: {self.name}>"

And this is main.py:

import os

from flask import Flask, render_template
from flask_socketio import SocketIO
from flask_migrate import Migrate
from sqlalchemy.orm.session import Session
from dotenv import load_dotenv

from models import *

load_dotenv()

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socket_ = SocketIO(app)
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("URI", None)

db.init_app(app)
migrate = Migrate(app, db)
with app.app_context():
    db.create_all()
sess: Session = db.session  # type: ignore

@app.route("/test", methods=["GET"])
def test():
    return {
        "hello": "world"
    }

if __name__ == '__main__':
    socket_.run(app)

This is what I see in the console:

Attaching to test_postgres_1, test_pgadmin_1, test_web_1, test_nginx_1
nginx_1     | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
nginx_1     | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
nginx_1     | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
postgres_1  |
postgres_1  | PostgreSQL Database directory appears to contain a database; Skipping initialization
postgres_1  |
postgres_1  | 2022-09-10 09:06:25.043 UTC [1] LOG:  starting PostgreSQL 14.5 (Debian 14.5-1.pgdg110 1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
postgres_1  | 2022-09-10 09:06:25.045 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
postgres_1  | 2022-09-10 09:06:25.045 UTC [1] LOG:  listening on IPv6 address "::", port 5432
postgres_1  | 2022-09-10 09:06:25.049 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres_1  | 2022-09-10 09:06:25.058 UTC [24] LOG:  database system was shut down at 2022-09-10 09:05:55 UTC
postgres_1  | 2022-09-10 09:06:25.073 UTC [1] LOG:  database system is ready to accept connections
nginx_1     | 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
nginx_1     | 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
nginx_1     | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
nginx_1     | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
nginx_1     | /docker-entrypoint.sh: Configuration complete; ready for start up
web_1       | [2022-09-10 09:06:27  0000] [1] [INFO] Starting gunicorn 20.1.0
web_1       | [2022-09-10 09:06:27  0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
web_1       | [2022-09-10 09:06:27  0000] [1] [INFO] Using worker: gevent
web_1       | [2022-09-10 09:06:27  0000] [7] [INFO] Booting worker with pid: 7
web_1       | /usr/local/lib/python3.8/site-packages/flask_sqlalchemy/__init__.py:872: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
web_1       |   warnings.warn(FSADeprecationWarning(
web_1       | Application object must be callable.
web_1       | [2022-09-10 09:06:30  0000] [7] [INFO] Worker exiting (pid: 7)
web_1       | [2022-09-10 09:06:30  0000] [1] [INFO] Shutting down: Master
web_1       | [2022-09-10 09:06:30  0000] [1] [INFO] Reason: App failed to load.
test_web_1 exited with code 4

If I go to to VPS IP I get the default nginx page where it tells me further configuration is required. If I go to domain I get 502 Bad Gateway.

CodePudding user response:

To fix the host name issue, I had to remove all the network directives in docker-compose.yml and change the gunicorn command to use eventlet worker class and point to app not socket_.

services:
    web:
        command: gunicorn wsgi:app --worker-class eventlet -w 1 --bind 0.0.0.0:5000 --reload

I have also changed the nginx.conf with the example on gunicorn docs.

worker_processes 1;

user nobody nogroup;
# 'user nobody nobody;' for systems with 'nobody' as a group instead
error_log  /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
  worker_connections 1024; # increase if you have lots of clients
  accept_mutex off; # set to 'on' if nginx worker_processes > 1
  # 'use epoll;' to enable for Linux 2.6 
  # 'use kqueue;' to enable for FreeBSD, OSX
}

http {
  include mime.types;
  # fallback in case we can't determine a type
  default_type application/octet-stream;
  access_log /var/log/nginx/access.log combined;
  sendfile on;

  upstream app_server {
    # fail_timeout=0 means we always retry an upstream even if it failed
    # to return a good HTTP response

    # for UNIX domain socket setups
    server unix:/tmp/gunicorn.sock fail_timeout=0;

    # for a TCP configuration
    # server 192.168.0.7:8000 fail_timeout=0;
  }

  server {
    # if no Host match, close the connection to prevent host spoofing
    listen 80 default_server;
    return 444;
  }

  server {
    # use 'listen 80 deferred;' for Linux
    # use 'listen 80 accept_filter=httpready;' for FreeBSD
    listen 80;
    client_max_body_size 4G;

    # set the correct host(s) for your site
    server_name mydomain.com www.mydomain.com;

    keepalive_timeout 5;

    # path for static files
    root /path/to/app/current/public;

    location / {
      # checks for static file, if not found proxy to app
      try_files $uri @proxy_to_app;
    }

    location /socket.io {
        include proxy_params;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://127.0.0.1:5000/socket.io;
    }

    location @proxy_to_app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $http_host;
      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;
      proxy_pass http://app_server;
    }

    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /path/to/app/current/public;
    }
  }
}
  • Related