Getting a Flask app running locally is one thing. Getting it running on a real server, accessible to anyone on the internet, is another. Here's how I deployed this portfolio to my VPS.
The Stack
The server runs Ubuntu 24.04 LTS, managed through Plesk. The portfolio itself is a Flask application served by Gunicorn, sitting behind nginx and Apache as a reverse proxy.
The full request flow looks like this:
Visitor → nginx → Apache → Gunicorn → Flask
Setting Up the Server
The first step was getting Python and the necessary tools installed:
apt update
apt install python3-pip python3-venv git -y
I also set up an SSH key on the VPS and added it to GitHub so I could clone my private repository directly onto the server without entering credentials.
Installing Dependencies
After cloning the repository I set up a virtual environment and installed all dependencies from the requirements file:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install gunicorn
Environment Variables
Since the .env file is gitignored it had to be created manually on the server. This is where production specific values live — the database connection string, mail credentials, and a strong secret key generated with:
python3 -c "import secrets; print(secrets.token_hex(32))"
Keeping sensitive values out of version control and in environment variables is standard practice for any production deployment.
Database
Locally I use SQLite for simplicity. In production I switched to MySQL, which Plesk manages easily through its database panel. SQLAlchemy handles both without any changes to the application code — just a different connection string in the environment variables.
After creating the database in Plesk and adding the credentials to .env, initializing the tables was straightforward — a small script that calls db.create_all() within the app context.
Running with Gunicorn
Flask's built in development server is not suitable for production. Gunicorn is a production grade WSGI server that handles multiple workers and concurrent requests properly.
I set it up as a systemd service so it starts automatically on boot and restarts if it ever crashes:
[Unit]
Description=Portfolio
After=network.target
[Service]
User=root
Environment="PATH=/path/to/venv/bin"
ExecStart=/path/to/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 run:app
Restart=always
[Install]
WantedBy=multi-user.target
Configuring Plesk
With Gunicorn running on port 8000, I configured Plesk to forward incoming web traffic to it using Apache proxy directives:
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
SSL was handled automatically through Let's Encrypt, which is built into Plesk and renews certificates automatically.
Deployment Workflow
Going forward, deploying any update is straightforward — push to GitHub from the local machine, pull on the server, restart Gunicorn. Three commands and the update is live.
What I Learned
Deployment has a lot of moving parts that don't come up during local development — process management, reverse proxying, environment separation, production databases. Every problem I ran into during this process was a genuine learning experience.
The site is now live at kylewheatley.com.