Youtube
How to Deploy Flask with Gunicorn and Nginx (on Ubuntu) 14 minutes
https://youtu.be/KWIIPKbdxD0?si=Xi_jnjO5LLYKBqE9
Google A.I.
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:
$ 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
•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:
• 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>
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
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
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
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:
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