> devlog / March 06, 2026 Portfolio Website

Going Live — Deploying a Flask App to a VPS

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.

// previous ← Setting Up My Development Environment
all posts
// next Why I Built My Own Portfolio Instead of Using a Template →