{"id":51,"date":"2024-11-10T18:45:00","date_gmt":"2024-11-10T15:45:00","guid":{"rendered":"https:\/\/fsaeed.blog\/?p=51"},"modified":"2026-10-31T00:18:53","modified_gmt":"2026-10-30T21:18:53","slug":"your-first-vps-python-web-app","status":"publish","type":"post","link":"https:\/\/fsaeed.blog\/index.php\/2024\/11\/10\/your-first-vps-python-web-app\/","title":{"rendered":"Your First VPS: Hosting a Small Python Web App"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Hello, tech enthusiasts! \ud83d\udc4b<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You built a small Python tool. Maybe it shows a sales summary, or it lets your team look up invoice status. It works great on your laptop. But when your laptop is off, nobody can use it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The answer is a <strong>VPS<\/strong>, a Virtual Private Server. Think of it as renting a small computer in a data centre. It is always on and always online, and small ones cost only a few dollars a month.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Today we will rent one, connect safely, and put a tiny Flask app online.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What you need<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A small VPS with Ubuntu 24.04 (1 CPU and 1 GB of memory is enough)<\/li>\n\n\n<li>A terminal on your computer (Windows 10 and 11 have <code>ssh<\/code> built in)<\/li>\n\n\n<li>Optional: a domain name, like example.com<\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Rent the server and connect with SSH keys<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pick any hosting provider and create the smallest Ubuntu server. You will get its IP address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SSH<\/strong> is a safe tunnel to type commands on the server. Instead of a password, we use an <strong>SSH key<\/strong>. A key is like a house key that only you have: much harder to guess than a password.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On your own computer, create a key:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh-keygen -t ed25519<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Most providers let you paste your <strong>public<\/strong> key (the file ending in <code>.pub<\/code>) when you create the server. Do that. Then connect (replace the address with your server&#8217;s IP):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh root@203.0.113.10<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Update and create a normal user<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">First, install all updates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apt update &amp;&amp; apt upgrade -y<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Working as <code>root<\/code> all the time is risky. Create a normal user with admin rights and copy your key to it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>adduser deploy\nusermod -aG sudo deploy\nrsync --archive --chown=deploy:deploy ~\/.ssh \/home\/deploy<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Log out and connect again as <code>ssh deploy@203.0.113.10<\/code>. Once that works, turn off password logins. Open <code>\/etc\/ssh\/sshd_config<\/code>, set <code>PasswordAuthentication no<\/code>, check that no file in <code>\/etc\/ssh\/sshd_config.d\/<\/code> sets it back to <code>yes<\/code>, then run <code>sudo systemctl restart ssh<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Create the Python app<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Install Python tools and nginx:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y python3-venv nginx<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Make a folder and a <strong>virtual environment<\/strong>. A venv is a private box for your app&#8217;s libraries, so they do not mix with the system.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir ~\/myapp &amp;&amp; cd ~\/myapp\npython3 -m venv venv\nsource venv\/bin\/activate\npip install flask gunicorn<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>app.py<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from flask import Flask\n\napp = Flask(__name__)\n\n@app.route(\"\/\")\ndef home():\n    return \"Hello from my first server!\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Run it with gunicorn and systemd<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Flask&#8217;s built-in server is only for testing. <strong>Gunicorn<\/strong> is a real web server for Python apps. Test it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>gunicorn --workers 2 --bind 127.0.0.1:8000 app:app<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Press Ctrl+C to stop. Now we want it to start by itself, even after a reboot. That is the job of <strong>systemd<\/strong>, Linux&#8217;s service manager. Create <code>\/etc\/systemd\/system\/myapp.service<\/code> with <code>sudo nano<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[Unit]\nDescription=My Flask app\nAfter=network.target\n\n[Service]\nUser=deploy\nWorkingDirectory=\/home\/deploy\/myapp\nExecStart=\/home\/deploy\/myapp\/venv\/bin\/gunicorn --workers 2 --bind 127.0.0.1:8000 app:app\nRestart=always\n\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Start it and enable it at boot:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable --now myapp\nsudo systemctl status myapp<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Put nginx in front<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Nginx<\/strong> is like a receptionist. It receives visitors on the normal web port (80) and passes them to gunicorn. It is fast and safe. Create <code>\/etc\/nginx\/sites-available\/myapp<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>server {\n    listen 80;\n    server_name example.com;\n\n    location \/ {\n        proxy_pass http:\/\/127.0.0.1:8000;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Enable it, remove the default site, test and reload:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ln -s \/etc\/nginx\/sites-available\/myapp \/etc\/nginx\/sites-enabled\/\nsudo rm \/etc\/nginx\/sites-enabled\/default\nsudo nginx -t\nsudo systemctl reload nginx<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 6: Turn on the firewall<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A <strong>firewall<\/strong> is a door guard. It only lets in the traffic you allow. Allow SSH <strong>first<\/strong>, so you do not lock yourself out:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw allow 22\/tcp\nsudo ufw allow 80\/tcp\nsudo ufw allow 443\/tcp\nsudo ufw enable\nsudo ufw status<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now open <code><a href=\"http:\/\/203.0.113.10\">http:\/\/203.0.113.10<\/a><\/code> (or your domain) in a browser. You should see your message.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 7: Add HTTPS<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you have a domain pointing to your server, get a free certificate with certbot:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y certbot python3-certbot-nginx\nsudo certbot --nginx -d example.com<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Certbot changes the nginx file for you and renews the certificate automatically.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Common problems<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>502 Bad Gateway<\/strong> \u2192 Gunicorn is not running. Check <code>sudo systemctl status myapp<\/code> and <code>journalctl -u myapp<\/code>.<\/li>\n\n\n<li><strong>Locked out after enabling ufw<\/strong> \u2192 Use your provider&#8217;s web console, then run <code>sudo ufw allow 22\/tcp<\/code>.<\/li>\n\n\n<li><strong>Code changes do not show<\/strong> \u2192 Restart the app with <code>sudo systemctl restart myapp<\/code>.<\/li>\n\n\n<li><strong>certbot fails<\/strong> \u2192 Your domain&#8217;s DNS must point to the server&#8217;s IP, and port 80 must be open.<\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Wrap-up<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You now have a real server, with key-only SSH, a firewall, a proper app server and a web server in front. That is the same basic setup many big websites use.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next idea: turn on automatic security updates with <code>sudo apt install unattended-upgrades<\/code>, so your server stays patched while you sleep.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Rent a small Linux server and put a simple Python web app online, safely and step by step.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[28,27,29,12,26],"class_list":["post-51","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-flask","tag-linux","tag-nginx","tag-python","tag-vps"],"_links":{"self":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/51","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/comments?post=51"}],"version-history":[{"count":1,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/51\/revisions"}],"predecessor-version":[{"id":82,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/51\/revisions\/82"}],"wp:attachment":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/media?parent=51"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/categories?post=51"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/tags?post=51"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}