Web servers on a VPS: nginx, Apache and Caddy

A web server is the program that answers when someone visits your site. On a VPS you choose it yourself, and the three common choices are nginx, Apache and Caddy. All three are free, mature and fast enough for almost any small or medium site. They differ in how they are configured, how they handle HTTPS, and how well they fit the software you plan to run.

This guide explains what a web server does, compares the three, shows a minimal working config for each, and explains where app servers such as Gunicorn, Node.js and PHP-FPM fit. By the end you should know which one to install for your site, and why.

1 GB and 2 GB VPS plans in stock, enough for most small sites

In stock when this page loaded. See all 7 matching plans

What is in this guide

A few words you will see

TermWhat it means
Web serverThe program that receives visits on ports 80 and 443 and sends back pages.
Static sitePlain files such as HTML, CSS and images, sent as they are.
Dynamic sitePages built by code, such as PHP or Python, often using a database.
App serverA program that runs your site's code and hands the result to the web server.
Reverse proxyA web server that passes visits on to an app running on another port.
Virtual hostThe settings for one site on a web server. One server can host many.
HTTPS certificateWhat lets browsers connect securely. Free ones are available and renew automatically.
.htaccessPer-folder settings files that only Apache reads.

What a web server does

When a visitor opens your site, their browser connects to your server on port 443, the HTTPS port. The web server:

  1. Handles the secure connection using your certificate.
  2. Looks at which domain was asked for, so one server can host many sites.
  3. Either sends a file from disk, for static content, or passes the request to an app server, for dynamic content.
  4. Sends the answer back, and writes a line in its access log.

Only one program can listen on ports 80 and 443 at a time. If you run several sites or apps, one web server handles all of them and passes each request to the right place.

The three side by side

General traits. Each can do almost anything the others can, with enough configuration.

CaddynginxApache
HTTPS certificatesAutomatic, built inAdded with a tool such as CertbotAdded with a tool such as Certbot
Config styleShort and readableCompact blocks, more detailLonger, very flexible
.htaccess filesNoNoYes
Reverse proxyVery easyEasy and very commonWorks well with extra modules
PHP sitesThrough PHP-FPMThrough PHP-FPMThrough PHP-FPM, and the best fit for software that ships .htaccess rules
Memory useLowVery lowLow to moderate, depending on setup
Help onlineGood and growingHugeHuge, going back many years
Best known forSimplicity and automatic HTTPSSpeed and reverse proxyingCompatibility with PHP apps and shared-hosting habits

Which one to pick

Your situationA good choiceWhy
A static site or a few Docker apps, and you want it simpleCaddyHTTPS works with almost no setup
Python, Node.js or other apps behind a proxyCaddy or nginxBoth are excellent reverse proxies
WordPress or other PHP apps with plugins that write .htaccess rulesApacheThose rules just work
Moving a site from cPanel shared hostingApachecPanel uses Apache, so your .htaccess files keep working
Following a tutorial written for one of themThe one it usesTranslating configs is a common source of mistakes
A very busy site on a small plannginxVery efficient, and widely tuned for heavy traffic

If you have no strong reason either way, start with Caddy for apps and static sites, and Apache for PHP apps. You can change later.

Caddy: a minimal setup

Caddy is in the package lists of recent Ubuntu and Debian releases. If it is not found, follow the install steps in Caddy's own documentation.

sudo apt install caddy
sudo nano /etc/caddy/Caddyfile

Replace the file's contents with this, using your own domain:

example.com, www.example.com {
    root * /var/www/example
    file_server
    encode gzip
}

Put your files in /var/www/example, open the web ports with sudo ufw allow 80,443/tcp, and reload:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Caddy gets and renews the HTTPS certificate by itself, as long as your domain points at the server. See domains and DNS explained. For an app instead of files, a site block can be as short as this:

app.example.com {
    reverse_proxy 127.0.0.1:8080
}

nginx: a minimal setup

sudo apt install nginx
sudo nano /etc/nginx/sites-available/example.com

Put this in it:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/example;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Enable the site, check it and reload:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Then add HTTPS with Certbot, which edits the config for you and sets up renewals:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

To proxy an app instead of serving files, replace the location block with:

location / {
    proxy_pass http://127.0.0.1:8080;
    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_set_header X-Forwarded-Proto $scheme;
}

The proxy_set_header lines tell the app the visitor's real address and that the original visit used HTTPS.

Apache: a minimal setup

sudo apt install apache2
sudo nano /etc/apache2/sites-available/example.com.conf

Put this in it:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example

    <Directory /var/www/example>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

AllowOverride All lets .htaccess files in the site work. Enable it, check it and reload, then add HTTPS:

sudo a2ensite example.com
sudo a2dissite 000-default
sudo apache2ctl configtest
sudo systemctl reload apache2
sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d example.com -d www.example.com

For a full PHP setup with Apache, see installing WordPress on a VPS, which runs PHP as the site's own user.

2 GB and 4 GB VPS plans in stock, for dynamic sites and apps

In stock when this page loaded. See all 6 matching plans

App servers: Gunicorn, Node.js and PHP-FPM

Web servers are great at files and connections but do not run your code. For dynamic sites, an app server runs the code, and the web server passes requests to it. Keep the app server listening on 127.0.0.1 or a local socket, so only the web server can reach it.

LanguageCommon app serverHow the web server reaches it
PHPPHP-FPMA local socket, through FastCGI
Python (Django, Flask)Gunicorn or uvicornReverse proxy to a local port
Node.jsThe app itselfReverse proxy to a local port
Ruby, Go and othersThe app or its own serverReverse proxy to a local port
Docker appsThe containerReverse proxy to a port published on 127.0.0.1

A Python app with Gunicorn, as an example. Install Gunicorn in your app's virtual environment as its documentation explains, then run it as a service so it starts at boot. Create /etc/systemd/system/myapp.service:

[Unit]
Description=My Python app
After=network.target

[Service]
User=myapp
WorkingDirectory=/home/myapp/app
ExecStart=/home/myapp/app/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 myapp.wsgi:application
Restart=on-failure

[Install]
WantedBy=multi-user.target

Change the user, folders and the last part of the ExecStart line to match your app. Then start it with sudo systemctl daemon-reload and sudo systemctl enable --now myapp, and point your web server's reverse proxy at 127.0.0.1:8000. Each Gunicorn worker uses memory, so start with two or three on a small plan.

One web server, several apps

A single web server can host many sites and apps, each on its own domain or subdomain:

  • example.com served from files.
  • blog.example.com passed to a PHP pool.
  • notes.example.com passed to a Docker app on 127.0.0.1:8081.
  • status.example.com passed to another app on 127.0.0.1:8082.

Each one is a separate site block or virtual host. The web server handles HTTPS for all of them, and the apps stay private behind it. Before making any app public, make sure it has a login and that open sign-up is off. For apps only you use, keep them off the web entirely and reach them over a VPN. See setting up a WireGuard VPN.

Installing and using Docker on a VPS covers publishing Docker apps safely behind a web server.

Troubleshooting

SymptomLikely causeFix
Web server will not start, "address already in use"Another web server already uses port 80 or 443Run sudo ss -tlnp to see which. Stop or remove one of them
Site does not load at allFirewall closed, or DNS not pointing at the serverRun sudo ufw status and dig example.com +short
Certificate fails to issueDNS not ready, or port 80 blockedFix DNS and open port 80, then try again
502 Bad GatewayThe app behind the proxy is not running, or uses another portCheck the app's service and logs, and the port in the config
403 ForbiddenThe web server cannot read the filesCheck folder permissions and the folder named in the config
404 for pages that existWrong folder in the config, or missing rewrite rulesCheck the root folder. On nginx, rewrite rules from .htaccess must be translated
Redirect loopThe app does not know the visit came over HTTPSPass the forwarded headers shown in the nginx section, or set the app's own HTTPS setting
Changes have no effectConfig not reloaded, or a syntax errorRun the check command for your server, then reload

Limits

  • Speed comes from your site more than the server. All three are fast. Caching, image sizes and database queries matter far more than which web server you pick.
  • Configs are not interchangeable. Rules written for one do not work in another. Translate carefully, and test.
  • Package versions vary. Your distribution may ship an older release than the project's own packages. Both work for most sites.
  • It is your job to keep it updated. Web servers face the internet directly, so turn on automatic security updates. See securing your VPS.
  • Fair use applies. Unmetered does not mean unlimited. A popular site with large files uses bandwidth, and RackNerd's acceptable use policy covers what you serve.

When to step up

The web server is rarely what runs out of memory. The apps behind it, especially databases and PHP or Python workers, use far more. Step up when those apps leave little memory free, or when you host several dynamic sites on one server. A 4 GB plan suits several apps comfortably, and 6 GB or 8 GB gives room for busy sites with databases.

4 GB to 8 GB VPS plans in stock, for several sites and apps

In stock when this page loaded. See all 9 matching plans

To compare what is available now, start with the cheapest RackNerd VPS guide and check the sales page.

More RackNerd guides