From Droplet to Live:
Your First Production Server
A complete, no-fluff guide to setting up a production-ready server from scratch. Real commands, real explanations, and real-world analogies — written for people who learn by doing.
Why Build Your Own Server?
Because drag-and-drop hosting won't cut it forever.
If you've ever built a website, you've probably used services like Netlify or Vercel — drag your files, get a link, done. And that's great for simple static sites. But the moment you need a database, a backend API, email sending, cron jobs, or multiple websites on one machine — you need your own server.
This guide walks you through everything — from creating a blank server to having multiple live websites with SSL certificates, firewall protection, and automated security updates. No frameworks, no Docker, no Kubernetes. Just a clean Linux server that you actually understand.
What you'll have by the end:
- A secured Ubuntu server on DigitalOcean
- Nginx serving multiple websites
- Free SSL certificates (auto-renewing)
- Firewall with only necessary ports open
- Automatic security updates
- Fail2Ban blocking attackers
- FileZilla connected for easy file management
Create a Droplet
Your server starts with a few clicks.
A "Droplet" is DigitalOcean's name for a virtual server. Think of it as renting a computer in a data center somewhere — it runs 24/7, has its own IP address, and you control it entirely from your terminal.
Step 1 — Generate an SSH Key
Before creating the Droplet, you need an SSH key. This is like a digital fingerprint — it lets you log into your server without a password (and it's way more secure).
Open PowerShell on your Windows PC and run:
ssh-keygen -t rsa -b 4096
Press Enter for all the questions (default location and no passphrase is fine for now). This creates two files:
id_rsa— Your private key (never share this with anyone)id_rsa.pub— Your public key (this goes on the server)
To find your public key, run:
cat ~/.ssh/id_rsa.pub
Copy the entire output — you'll need it in the next step.
Step 2 — Create the Droplet
Go to DigitalOcean and create a new Droplet with these settings:
- OS: Ubuntu 24.04 LTS (Long Term Support — gets security updates until 2029)
- Plan: Pick based on your needs (1GB RAM is fine for a few static sites)
- Region: Choose the closest to your target audience
- Authentication: Select SSH Key → paste your public key from Step 1
- Hostname: Give it a meaningful name like "production-server"
Once created, DigitalOcean gives you an IP address — something like 143.110.52.87. Save this. It's your server's home address on the internet.
First Login via SSH
Time to enter your new server for the first time.
SSH (Secure Shell) is how you remotely control your server. It's like opening a terminal window directly on that faraway computer. Open PowerShell and run:
ssh root@143.110.52.87
Replace 143.110.52.87 with your actual Droplet IP. If it asks "Are you sure you want to continue connecting?" — type yes and press Enter. This only happens the first time.
You should see something like:
Welcome to Ubuntu 24.04 LTS
root@production-server:~#
That root@production-server:~# means you're in. You're now controlling your server. The root part means you're the admin — you have full control over everything.
root (the super admin), you don't need to type sudo before commands. Every command in this guide assumes you're root.
Update & Upgrade
First thing you do on any new server — update everything.
Think of this like updating apps on your phone. Your server came with software pre-installed, but there might be newer versions available with bug fixes and security patches.
# Step 1 — Check what updates are available apt update # Step 2 — Install all available updates apt upgrade -y
apt update— Like checking "Are there any updates?" (just checking, not installing)apt upgrade -y— Like clicking "Install all updates" (the-ymeans "yes to everything" so it doesn't keep asking for confirmation)
do-release-upgrade) — and you should generally avoid it on production servers.
If during the upgrade you see a prompt asking about configuration files (like "What do you want to do about modified sshd_config?"), always choose "keep the local version currently installed". This preserves your existing settings.
Reboot the Server
Some updates need a restart to take effect.
Just like your PC sometimes says "Restart to finish installing updates" — your server needs the same after major upgrades.
reboot
Your terminal will disconnect — that's completely normal. The server is restarting. Wait about 30 seconds, then reconnect:
ssh root@143.110.52.87
reboot restarts the entire server — only needed after OS-level upgrades. systemctl restart nginx restarts only Nginx — use after config changes. You don't need to reboot the whole server every time you change something.
Install Nginx
The software that actually serves your websites to visitors.
Nginx (pronounced "Engine-X") is a web server. When someone types your domain in their browser, Nginx is the program that receives that request and sends back your HTML, CSS, and JS files. Think of it as a receptionist — it listens at the door (ports 80 and 443) and hands visitors the right files.
# Install Nginx apt install nginx -y # Start it up systemctl start nginx # Make sure it starts automatically on reboot systemctl enable nginx # Check it's running systemctl status nginx
You should see "active (running)" in green. At this point, if you visit your server's IP in a browser (http://143.110.52.87), you'll see the default "Welcome to Nginx" page. That means it's working!
Understanding Ports
Before we set up the firewall, let's understand what we're protecting.
Every server has 65,535 ports. Think of your server as a building with 65,535 doors. Each door serves a specific purpose — some let visitors in, some let you manage the building, and most should stay locked.
| Port | Service | What It Does |
|---|---|---|
| 22 | SSH | Your remote login (PowerShell, FileZilla) |
| 80 | HTTP | Website without SSL (the "not secure" version) |
| 443 | HTTPS | Website with SSL (the secure lock icon version) |
| 21 | FTP | Old-school file transfer (we don't use this) |
| 25 | SMTP | Sending emails |
| 3306 | MySQL | Database connections |
| 5432 | PostgreSQL | Another type of database |
How Hackers Attack Through Ports
Port 22 (SSH) — If this door is open and you use a password, hackers run brute-force attacks — trying thousands of passwords per second like "admin123", "password", "root123" until one works. That's why we use SSH keys instead — it's like replacing a regular lock with a fingerprint scanner.
Port 80/443 (Websites) — Hackers try SQL injection (typing code in your login form to trick your database), or DDoS attacks (sending millions of fake visitors to crash your site — like a crowd blocking a shop entrance so real customers can't enter).
Port 3306 (Database) — If this door is open to the internet, anyone can try to connect to your database directly and steal your data. That's why databases should only be accessible locally.
Port 25 (Email) — Hackers can hijack your server to send spam emails. Your server gets blacklisted and your real emails stop working.
Firewall Setup (UFW)
Lock all 65,535 doors, then only open the ones you need.
UFW stands for Uncomplicated Firewall — and it really is uncomplicated. Right now all 65,535 ports are wide open. We're going to lock them all and only open three: 22 (SSH), 80 (HTTP), and 443 (HTTPS).
# Step 1 — Allow SSH first (so you don't get locked out!) ufw allow OpenSSH # Step 2 — Allow web traffic (HTTP + HTTPS) ufw allow 'Nginx Full' # Step 3 — Turn the firewall ON ufw enable
It will ask "Command may disrupt existing SSH connections. Proceed?" — type y and press Enter. Don't worry, you already allowed SSH so your connection stays alive.
Verify everything:
ufw status
You should see OpenSSH and Nginx Full both showing ALLOW. Everything else is blocked by default.
ufw allow 3306 for MySQL, ufw allow 3000 for a Node.js app, etc. Only open what you actually use.
Security Hardening
Three extra layers of protection that run on autopilot.
8.1 — Verify Password Login is Disabled
Since you chose SSH key during Droplet creation, password login should already be disabled. But DigitalOcean has a sneaky config file called cloud-init that can override your main SSH settings. Let's verify both:
# Check the main SSH config grep PasswordAuthentication /etc/ssh/sshd_config # Check the cloud-init override (the sneaky one!) grep PasswordAuthentication /etc/ssh/sshd_config.d/50-cloud-init.conf
You want both to show PasswordAuthentication no. If the main config shows #PasswordAuthentication yes (with a # at the start), that's fine — the # means it's commented out (disabled). The cloud-init file is the one that actually matters.
8.2 — Enable Automatic Security Updates
This makes your server automatically install critical security patches in the background — like Windows auto-update but only for security fixes.
# Install the auto-update tool apt install unattended-upgrades -y # Configure it — select "Yes" when asked dpkg-reconfigure --priority=low unattended-upgrades
Breaking down that second command: dpkg-reconfigure means "open the settings of an installed tool." --priority=low means "show me all the options." unattended-upgrades is the tool name — "unattended" means "without you being there" and "upgrades" means "updates."
8.3 — Install Fail2Ban
Fail2Ban watches your server's logs and if someone fails to login multiple times (like 5 wrong attempts), it automatically bans their IP address. Think of it as a bouncer who kicks out troublemakers.
# Install Fail2Ban apt install fail2ban -y # Create a local config (so updates don't overwrite your settings) cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local # Start it and enable on boot systemctl start fail2ban systemctl enable fail2ban # Verify it's running systemctl status fail2ban
Default settings are solid — it bans anyone who fails 5 login attempts for 10 minutes. You'll see "active (running)" if everything's good.
8.4 — Block Direct IP Access
Right now, if someone types your server's IP address in a browser, they'll see one of your websites (whichever loads first alphabetically). That's not ideal — it tells hackers "hey, this is an active Nginx server." Let's make the IP show nothing.
Create a file called 00-block-ip in /etc/nginx/sites-available/ with this content:
# Block direct IP access — drop connection silently
server {
listen 80 default_server;
server_name _;
return 444;
}
The underscore _ means "no real domain" — it catches all requests that don't match any domain. 444 is Nginx's special code that silently closes the connection. The file is named 00-block-ip so it loads first alphabetically.
# Enable it ln -s /etc/nginx/sites-available/00-block-ip /etc/nginx/sites-enabled/ # Test and reload nginx -t systemctl reload nginx
Now visiting http://143.110.52.87 directly shows nothing. Connection drops silently.
Connect FileZilla (SFTP)
A visual way to manage your server's files — drag and drop instead of commands.
FileZilla lets you browse your server's files just like File Explorer on Windows. You can drag-and-drop files between your PC and server instead of typing commands.
Open FileZilla → File → Site Manager → New Site and fill in:
- Protocol: SFTP - SSH File Transfer Protocol
- Host:
143.110.52.87(your server IP) - Port:
22 - Logon Type: Key file
- User:
root - Key file: Browse to
C:\Users\YOUR_USERNAME\.ssh\id_rsa
id_rsa file, change the file filter at the bottom from "PPK files" to "All files". FileZilla will ask to convert it to .ppk format — click Yes and save it.
Click Connect. You should see your server's files on the right side of FileZilla. Navigate to /var/www/ — this is where all your website files will live.
Upload Website Files
Put your site files where Nginx can find them.
The standard location for website files on a Linux server is /var/www/. Each website gets its own folder:
/var/www/
├── mysite.com/
│ └── index.html
├── blog.mysite.com/
│ └── index.html
├── app.mysite.com/
│ └── index.html
└── html/ ← default Nginx page (we disabled this)
In FileZilla, navigate to /var/www/ on the right panel. Then on the left panel (your PC), find your website files. Simply drag and drop your website folder from left to right.
You can also create folders directly in FileZilla — right-click on the right panel → Create directory → name it after your domain.
index.html is at the root of your site folder — not buried inside a subfolder. If your files are inside a html/ subfolder, you'll need to account for that in your Nginx config later.
Nginx Config Files
Telling Nginx which domain goes to which folder.
Nginx needs to know: "When someone visits mysite.com, which folder should I serve files from?" That's what config files do. They live in two folders:
/etc/nginx/sites-available/— All your config files (like a filing cabinet)/etc/nginx/sites-enabled/— Only the active ones (shortcuts pointing to the cabinet)
Create a Config File
In FileZilla, navigate to /etc/nginx/sites-available/. Right-click → Create new file → name it after your domain (e.g., mysite.com). Then right-click → View/Edit and paste:
# Config for mysite.com server { # Listen on port 80 (HTTP) — Certbot will add SSL later listen 80; # Domain names this site responds to server_name mysite.com www.mysite.com; # Where the site files are stored root /var/www/mysite.com; # Default file to load index index.html; # How to handle requests location / { # Try the URL as a file, then folder, then show 404 try_files $uri $uri/ =404; } }
Save and close. Each line explained:
listen 80— Listen on port 80 (HTTP, no SSL yet — Certbot adds that later)server_name— The domain(s) that trigger this configroot— The folder path where your website files liveindex— The default file to serve when someone visits the root URLtry_files— If someone visits a URL, check if a file exists at that path, then check if it's a folder, otherwise show a 404 error
Enable the Site
Creating the config doesn't activate it. You need to create a shortcut (symlink) from sites-available to sites-enabled:
# Create the shortcut ln -s /etc/nginx/sites-available/mysite.com /etc/nginx/sites-enabled/ # Test if the config file has no errors nginx -t # If test passes, reload Nginx systemctl reload nginx
The nginx -t command is your safety net — it checks for syntax errors before applying changes. Always run it before reloading. If it shows "syntax is ok" and "test is successful" — you're good.
sites-available and its own symlink in sites-enabled. One server can host as many sites as you want.
Disable the Default Site
Nginx comes with a default config that shows the "Welcome to Nginx" page. Remove it:
# Remove the shortcut (original file stays safe in sites-available) rm /etc/nginx/sites-enabled/default # Test and reload nginx -t systemctl reload nginx
This only removes the shortcut, not the actual config file. If you ever need it back, just create the symlink again. With the default disabled, only your actual domains will work — no more "Welcome to Nginx" page on the bare IP.
Point Your Domain (DNS)
Connecting your domain name to your server's IP address.
Right now, your website lives at http://143.110.52.87 — but nobody's going to remember that. DNS (Domain Name System) is like the internet's phone book — it translates mysite.com to 143.110.52.87.
Go to your domain registrar (wherever you bought your domain — Namecheap, GoDaddy, Cloudflare, etc.) and add A records:
| Type | Host | Value |
|---|---|---|
| A | @ | 143.110.52.87 |
| A | www | 143.110.52.87 |
The @ symbol means the root domain (mysite.com), and www handles www.mysite.com.
For subdomains (like blog.mysite.com or app.mysite.com), add more A records:
| Type | Host | Value |
|---|---|---|
| A | blog | 143.110.52.87 |
| A | app | 143.110.52.87 |
DNS changes can take anywhere from a few minutes to a few hours to propagate. You can verify if DNS has updated using:
# Check if your domain points to the right IP
dig mysite.com +short
dig www.mysite.com +short
Both should return your server's IP. Only proceed to SSL once DNS is fully propagated — Certbot needs to verify domain ownership.
Install SSL (HTTPS)
Free HTTPS with Let's Encrypt — because "Not Secure" warnings kill trust.
SSL makes your site use https:// instead of http:// — that lock icon in the browser. Without it, browsers show a scary "Not Secure" warning to your visitors. Certbot makes this completely free using Let's Encrypt certificates.
Install Certbot
apt install certbot python3-certbot-nginx -y
Get a Certificate
Run Certbot for each domain:
# For your main site (includes www) certbot --nginx -d mysite.com -d www.mysite.com # For a subdomain certbot --nginx -d blog.mysite.com
What each flag does:
--nginx— "I'm using Nginx, handle everything automatically" (Certbot edits your Nginx config to add SSL)-d— Each-dadds a domain to the certificate
The first time Certbot runs, it asks for your email address (for expiry reminders) and to agree to terms. After that, it handles everything automatically — generates the certificate, updates your Nginx config, and reloads Nginx.
Verify Your Certificates
# See all your active certificates
certbot certificates
Test Auto-Renewal
Let's Encrypt certificates expire every 90 days, but Certbot automatically renews them. Test that the auto-renewal works:
# Dry run = pretend to renew (doesn't actually change anything)
certbot renew --dry-run
If you see "Congratulations, all simulated renewals succeeded" — your certificates will auto-renew forever without you lifting a finger.
--nginx flag automatically reloads Nginx after getting the certificate. No need to restart manually.
Troubleshooting
Common issues you'll run into (and how to fix them).
Nginx Config Test Fails
If nginx -t throws an error like "open() failed — No such file or directory", it usually means you have a symlink in sites-enabled pointing to a config file that doesn't exist yet.
# List all symlinks in sites-enabled ls -la /etc/nginx/sites-enabled/ # Remove the broken one rm /etc/nginx/sites-enabled/broken-config-name # Test again nginx -t
403 Forbidden Error
This means Nginx found the folder but can't serve files from it. Two common causes:
Cause 1: No index.html in the root folder. Your files might be inside a subfolder. Check:
ls -la /var/www/mysite.com/
If you see a html/ subfolder containing your files, either move the files up or update your Nginx config's root line to /var/www/mysite.com/html.
Cause 2: Permission issues. Fix with:
chmod -R 755 /var/www/mysite.com/
Certbot Fails — Domain Not Pointing
If Certbot gives "unauthorized" or mentions the wrong IP address, your DNS hasn't fully propagated yet. Verify with dig yourdomain.com +short and wait until it shows your server's IP.
Cloud-Init SSH Override
You edited /etc/ssh/sshd_config to disable password login, but it's still enabled? Check the cloud-init override file — it takes priority over the main config:
cat /etc/ssh/sshd_config.d/50-cloud-init.conf
If it shows PasswordAuthentication yes, edit it and change to no, then restart SSH:
systemctl restart sshd
Final Checklist
Make sure everything is locked down before you go live.
- SSH key login working — no password
- Password authentication disabled (check cloud-init too!)
- System packages updated and upgraded
- Nginx installed and running
- Firewall enabled — only ports 22, 80, 443 open
- Automatic security updates enabled
- Fail2Ban active and running
- Direct IP access blocked (returns nothing)
- Website files uploaded to /var/www/
- Nginx config files created and enabled
- Default Nginx site disabled
- DNS A records pointing to server IP
- SSL certificates installed for all domains
- SSL auto-renewal tested with dry-run
- All sites loading with https:// and lock icon