Skip to content

Instantly share code, notes, and snippets.

@bockor
Last active August 23, 2025 14:39
Show Gist options
  • Save bockor/d751e252c5bb459bd4c6fe6934a46818 to your computer and use it in GitHub Desktop.
Save bockor/d751e252c5bb459bd4c6fe6934a46818 to your computer and use it in GitHub Desktop.
hosting-multiple-flask-apps-using-apache-mod_wsgi
# Inspiration ... where did I start ?:
# https://www.blopig.com/blog/2021/05/hosting-multiple-flask-apps-using-apache-mod_wsgi/
# https://www.rosehosting.com/blog/how-to-install-flask-on-ubuntu-22-04-with-apache-and-wsgi/?srsltid=AfmBOoqkm3GW5tXYBQywDTb_Y6SuLD0URVm9OC3uBqO4QhtSHpndMbz-
# https://stackoverflow.com/questions/29882579/run-multiple-independent-flask-apps-in-ubuntu
# https://serverfault.com/questions/357108/what-permissions-should-my-website-files-folders-have-on-a-linux-webserver
sudo apt install apache2 -y
sudo systemctl status apache2
sudo apt install libapache2-mod-wsgi-py3
apachectl -M | grep wsgi
sudo apt install python3-pip
sudo apt install python3.12-venv
sudo mkdir /opt/ducksoup.retro/
sudo chown -R bruno ducksoup.retro/
sudo chgrp -R www-data ducksoup.retro/
sudo chmod -R 750 ducksoup.retro/
sudo chmod g+s ducksoup.retro/
Setting directories g+s makes all new files created in said directory have their group set to the directory's group.
This can actually be really handy for collaborative purposes if you have the umask set so that files have group write by default.
cd /opt/ducksoup.retro/
python3 -m venv .venv
. .venv/bin/activate
pip3 install flask
vim /opt/ducksoup.retro/app.py
===============================
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
@app.route('/moth')
def index():
moth = "DuckSoup Sucks !"
return render_template('index.html', moth=moth)
===============================
vim /opt/ducksoup.retro/templates/index.html
===============================
<html>
<head>
<title>DuckSoup Message Of the Day</title>
</head>
<body>
<h3>{{moth}}</h3>
</body>
</html>
===============================
vim /opt/ducksoup.retro/ducksoup.retro.wsgi
===============================
import sys
sys.path.insert(0,'/opt/ducksoup.retro')
from app import app as application
==============================
vim /etc/apache2/sites-available/ducksoup.retro.conf
===============================
<VirtualHost *:80>
ServerName ducksoup.retro
DocumentRoot /opt/ducksoup.retro/
ErrorLog ${APACHE_LOG_DIR}/ducksoup.retro-error.log
CustomLog ${APACHE_LOG_DIR}/ducksoup.retro-access.log combined
WSGIDaemonProcess app user=www-data group=www-data threads=5 python-home=/opt/ducksoup.retro/.venv
WSGIScriptAlias / /opt/ducksoup.retro/ducksoup.retro.wsgi
<Directory /opt/ducksoup.retro>
WSGIProcessGroup app
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Require all granted
</Directory>
</VirtualHost>
===============================
sudo a2ensite ducksoup.retro.conf
sudo a2dissite 000-default.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
+++++++++++++++++++++++++++++++
Now, let's add a new, dedicated api ... which returns json only
sudo mkdir /opt/ducksoup-api.retro/
sudo chown -R bruno ducksoup-api.retro/
sudo chgrp -R www-data ducksoup-api.retro/
sudo chmod -R 750 ducksoup-api.retro/
sudo chmod g+s ducksoup-api.retro/
vim /opt/ducksoup-api.retro/app.py
===============================
from flask import Flask, jsonify
from datetime import datetime
app = Flask(__name__)
@app.route('/')
def index():
version = datetime.now().strftime("%Y%m%dT%H%M")
sitez = [
{ 'id': 1, 'site': 'RED', 'location': 'ALPHA','comment': 'bla' },
{ 'id': 2, 'site': 'BLUE', 'location': 'BRAVO','comment': 'blabla' },
{ 'id': 3, 'site': 'GREEN','location': 'ALPHA', 'comment': 'blablabla' },
]
return jsonify({"version": version, "result":sitez}), 200
===============================
vim /opt/ducksoup-api.retro/ducksoup-api.retro.wsgi
===============================
import sys
sys.path.insert(0,'/opt/ducksoup-api.retro')
from app import app as application
===============================
vim /etc/apache2/sites-available/ducksoup.retro.conf
===============================
<VirtualHost *:80>
ServerName ducksoup.retro
ErrorLog ${APACHE_LOG_DIR}/ducksoup.retro-error.log
CustomLog ${APACHE_LOG_DIR}/ducksoup.retro-access.log combined
WSGIDaemonProcess ducksoup_api user=www-data group=www-data threads=5 python-home=/opt/ducksoup.retro/.venv
WSGIScriptAlias /api /opt/ducksoup-api.retro/ducksoup-api.retro.wsgi
<Directory /opt/ducksoup-api.retro>
WSGIProcessGroup ducksoup_api
WSGIApplicationGroup %{GLOBAL}
Require all granted
</Directory>
WSGIDaemonProcess ducksoup_app user=www-data group=www-data threads=5 python-home=/opt/ducksoup.retro/.venv
WSGIScriptAlias / /opt/ducksoup.retro/ducksoup.retro.wsgi
<Directory /opt/ducksoup.retro>
WSGIProcessGroup ducksoup_app
WSGIApplicationGroup %{GLOBAL}
Require all granted
</Directory>
</VirtualHost>
===============================
sudo apache2ctl configtest
sudo systemctl reload apache2
curl -i http://ducksoup.retro
###################
HTTP/1.1 200 OK
Date: Sat, 16 Aug 2025 11:57:02 GMT
Server: Apache/2.4.58 (Ubuntu)
Content-Length: 145
Vary: Accept-Encoding
Content-Type: text/html; charset=utf-8
<html>
<head>
<title>DuckSoup Message Of the Day</title>
</head>
<body>
<h3>DuckSoup Sucks !</h3>
</body>
</html>
===============================
curl -i http://ducksoup.retro/api/
######################
HTTP/1.1 200 OK
Date: Sat, 16 Aug 2025 10:36:28 GMT
Server: Apache/2.4.58 (Ubuntu)
Content-Length: 222
Content-Type: application/json
{"result":[{"comment":"bla","id":1,"location":"ALPHA","site":"RED"},{"comment":"blabla","id":2,"location":"BRAVO","site":"BLUE"},{"comment":"blablabla","id":3,"location":"ALPHA","site":"GREEN"}],"version":"20250607T1245"}
===============================
bruno@ducksoup:/opt$ tree
.
├── ducksoup-api.retro
│   ├── app.py
│   └── ducksoup-api.retro.wsgi
└── ducksoup.retro
├── app.py
├── ducksoup.retro.wsgi
└── templates
└── index.html
===============================
Extra: How to solve the Apache Configuration Error AH00558: Could not reliably determine the server's fully qualified domain name
************************************
https://www.digitalocean.com/community/tutorials/apache-configuration-error-ah00558-could-not-reliably-determine-the-server-s-fully-qualified-domain-name
sudo nano /etc/apache2/apache2.conf
Add a line containing ServerName 127.0.0.1 to the end of the file:
ServerName 127.0.0.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment