How to Host a Website and Web App on Ubuntu 25.04 — Complete Server Setup Guide
A complete, beginner-to-intermediate guide to setting up a Linux server on Ubuntu 25.04, hosting a static website with Nginx, and deploying a Node.js or Python web application — from a fresh VPS to a live domain with HTTPS.
How to Host a Website and Web App on Ubuntu 25.04
Whether you're a developer launching your first project, a sysadmin setting up infrastructure, or a tech enthusiast wanting full control over your hosting — this guide walks you through the entire process from a blank Ubuntu 25.04 server to a live website and web application with a real domain and HTTPS.
No prior Linux server experience is required, but familiarity with the command line will help. Every command is explained so you understand what it does, not just how to run it.
What We'll Cover
- Choosing and accessing a VPS (Virtual Private Server)
- Initial server setup and security hardening
- Installing Nginx as a web server
- Hosting a static website
- Setting up a domain and HTTPS with Let's Encrypt
- Deploying a Node.js web application
- Deploying a Python (Flask/Django) web application
- Using PM2 to keep your app running
- Setting up a firewall
Part 1 — Getting a Server
What Is a VPS?
A VPS is a virtual machine you rent from a provider. It runs Linux 24/7 in a data center and is accessible over the internet via SSH. Popular providers include:
- DigitalOcean — beginner-friendly, $6/month for a basic droplet
- Vultr — competitive pricing, good global locations
- Linode (Akamai) — solid performance, $5/month entry
- Hetzner — excellent value, especially in Europe
- AWS EC2 / Google Cloud / Azure — enterprise-grade, more complex
For this guide, any provider works. Create an account, spin up a new server (often called a "droplet", "instance", or "VPS"), and select:
- OS: Ubuntu 25.04 (Noble Numbat)
- Plan: At least 1 vCPU, 1 GB RAM (2 GB recommended for apps)
- Region: Closest to your users
After creation, you'll receive an IP address (e.g., 203.0.113.42) and either a root password or an SSH key option.
Part 2 — Connecting to Your Server
SSH Into Your Server
From your local machine (Mac/Linux terminal, or Windows with PuTTY/Windows Terminal):
ssh root@203.0.113.42
Replace 203.0.113.42 with your server's actual IP address. If you set up an SSH key during provisioning, you'll connect automatically. Otherwise, enter the root password when prompted.
You should now see a terminal prompt like:
root@ubuntu-server:~#
You're inside your server.
Part 3 — Initial Server Setup
Before installing anything, get the server in a clean, secure state.
Step 1 — Update the System
Ubuntu ships with some packages already outdated. Always update before doing anything else:
apt update && apt upgrade -y
- apt update — refreshes the list of available packages
- apt upgrade -y — upgrades all installed packages to their latest versions
- -y — automatically says "yes" to prompts
Step 2 — Create a Non-Root User
Running everything as root is dangerous. If you make a mistake or get hacked, the attacker has full control. Create a regular user and give it sudo (admin) privileges:
adduser deploy
You'll be prompted to set a password and fill in some optional details (just press Enter to skip them).
Now give this user sudo access:
usermod -aG sudo deploy
This adds deploy to the sudo group — meaning they can run admin commands by prefixing them with sudo.
Step 3 — Set Up SSH Key Authentication for the New User
Copy the authorized SSH keys from root to the new user so you can log in without a password:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Now test the new user in a new terminal window (don't close your current session yet):
ssh deploy@203.0.113.42
If it connects, you're good. From now on, use this user — not root.
Step 4 — Disable Root SSH Login
Open the SSH configuration file:
sudo nano /etc/ssh/sshd_config
Find the line PermitRootLogin yes and change it to:
PermitRootLogin no
Also ensure password authentication is disabled (if you're using SSH keys):
PasswordAuthentication no
Save the file (Ctrl+O, Enter, Ctrl+X in nano), then reload SSH:
sudo systemctl reload sshd
Part 4 — Set Up the Firewall
Ubuntu 25.04 comes with UFW (Uncomplicated Firewall). Let's enable it and only allow the ports we need:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
- OpenSSH — allows your SSH connection (port 22)
- 80/tcp — HTTP traffic (regular web)
- 443/tcp — HTTPS traffic (secure web)
Check the status:
sudo ufw status
Part 5 — Install Nginx
Nginx (pronounced "engine-x") is a high-performance web server. It serves your website files to visitors and can also act as a reverse proxy — forwarding requests to your backend application.
Install Nginx
sudo apt install nginx -y
Start and Enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx
- start — starts Nginx right now
- enable — makes Nginx start automatically when the server reboots
Test It
Open a browser and go to http://203.0.113.42 (your server IP). You should see the "Welcome to nginx!" default page. If you do, Nginx is working.
Part 6 — Host a Static Website
A static website is one made of plain HTML, CSS, and JavaScript files — no server-side processing needed. This includes portfolios, landing pages, documentation sites, and more.
Step 1 — Create Your Site Directory
sudo mkdir -p /var/www/mysite.com/html
sudo chown -R deploy:deploy /var/www/mysite.com
- /var/www/ — standard location for website files on Linux
- chown — changes the owner so your user (not root) can edit files
Step 2 — Add Your Website Files
Create a simple test page:
nano /var/www/mysite.com/html/index.html
Paste this basic HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Site</title>
</head>
<body>
<h1>Hello from my Ubuntu 25.04 server!</h1>
<p>My website is live.</p>
</body>
</html>
Save and exit (Ctrl+O, Enter, Ctrl+X).
Step 3 — Create an Nginx Server Block
A server block (equivalent to Apache's "virtual host") tells Nginx which files to serve for which domain.
sudo nano /etc/nginx/sites-available/mysite.com
Paste the following:
server {
listen 80;
listen [::]:80;
server_name mysite.com www.mysite.com;
root /var/www/mysite.com/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
What this means:
- listen 80 — listen for HTTP traffic on port 80
- server_name — which domain name this block handles
- root — where the website files are located
- try_files — try to serve the requested file; return 404 if not found
Step 4 — Enable the Site
sudo ln -s /etc/nginx/sites-available/mysite.com /etc/nginx/sites-enabled/
This creates a symbolic link (shortcut) from sites-available to sites-enabled. Nginx only serves sites in sites-enabled.
Step 5 — Test and Reload Nginx
sudo nginx -t
sudo systemctl reload nginx
- nginx -t — tests your configuration for syntax errors before applying it
- Always test before reloading to avoid breaking a live server
Part 7 — Point Your Domain to the Server
Update DNS Records
Log in to wherever you bought your domain (Namecheap, GoDaddy, Cloudflare, etc.) and update the DNS records:
| Type | Name | Value |
|---|---|---|
| A | @ | 203.0.113.42 |
| A | www | 203.0.113.42 |
- The @ symbol represents the root domain (mysite.com)
- www handles www.mysite.com
DNS changes can take anywhere from a few minutes to 48 hours to propagate globally. You can check propagation at dnschecker.org.
Part 8 — Enable HTTPS with Let's Encrypt (Free SSL)
HTTPS encrypts the connection between your visitor and your server. Without it, browsers show a "Not Secure" warning. Let's Encrypt provides free, auto-renewing SSL certificates.
Install Certbot
sudo apt install certbot python3-certbot-nginx -y
Obtain a Certificate
sudo certbot --nginx -d mysite.com -d www.mysite.com
Certbot will:
- Verify you own the domain (via HTTP challenge)
- Download and install the certificate
- Automatically modify your Nginx config to enable HTTPS
- Set up automatic renewal
Follow the prompts — enter your email for renewal notifications and agree to the terms.
Test Auto-Renewal
sudo certbot renew --dry-run
This simulates a renewal without actually doing it, confirming the renewal process will work when the certificate expires in 90 days.
Your site is now live at https://mysite.com with a green padlock.
Part 9 — Deploy a Node.js Web Application
Now let's go beyond static files and host a real web application. We'll deploy a Node.js app and use Nginx as a reverse proxy in front of it.
Step 1 — Install Node.js
The recommended way to install a modern Node.js version is via NodeSource:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install nodejs -y
Verify:
node --version
npm --version
Step 2 — Upload or Clone Your App
If you have a Git repository:
cd /home/deploy
git clone https://github.com/yourusername/your-app.git
cd your-app
Or create a simple test app:
mkdir /home/deploy/myapp && cd /home/deploy/myapp
nano app.js
Paste this minimal Node.js HTTP server:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from my Node.js app!\n');
});
server.listen(3000, '127.0.0.1', () => {
console.log('App running on port 3000');
});
Step 3 — Install Dependencies
If your app has a package.json:
npm install
Step 4 — Install PM2 (Process Manager)
If you run node app.js directly in the terminal and close your SSH session, the app stops. PM2 is a process manager that keeps your app running in the background, restarts it if it crashes, and starts it on server reboot.
sudo npm install -g pm2
Start your app with PM2:
pm2 start app.js --name "myapp"
Make PM2 start on boot:
pm2 startup
PM2 will output a command — copy and run it. Then save the current process list:
pm2 save
Useful PM2 commands:
pm2 list # See all running apps
pm2 logs myapp # View app logs
pm2 restart myapp # Restart the app
pm2 stop myapp # Stop the app
pm2 delete myapp # Remove it from PM2
Step 5 — Configure Nginx as a Reverse Proxy
Your Node.js app runs on port 3000 internally. Nginx will accept requests on port 80/443 and forward them to your app.
sudo nano /etc/nginx/sites-available/myapp.com
server {
listen 80;
server_name myapp.com www.myapp.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
}
}
What the proxy headers do:
- proxy_pass — forwards requests to your app running on port 3000
- X-Real-IP — passes the visitor's real IP address to your app
- Upgrade / Connection — required for WebSocket support
Enable the site:
sudo ln -s /etc/nginx/sites-available/myapp.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Then get your SSL certificate:
sudo certbot --nginx -d myapp.com -d www.myapp.com
Your Node.js app is now live at https://myapp.com.
Part 10 — Deploy a Python Web Application (Flask or Django)
Python apps work similarly — they run on a port and Nginx proxies to them. We'll use Gunicorn as the Python WSGI server (the equivalent of PM2 for Python apps).
Step 1 — Install Python and pip
Ubuntu 25.04 comes with Python 3.13. Verify:
python3 --version
pip3 --version
If pip isn't installed:
sudo apt install python3-pip python3-venv -y
Step 2 — Set Up Your App
Clone or create your app:
mkdir /home/deploy/myflaskapp && cd /home/deploy/myflaskapp
Create a virtual environment (isolates your project's dependencies from the system):
python3 -m venv venv
source venv/bin/activate
Your prompt will change to show (venv) — you're now inside the virtual environment.
Install Flask and Gunicorn:
pip install flask gunicorn
Create a simple Flask app:
nano app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return 'Hello from my Flask app!'
if __name__ == '__main__':
app.run()
Test it locally:
python app.py
Press Ctrl+C to stop it.
Step 3 — Run with Gunicorn
Gunicorn is a production-ready Python WSGI server — more robust than Flask's built-in development server.
gunicorn --workers 3 --bind 127.0.0.1:5000 app:app
- --workers 3 — 3 parallel worker processes (rule of thumb: 2 x CPU cores + 1)
- --bind 127.0.0.1:5000 — listen on localhost port 5000
- app:app — the file app.py, the object app inside it
Press Ctrl+C to stop for now.
Step 4 — Create a Systemd Service (Auto-start)
Instead of PM2, Python apps typically use systemd to stay running:
sudo nano /etc/systemd/system/myflaskapp.service
[Unit]
Description=Gunicorn instance to serve myflaskapp
After=network.target
[Service]
User=deploy
Group=www-data
WorkingDirectory=/home/deploy/myflaskapp
Environment="PATH=/home/deploy/myflaskapp/venv/bin"
ExecStart=/home/deploy/myflaskapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:5000 app:app
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl start myflaskapp
sudo systemctl enable myflaskapp
Check it's running:
sudo systemctl status myflaskapp
Step 5 — Nginx Reverse Proxy for Python App
sudo nano /etc/nginx/sites-available/myflaskapp.com
server {
listen 80;
server_name myflaskapp.com www.myflaskapp.com;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
sudo ln -s /etc/nginx/sites-available/myflaskapp.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d myflaskapp.com -d www.myflaskapp.com
Your Flask app is now live at https://myflaskapp.com.
Part 11 — Hosting Multiple Sites on One Server
You can host as many websites and apps as you want on one server — each gets its own Nginx server block and its own domain.
The pattern is always the same:
- Create a site config in /etc/nginx/sites-available/
- Enable it with a symlink to /etc/nginx/sites-enabled/
- Run your app on a unique port (3000, 3001, 5000, 5001, etc.)
- Point the domain to your server IP via DNS
- Get an SSL certificate with Certbot
Part 12 — Useful Server Management Commands
Check Disk Space
df -h
Check RAM Usage
free -h
Check Running Processes
htop
Install htop if needed: sudo apt install htop -y
View Nginx Error Logs
sudo tail -f /var/log/nginx/error.log
View Nginx Access Logs
sudo tail -f /var/log/nginx/access.log
Restart a Service
sudo systemctl restart nginx
sudo systemctl restart myflaskapp
Check Service Status
sudo systemctl status nginx
Troubleshooting Common Issues
"502 Bad Gateway" in the Browser
Your app isn't running or crashed. Check:
pm2 logs myapp # For Node.js
sudo systemctl status myflaskapp # For Python
"403 Forbidden"
Nginx can't read your files. Fix permissions:
sudo chown -R www-data:www-data /var/www/mysite.com
sudo chmod -R 755 /var/www/mysite.com
"404 Not Found"
Your Nginx root path or try_files directive is wrong. Double-check the file exists where Nginx is looking.
Certbot Fails
DNS hasn't propagated yet. Wait a few minutes and try again. Also ensure ports 80 and 443 are open in your firewall.
Can't Connect via SSH
Make sure UFW has OpenSSH allowed:
sudo ufw allow OpenSSH
Summary — The Full Stack
Here's the complete picture of what you've built:
Internet visitor
|
Your Domain (DNS -> Server IP)
|
Ubuntu 25.04 Server
|
UFW Firewall (allows 80, 443, SSH)
|
Nginx (web server / reverse proxy)
|
+----------------------------------+
| Static Site | Node.js App |
| /var/www/ | PM2 -> :3000 |
| | Python/Gunicorn |
| | systemd -> :5000 |
+----------------------------------+
Every layer has a job:
- Firewall — only lets in what it should
- Nginx — routes requests to the right place and handles HTTPS
- PM2 / systemd — keeps your apps running forever
- Certbot — keeps your SSL certificate valid
This setup powers millions of production websites and applications worldwide. It scales from a personal blog to a high-traffic platform — just upgrade your VPS plan as you grow.
Derrk Samuel
Web3, crypto, and Linux PC insights — practical guides, security tips, mining setups, blockchain tools, and decentralized tech strategies for techs.
Related Articles
Discussion
Leave a comment
No comments yet. Be the first to start the discussion!
