$linuxjunkies
>

Host Multiple Sites on One Linux Server

Host multiple websites on one Linux server using Nginx as a reverse proxy, systemd-managed app services, per-domain TLS, and isolated system users.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

Before you start

  • A Linux server with a public IP address and root or sudo access
  • DNS A records for each domain already pointing to the server's IP
  • Application code deployed under /srv or /opt with a known start command
  • Basic familiarity with systemd service management and a text editor

Running several web applications on a single Linux server is a common production pattern. A reverse proxy sits in front of all your apps, accepts requests on ports 80 and 443, and forwards each request to the correct backend based on the domain name or path. Each app listens on a private port or Unix socket that is never exposed to the internet. This guide uses Nginx as the reverse proxy, but the architecture applies equally to Caddy or Apache. The app tier examples use Node.js and Python/Gunicorn to show the pattern with two real stacks.

Architecture Overview

Before touching the server, sketch your layout:

  • Nginx binds to 0.0.0.0:80 and 0.0.0.0:443 — the only publicly exposed ports.
  • Each app runs as its own non-root system user and listens on a private TCP port (e.g., 3000, 8000) or a Unix socket under /run/.
  • DNS A records for every domain point to the same server IP.
  • TLS certificates are obtained per-domain with Certbot (Let's Encrypt).

Unix sockets are slightly faster than loopback TCP and avoid port-number sprawl. Use TCP ports when containers or cross-host routing are involved; use sockets for local processes you control directly.

Step 1: Install Nginx

Debian / Ubuntu

sudo apt update
sudo apt install -y nginx

Fedora / RHEL / Rocky

sudo dnf install -y nginx

Arch

sudo pacman -S --noconfirm nginx

Enable and start the service:

sudo systemctl enable --now nginx

Step 2: Create Isolated System Users for Each App

Never run application code as root or as your own login user. Create a dedicated system account per app so a compromise stays contained.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin nodeapp
sudo useradd --system --no-create-home --shell /usr/sbin/nologin pyapp

Place each application's files under /srv or /opt and set ownership accordingly:

sudo mkdir -p /srv/nodeapp /srv/pyapp
sudo chown nodeapp: /srv/nodeapp
sudo chown pyapp:  /srv/pyapp

Step 3: Write systemd Service Units for Each App

systemd manages process lifecycle, restarts, and logging. Define one unit per application.

Node.js app on TCP port 3000

sudo tee /etc/systemd/system/nodeapp.service <<'EOF'
[Unit]
Description=Node.js App (site1.example.com)
After=network.target

[Service]
Type=simple
User=nodeapp
WorkingDirectory=/srv/nodeapp
ExecStart=/usr/bin/node /srv/nodeapp/server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production PORT=3000

[Install]
WantedBy=multi-user.target
EOF

Python/Gunicorn app on a Unix socket

sudo tee /etc/systemd/system/pyapp.service <<'EOF'
[Unit]
Description=Gunicorn App (site2.example.com)
After=network.target

[Service]
Type=notify
User=pyapp
WorkingDirectory=/srv/pyapp
ExecStart=/srv/pyapp/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/run/pyapp/gunicorn.sock \
    wsgi:app
RuntimeDirectory=pyapp
RuntimeDirectoryMode=0750
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

RuntimeDirectory=pyapp tells systemd to create /run/pyapp/, owned by the service user, at startup — no manual mkdir required, and it is cleaned up on reboot automatically.

Reload systemd and start both services:

sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp pyapp

Verify they are running:

sudo systemctl status nodeapp pyapp

Step 4: Configure Nginx Virtual Hosts

Create one configuration file per site inside /etc/nginx/sites-available/ (Debian/Ubuntu) or /etc/nginx/conf.d/ (RHEL family / Arch). The pattern is identical; only the directory differs.

site1.example.com → Node.js on port 3000

sudo tee /etc/nginx/sites-available/site1.example.com <<'EOF'
server {
    listen 80;
    server_name site1.example.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_set_header   X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}
EOF

site2.example.com → Gunicorn on Unix socket

sudo tee /etc/nginx/sites-available/site2.example.com <<'EOF'
server {
    listen 80;
    server_name site2.example.com;

    location / {
        proxy_pass         http://unix:/run/pyapp/gunicorn.sock;
        proxy_http_version 1.1;
        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;
    }
}
EOF

On Debian/Ubuntu, symlink the files to enable them:

sudo ln -s /etc/nginx/sites-available/site1.example.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site2.example.com /etc/nginx/sites-enabled/

On RHEL/Arch, drop the files directly in /etc/nginx/conf.d/ — no symlink step needed.

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Step 5: Obtain TLS Certificates with Certbot

# Debian/Ubuntu
sudo apt install -y certbot python3-certbot-nginx

# Fedora/RHEL
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx -d site1.example.com -d site2.example.com

Certbot edits your Nginx configs to add the TLS directives and sets up a systemd timer (or cron job) for automatic renewal. Verify the timer is active:

sudo systemctl status certbot.timer

Step 6: Open the Firewall

Only ports 80 and 443 need to be reachable from the internet. Your app ports (3000, Unix sockets) must remain closed externally.

ufw (Debian/Ubuntu)

sudo ufw allow 'Nginx Full'
sudo ufw enable

firewalld (Fedora/RHEL/Rocky)

sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload

Step 7: Verify Everything End-to-End

# Check both apps are listening
sudo ss -tlnp | grep -E '3000'
sudo ls -lh /run/pyapp/gunicorn.sock

# Test HTTP response headers for each site
curl -I http://site1.example.com
curl -I http://site2.example.com

# After TLS is active, confirm certificate
curl -vI https://site1.example.com 2>&1 | grep -E 'subject|expire|HTTP'

Expected: HTTP 200 (or your app's response code) and a valid certificate chain. If you see a 502 Bad Gateway, the app is not running or Nginx cannot reach its socket/port — see troubleshooting below.

Troubleshooting

  • 502 Bad Gateway — The backend is down or the path/port is wrong. Run sudo systemctl status nodeapp or sudo journalctl -u pyapp -n 50 to see crash output. Double-check the proxy_pass address matches where the app is actually listening.
  • Unix socket permission denied — Nginx runs as the www-data (Debian) or nginx (RHEL/Arch) user. The socket directory must be group-readable by that user, or add the Nginx user to the app's group: sudo usermod -aG pyapp www-data, then reload Nginx.
  • Certbot fails DNS check — Ensure your DNS A record has propagated and port 80 is open before running Certbot. Use dig +short site1.example.com to confirm.
  • SELinux blocking connections (RHEL/Rocky) — Allow Nginx to connect to the network: sudo setsebool -P httpd_can_network_connect 1. For Unix sockets, the socket file's SELinux context must be httpd_var_run_t.
  • Adding a third site — Follow the same pattern: new user, new service unit, new Nginx config, then certbot --nginx -d site3.example.com. The reverse proxy scales horizontally with no changes to existing sites.
tested on:Ubuntu 24.04Debian 12Rocky 9Arch rolling

Frequently asked questions

Should I use Unix sockets or TCP ports for the backend apps?
Unix sockets are faster for local processes and avoid managing port numbers, making them the better default for apps running directly on the host. Use TCP ports when apps run in containers or need to be reachable from another machine.
Can I use Caddy instead of Nginx for the reverse proxy?
Yes. Caddy uses a Caddyfile with one site block per domain, handles TLS automatically without Certbot, and supports the same proxy_pass pattern. The app-tier setup with systemd users and service units is identical regardless of which reverse proxy you choose.
How many sites can one server realistically host?
That depends entirely on each app's resource usage, not on the reverse proxy. Nginx itself adds negligible overhead. A $20/month VPS with 2 GB RAM can comfortably run five to ten lightweight apps; heavier frameworks may limit you to two or three.
Do I need a separate Nginx config file per site, or can I put everything in one file?
Separate files per site are strongly preferred. They make it easy to disable a single site with one symlink removal, reduce the chance of a typo breaking all sites, and keep Certbot's automated edits isolated.
What happens to the other sites if one app crashes?
Only the site backed by that app is affected; Nginx returns a 502 for it while all other sites continue serving normally. systemd will attempt to restart the failed service automatically according to its Restart policy.

Related guides