Create a wsgi file for Gunicorn and Python

by Lance Gold

In between the app and server, the wsgi file has run(hello.py)

Return to index
Here are some of the references used for this:

Youtube

How to Deploy Flask with Gunicorn and Nginx (on Ubuntu) 14 minutes https://youtu.be/KWIIPKbdxD0?si=Xi_jnjO5LLYKBqE9

Google A.I.

"debian where is sshd_condig.d configuration file"

The book "Beginning Node.js" p. 142. Listing 7-3 "1basic.js"

Here is the short version:

In the directory /etc/nginx/sites-enabled is a link that points to the file nginx-443.config in the directory /etc/nginx/sites-available:


x@c7:/etc/nginx/sites-enabled$ ls -l /etc/nginx/sites-enabled/
total 0
lrwxrwxrwx 1 root root 43 Feb  7 12:11 nginx-443.config -> /etc/nginx/sites-available/nginx-443.config

Inside the nginx-443.config are the lines:


...
        location /py/ {
                proxy_pass http://127.0.0.1:8000;
                proxy_set_header Host $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_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                # Add other necessary headers for Gunicorn
        }
...

In the directory /etc/systemd/system/ is the gunicorn.service with the lines:


...
[Service]
User=my-servicer
Group=www-data
WorkingDirectory=/opt/python_server_project
Environment="PATH=/opt/python_server_project/web_env/bin"
ExecStart=/opt/python_server_project/web_env/bin/gunicorn --workers 3 --bind unix:hello-py.sock -m 007 wsgi:app
...

In the directory /opt/python_server_project/ the file wsgi.py contains:


from hello import app
if __name__ == "__main__":
    app.run()

The hello.py file contains the lines:


from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
    return "Hello, world! from hello.py\n"
if __name__ == "__main__":
    app.run(host='127.0.0.1', port=8000)

From the youtube tonyboni:

Avoid root user in development

$ adduser tony
password:
confirm password:
Creating home directory /home/tony
Enter the new value, or press ENTER for the default
Full Name []:
...

We need the new user to have sudo privileges


$ usermode -aG sudo tony

check to make sure new user can log into remote server
•Main Configuration File: The primary OpenSSH server configuration file is still /etc/ssh/sshd_config.

•How it works: The main sshd_config file typically contains an Include directive that automatically pulls in settings from files in the sshd_config.d directory. This allows for modular configurations, simplifying updates and custom local settings by avoiding direct edits to the main, package-managed configuration file.


$ vim /etc/ssh/sshd_config.d/*.conf

Before:


#PasswordAuthentication yes

After:


PasswordAuthentication yes

This change to the commented line allows the new user to log into the server remotely.

To apply changes


$ systemctl restart sshd

Exit out of the server as root


$ exit

Reconnect


$ ssh tony@tonyboni.com

tony@vultr:~$

$ mkdir hike
$ ls
hike
$

this is a new user, so create a virtual environment


$ python3 -m venv ~/env/teton
$ source ~/env/teton/bin/activate
(teton) $

Following along with our server:


x@c7:~$ source /opt/python_server_project/web_env/bin/activate
(web_env) x@c7:~$

Tony's environment

from this point forward, any pip or python commands will be self-contained in this environment


(teton) $ pip install flash
(teton) $ cd hike/

(web_env) x@c7:~$ cd /opt/python_server_project
(web_env) x@c7:/opt/python_server_project$

Here is the webpresentation, not including colors, links, or music:

Let's go climb a mountain!

• Everest

• K2

• Kilimanjaro

Here is file peak.py:


from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index();
    mountains = ['Everest', 'K2', 'Kilimanjaro']
    return render_template('index.html', mountain=mountains)
@app.route('/mountain/')
def mountain(mt);
    return "This is " + str(mt)
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5000)

That is Tony's file. Here is ours:


from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
    return "Hello, world! from hello.py\n"

updated:


from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
    return "Hello, world! from hello.py\n"
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8000)

Tony's


(teton) hike$ python peak.py
  * Serving Flask app 'peak'
...
  * Running on all addresses (0.0.0.0)
  * Running on http://127.0.0.1:5000
  * Running on http://149.38.222.112:5000
Press CTRL+C to quit

test on browser


tonyboni.com:5000

Now Ours


(web_env) x@c7:/opt/python_server_project$ python hello.py
 * Serving Flask app 'hello'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8000
 * Running on http://69.55.235.35:8000
Press CTRL+C to quit

Check permissions.

Before:


(web_env) x@c7:/opt/python_server_project$ ls -l
total 12
-rw-r--r-- 1 my-servicer my-servicer  182 Feb  6 17:10 hello.py
drwxrwxr-x 2 x           x           4096 Feb  5 14:40 __pycache__
drwxr-xr-x 5 root        root        4096 Feb  5 14:26 web_env
(web_env) x@c7:/opt/python_server_project$

After:


(web_env) x@c7:/opt/python_server_project$ sudo chmod 775 hello.py
(web_env) x@c7:/opt/python_server_project$ ls -l
total 12
-rwxrwxr-x 1 my-servicer my-servicer  182 Feb  6 17:10 hello.py
drwxrwxr-x 2 x           x           4096 Feb  5 14:40 __pycache__
drwxr-xr-x 5 root        root        4096 Feb  5 14:26 web_env
(web_env) x@c7:/opt/python_server_project$

C:\Users\x>curl c7.xcvvc.com:8000
Hello, world! from hello.py

C:\Users\x>

Hello World! from appy.py from browser on port 8000

Next setup the gateway interface with gunicorn, with Tony's first:


(teton) hike$ pip install gunicorn

Inside our project directory next to peak.py make a web server gateway interface python file


(teton) hike$ vim wsgi.py

from peak import app
if __name__ == '__main__':
    app.run()

From our file wsgi.py:


from hello import app
if __name__ == "__main__':
    app.run()

Tony

In peak.py, there is an app variable.

We import that variable.

Here we are running the app directly without any of the host names or numbers that were in peak.py

Set up the infrastructure environment

Make sure gunicorn is working, wsgi.py and hello.py are in the ~/hike directory


(teton) hike$ gunicorn --bind 0.0.0.0:5000 wsgi:app

Ours

Press <ctrl-c> to stop hello.py


Press CTRL+C to quit
104.28.205.141 - - [06/Feb/2026 18:06:32] "GET / HTTP/1.1" 200 -
76.14.49.217 - - [06/Feb/2026 18:08:56] "GET / HTTP/1.1" 200 -
^C(web_env) x@c7:/opt/python_server_project$

(web_env) x@c7:/opt/python_server_project$ gunicorn --bind 0.0.0.0:8000 wsgi:app
[2026-02-06 18:18:44 -0800] [2166925] [INFO] Starting gunicorn 25.0.1
[2026-02-06 18:18:44 -0800] [2166925] [INFO] Listening at: http://0.0.0.0:8000 (2166925)
[2026-02-06 18:18:44 -0800] [2166925] [INFO] Using worker: sync
[2026-02-06 18:18:44 -0800] [2166926] [INFO] Booting worker with pid: 2166926

Check in browser to see if site still loads


C:\Users\x>curl http://c7.xcvvc.com:8000
Hello, world! from hello.py

C:\Users\x>

Tony's

Gunicorn installation is okay, along with Flash and pip installs, so return to virtual server, deactivate to return to natural environment


(teton) ~/hike$ deactivate
tony@vultr:~/hike$

Ours


(web_env) x@c7:/opt/python_server_project$ deactivate
x@c7:/opt/python_server_project$

Tony's

Set up system process file to access the python virtual environment, the flash app, so if the system reboots or crashes, it will automatically bring it back up. We can access the ctl status, start, reload commands

Create a new file in the system directory named peak.service


hike$ sudo vim /etc/systemd/system/peak.service

[Unit]
Description=Gunicorn instance to serve peak Flask app
After=network.target

[Service]
User=tony
Group=www-data
workingDirectory=/home/tony/hike
Environment="PATH=/home/tony/env/teton/bin"
ExecStart=/home/tony/env/teton/bin/gunicorn --workers 3 --bind unix:peak.sock -m 007 wsgi:app

[Install]
WantedBy=multi-user.target

Ours


x@c7:/etc/systemd/system$ sudo vi gunicorn.service

[Unit]
Description=Gunicorn instance to serve hello.py flask app using wsgi.py
After=network.target

[Service]
User=my-servicer
Group=www-data
WorkingDirectory=/opt/python_server_project
Environment="PATH=/opt/python_server_project/web_env/bin"
ExecStart=/opt/python_server_project/web_env/bin/gunicorn --workers 3 --bind unix:hello-py.sock -m 007 wsgi:app

[Install]
WantedBy=multi-user.target

Here is what the different lines mean:
After.. will start up after the service is ready

User.. will run under the user 'my-service' which we just created.

Group.. under www-data which is a standard group for serving up http requests

Workin... has the flash app

Envion... has the /bin with all the python addons, pip, flask, gunicorn applicable to this project

Exec...
runs the gunicorn from the bin directory in the virtual area (teton)

adds 3 workers for processing (2* number of cores +1)

--bind .... creates a temporary socket in the hike directory

-m 007... sets the permissions of the temporary socket file

wsgi:app... runs the wsgi.py file and inside that file is an app variable.

Wante....: multi-... to make accessible by everyuser in the system

Start up the service file

... or reload the service with changes


 x@c7:/opt/python_server_project$ sudo chmod 775 wsgi.py

Tony's


hike$ sudo systemctl start peak

Warning:  the...changed on disk. Run 'systemctl daemon-reload'....

hike$ sudo systemctl daemon-reload
hike$ sudo systemctl start peak
hike$ sudo systemctl enable peak

enable starts system automatically when the system starts up

hike$ sudo systemctl status peak

Ours


x@c7:/opt/python_server_project$ sudo systemctl start gunicorn
x@c7:/opt/python_server_project$ sudo systemctl status gunicorn
x@c7:/opt/python_server_project$ sudo journalctl -u gunicorn.service
...
Feb 06 19:17:15 c7.xcvvc.com gunicorn[2167983]: [2026-02-06 19:17:15 -0800] [2167983] [ERROR] connection to hello-py.sock failed: [Errno 13] Permission denied
Feb 06 19:17:16 c7.xcvvc.com gunicorn[2167983]: [2026-02-06 19:17:16 -0800] [2167983] [ERROR] Can't connect to hello-py.sock
Feb 06 19:17:16 c7.xcvvc.com systemd[1]: gunicorn.service: Main process exited, code=exited, status=1/FAILURE

Adjust user and group permissions

Before:


-rw-r--r-- 1 my-servicer www-data  184 Feb  6 18:04 hello.py
drwxrwxr-x 2 x           x        4096 Feb  6 18:15 __pycache__
drwxr-xr-x 5 root        root     4096 Feb  5 14:26 web_env
-rwxrwxr-x 1 my-servicer www-data   63 Feb  6 18:15 wsgi.py
x@c7:/opt/python_server_project$ ls -l ../
total 12
drwxrwxr-x 2 root        ndev        4096 Feb  3 22:39 html
drwxrwxr-x 2 my-servicer my-servicer 4096 Feb  3 23:47 nginx_server_project
drwxrwxr-x 4 root        ndev        4096 Feb  6 18:15 python_server_project

x@c7:/opt/python_server_project$ sudo usermod -a -G www-data my-servicer
x@c7:/opt/python_server_project$ sudo chown my-servicer:www-data ../python_server_project
x@c7:/opt/python_server_project$

After:


x@c7:/opt/python_server_project$ ls -l
total 16
-rw-r--r-- 1 my-servicer www-data  184 Feb  6 18:04 hello.py
drwxrwxr-x 2 x           x        4096 Feb  6 18:15 __pycache__
drwxr-xr-x 5 root        root     4096 Feb  5 14:26 web_env
-rwxrwxr-x 1 my-servicer www-data   63 Feb  6 18:15 wsgi.py
x@c7:/opt/python_server_project$ ls -l ../
total 12
drwxrwxr-x 2 root        ndev        4096 Feb  3 22:39 html
drwxrwxr-x 2 my-servicer my-servicer 4096 Feb  3 23:47 nginx_server_project
drwxrwxr-x 4 my-servicer www-data    4096 Feb  6 18:15 python_server_project
x@c7:/opt/python_server_project$

x@c7:/opt/python_server_project$ sudo systemctl status gunicorn
● gunicorn.service - Gunicorn instance to serve hello.py flask app using wsgi.py
     Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; preset: enabled)
     Active: active (running) since Fri 2026-02-06 19:33:54 PST; 18s ago

Before:


x@c7:/opt/python_server_project$ ls -l
total 16
-rw-r--r-- 1 my-servicer www-data  184 Feb  6 18:04 hello.py
srwxrwx--- 1 my-servicer www-data    0 Feb  6 19:33 hello-py.sock
drwxrwxr-x 2 x           x        4096 Feb  6 18:15 __pycache__
drwxr-xr-x 5 root        root     4096 Feb  5 14:26 web_env
-rwxrwxr-x 1 my-servicer www-data   63 Feb  6 18:15 wsgi.py


x@c7:/opt/python_server_project$ sudo chmod 775 hello.py

After:


x@c7:/opt/python_server_project$ ls -l
total 16
-rwxrwxr-x 1 my-servicer www-data  184 Feb  6 18:04 hello.py
...

Everything looks good, both active and running


...
    Active: active (running)
...

Testing with curl or the browser doesn't work

This has set up the Flask app, the wsgi, and the gunicorn interface.

Next is the Nginx web server, the proxy server

Tony's


hike$ sudo apt install nginx
...

Make a configuration file in the usual location named peak.conf


hike$ sudo vim /etc/nginx/sites-available/peak.conf

server {
    listen 80;
    server_name tonyboni.com www.tonyboni.com

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/tony/hike/peak.sock;
    }
}

Ours


x@c7:/opt/python_server_project$ sudo vi /etc/nginx/sites-available/gunicorn.config

server {
        listen 80;
        server_name c7.xcvvc.com www.c7.xcvvc.com

        location /py/ {
                include proxy_params;
                proxy_pass http://unix:/opt/python_server_project/hello_py.sock
        }
}

Tony's

What each line means:

listen 80... instead of 5000

location /... the root of the web directory tree, so no /something after the .com/something

include... will include the proxy_params

proxy_pass... nginx sends client request to the temporary running gunicorn socket (not just any file) peak.sock listening from a unix kind of a socket, located at: /home/tony/hike/peak.sock.

Create a symlink, as usual for nginx to point sites-enabled to sites-available. The file is in sites-available. The link is in sites-enabled


hike$ sudo ln -s /etc/nginx/sites-available/peak.conf /etc/sites/nginx/sites-enabled/peak.conf

Test for errors in the configuration


hike$ sudo nginx -t

hike$ sudo systemctl restart nginx

Ours


x@c7:/opt/python_server_project$ sudo systemctr restart nginx_server

check firewall which is running.


hike$ sudo ufw status
...
to 5000, action allow, from anywhere
to 5000 (v6), action allow, from Anywhere (v6)

Close that down, nginx will take of: in on 80 out to peak.sock on 5000

Tony's


hike$ sudo ufw delete allow 5000

Rule deleted
Rule deleted (v6)

There is a cool rule to allow port 80, port 443, all the standard http ports for Nginx.


hike$ sudo ufw allow "Nginx Full"

Check again


hike$ sudo ufw status
...
To Nginx Full, action Allow, from Anywhere
...

Test with browser (raises a 502 bad gateway error)

This is a permissions issue

Use the tail command which shows the end of a log file


hike$ sudo tail /var/log/nginx/error.log
...
...to unix:/home/tony/hike/peak.sock failed (13: Permission denied) while....
...

The socket file refused, so change the permissions of the directory of the socket file. (or parent of directory)


hike$ sudo chmod 775 /home/tony

Ours

Looking for existing gunicorn programs running:


pgrep gunicorn

2166909
2166910
2166925
2166943

sudo pkill -9 gunicorn

systemctl list-units --type=service --state=enabled
									--state=disabled
									--state=failed
									--state=running

nginx.service
nginx_server.service
 


notice the hello_py.sock needs to be running before nginx reads the sites-enabled -> sites-available: Here is sites-available/gunicorn.config for nginx:


server {
        listen 80;
        server_name c7.xcvvc.com www.c7.xcvvc.com;

        location /py/ {
                include proxy_params;
                proxy_pass http://unix:opt_python_server_project/hello_py.sock;
        }
}

sooo run the gunicorn service first before the nginx service

Here is nginx_server.service


[Unit]
Description=A Node.js Application web server
After=network.target

[Service]
Type=simple
User=my-servicer
WorkingDirectory=/opt/nginx_server_project
ExecStart=/usr/bin/node /opt/nginx_server_project/server.js
Restart=on-failure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

Here is gunicorn.service


[Unit]
Description=Gunicorn instance to serve hello.py flask app using wsgi.py
After=network.target

[Service]
User=my-servicer
Group=www-data
WorkingDirectory=/opt/python_server_project
Environment="PATH=/opt/python_server_project/web_env/bin"
ExecStart=/opt/python_server_project/web_env/bin/gunicorn --workers 3 --bind unix:hello-py
.sock -m 007 wsgi:app

[Install]
WantedBy=multi-user.target

run gunicorn, but not as a service


# hello.py

from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
    return "Hello, world! from hello.py\n"
if __name__ == "__main__":
    app.run(host='127.0.0.1', port=8000)
 

# wsgi.py

from hello import app
if __name__ == "__main__":
    app.run()


gunicorn
(web_env) x@c7:/opt/python_server_project$ gunicorn --bind 0.0.0.0:8000 wsgi:app

test from remote after logging into remote:


x@c7:~$ curl 127.0.0.1:8000
Hello, world! from hello.py
x@c7:~$

Next to nginx


# file /etc/nginx/sites-available/gunicorn.config
server {
        listen 80;
        server_name c7.xcvvc.com www.c7.xcvvc.com;

        location / {
                include proxy_params;
        #       proxy_pass http://unix:/opt/python_server_project/hello_py.sock;
                proxy_pass http://127.0.0.1:8000;
        }

}

that didn't work


# file /etc/nginx/sites-available/nginx-443.config
...
        location /py/ {
                proxy_pass http://127.0.0.1:8000;
                proxy_set_header Host $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_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                # Add other necessary headers for Gunicorn
        }
....

file /etc/systemd/system/nginx_server.service for running nginx as a service (not the file nginx-443.config)


x@c7:/opt/python_server_project$ more /etc/systemd/system/nginx_server.service
[Unit]
Description=A Node.js Application web server
After=network.target

[Service]
Type=simple
User=my-servicer
WorkingDirectory=/opt/nginx_server_project
ExecStart=/usr/bin/node /opt/nginx_server_project/server.js
Restart=on-failure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target