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;
}
}
}