...

How to Host Multiple Websites on a Single VPS with Nginx Proxy Manager

Martin Klein

Reading time 1 minute

In this guide, we will deploy two demo websites on a single VPS and configure routing to them through Nginx Proxy Manager. Both applications will run in Docker containers and share an internal Docker network with the reverse proxy.

In the process, we will:

  • Install Docker and Docker Compose;
  • Deploy Nginx Proxy Manager and two separate websites;
  • Connect the containers to a shared Docker network;
  • Assign different domain names to the websites;
  • Create a separate Proxy Host for each website;
  • Configure SSL certificates and HTTP → HTTPS redirection;
  • Review how Access Lists work for restricting access;
  • Explain why it is better not to publish applications’ internal ports directly to the internet.

As a result, a single VPS will be able to serve multiple websites through shared entry points on ports 80 and 443, while the applications themselves remain isolated inside the Docker network.

What We’ll Deploy

In this guide, we will deploy two independent demo websites on a single VPS and configure access to them through Nginx Proxy Manager. Each website will run in its own Docker container, and Nginx Proxy Manager will handle request routing, domain management, and HTTPS.

This setup is suitable when you need to host multiple websites, control panels, or web applications on a single server without manually installing and configuring a separate Nginx instance for each project, and without having to separate them by port or IP address.

How the setup will work

External traffic will arrive at a single public VPS IP address through standard ports 80 and 443. Nginx Proxy Manager will identify the required service by the domain name (SNI) and forward the request to the appropriate container.

At a high level, the setup will look like this:

We will connect both websites and Nginx Proxy Manager to a shared user-defined Docker network. Thanks to Docker’s built-in DNS, the reverse proxy will be able to access applications by container or service name instead of constantly changing internal IP addresses.

The websites’ own ports will not need to be published on the VPS interface. Only Nginx Proxy Manager and the ports it requires will be accessible from the internet. This approach reduces the number of services directly exposed externally and simplifies routing management.

Deployment prerequisites

To complete this guide, you will need:

  • A VPS running Ubuntu 24.04 with SSH access and sudo privileges;
  • A public IPv4 address;
  • Docker Engine and Docker Compose;
  • Two domain names or subdomains pointing to the VPS;
  • Open TCP ports 80 and 443 for HTTP and HTTPS;
  • The Nginx Proxy Manager management port, which we will use for initial setup only;
  • Two demo web applications.

In the examples, we will use the placeholder public address 203.0.113.10. This address is from a range reserved for documentation, so in a real deployment you must replace it with the public IP address of your VPS.

Preparing the VPS

Before deploying Nginx Proxy Manager, we will install Docker and prepare a dedicated working directory for the Compose files and configuration.

Installing Docker

Update the package list and install the dependencies required to add the official Docker repository:

sudo apt update

sudo apt install -y ca-certificates curl

Create a directory for APT keys and import the official Docker key:

sudo install -m 0755 -d /etc/apt/keyrings

sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \

-o /etc/apt/keyrings/docker.asc

sudo chmod a+r /etc/apt/keyrings/docker.asc

Add the official Docker repository:

echo \

"deb [arch=$(dpkg –print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \

$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \

sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Update the package index and install Docker Engine along with the Compose Plugin:

sudo apt update

sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

After installation, check the component versions:

docker –version

docker compose version

If both commands return the installed versions, Docker is ready to use.

Creating a working directory

To keep the Nginx Proxy Manager files and the demo site files in one place, create a separate project directory:

mkdir -p ~/npm-guide

cd ~/npm-guide

We will later place the Compose files, application data, and other required files here. A separate working directory will also make it easier to manage the environment and remove it when you are finished.

Deploying Nginx Proxy Manager

Now we will deploy Nginx Proxy Manager in Docker. It will serve as a single entry point for both sites: accepting HTTP and HTTPS requests, selecting the appropriate Proxy Host based on the domain name, and forwarding traffic to the corresponding container.

Creating the Compose file

In the working directory, create a compose.yaml file: nano compose.yaml

Add the Nginx Proxy Manager and MariaDB configuration:

services:

npm:

image: jc21/nginx-proxy-manager:2.12.6

container_name: npm

restart: unless-stopped

ports:

– "80:80"

– "81:81"

– "443:443"

environment:

DB_MYSQL_HOST: db

DB_MYSQL_PORT: 3306

DB_MYSQL_USER: npm

DB_MYSQL_PASSWORD: change_this_password

DB_MYSQL_NAME: npm

volumes:

– npm_data:/data

– npm_letsencrypt:/etc/letsencrypt

depends_on:

– db

networks:

– proxy

db:

image: mariadb:11.4

container_name: npm-db

restart: unless-stopped

environment:

MARIADB_ROOT_PASSWORD: change_root_password

MARIADB_DATABASE: npm

MARIADB_USER: npm

MARIADB_PASSWORD: change_this_password

volumes:

– npm_db:/var/lib/mysql

networks:

– proxy

volumes:

npm_data:

npm_letsencrypt:

npm_db:

networks:

proxy:

name: proxy

Here, Nginx Proxy Manager publishes three ports:

  • 80 — HTTP;
  • 443 — HTTPS;
  • 81 — the administrative web interface.

MariaDB does not publish port 3306 on the host and is available only to containers within the proxy network.

In a production environment, it is better to move passwords to a .env file. For this demo configuration, however, we will keep them directly in the Compose file and replace them with our own values before starting.

Save the file and exit the editor.

Starting containers

Start the services in the background: docker compose up -d

After the images have been pulled, check the container status: docker compose ps

The output should include npm and npm-db with the status Up.

You can also check the Docker network that was created: docker network ls

The proxy network will be needed later when we connect both demo sites to it.

Initial login to Nginx Proxy Manager

After startup, the Nginx Proxy Manager administration interface will be available on port 81: http://203.0.113.10:81

In a real environment, use the public IP address of your VPS instead of 203.0.113.10.

When you first launch Nginx Proxy Manager, use the default credentials:

Email: [email protected]

Password: changeme

After the first login, Nginx Proxy Manager will prompt you to change the administrator details. Enter your own email address and a new strong password.

After you log in, the Dashboard will open. It displays Proxy Hosts, certificates, Access Lists, and other objects that we will use later.

Port 81 is intended for administration and should not remain accessible to the entire internet unless necessary. In a production environment, access to it should be restricted with a firewall, VPN, or trusted IP addresses.

Deploying Two Demo Sites

Now we will prepare two simple websites. Each one will run in its own container, but both will be connected to the same proxy network as Nginx Proxy Manager.

For this demonstration, two Nginx containers with different static pages are sufficient. This clearly illustrates domain-based routing without adding unnecessary application logic.

Creating the First Website

Create directories for the websites: mkdir -p site1 site2

Create the home page for the first website: nano site1/index.html

Add some simple HTML:

<!DOCTYPE html>

<html lang=&quot;en&quot;>

<head>

<meta charset=&quot;UTF-8&quot;>

<title>Site One</title>

</head>

<body>

<h1>Site One</h1>

<p>This website is running behind Nginx Proxy Manager.</p>

</body>

</html>

Creating the second site

Create the page for the second site in the same way: nano site2/index.html

Add distinct content:

<!DOCTYPE html>

<html lang=&quot;en&quot;>

<head>

<meta charset=&quot;UTF-8&quot;>

<title>Site Two</title>

</head>

<body>

<h1>Site Two</h1>

<p>This is the second website on the same VPS.</p>

</body>

</html>

Now add two new services to compose.yaml:

site1:

image: nginx:1.28-alpine

container_name: site1

restart: unless-stopped

volumes:

– ./site1:/usr/share/nginx/html:ro

networks:

– proxy

site2:

image: nginx:1.28-alpine

container_name: site2

restart: unless-stopped

volumes:

– ./site2:/usr/share/nginx/html:ro

networks:

– proxy

Note that these containers do not have a ports section. This is intentional: the sites will be accessible only within the Docker network and will receive external traffic exclusively through Nginx Proxy Manager.

Apply the changes: docker compose up -d

Connecting containers to a shared Docker network

All four containers — npm, npm-db, site1, and site2 — are connected to the proxy network.

Inspect its contents: docker network inspect proxy

The Containers section should show the Nginx Proxy Manager container, the database container, and both site containers.

Thanks to Docker’s built-in DNS, Nginx Proxy Manager can reach the sites simply by name:

site1:80

site2:80

There is no need to use the containers’ internal IP addresses manually.

Why internal application ports should not be exposed to the internet

When you publish a port, Docker creates a direct path from the VPS network interface to the container. For example:

ports:

– &quot;8081:80&quot;

would make the first site directly accessible at: http://203.0.113.10:8081

This is not required in our setup. Nginx Proxy Manager already receives external traffic and forwards it to the appropriate service inside the proxy network.

Therefore, only the reverse proxy ports are exposed externally:

80/tcp

443/tcp

while the internal sites remain accessible only to containers on the Docker network.

This provides several benefits:

  • Fewer services are directly accessible from the internet;
  • Users cannot bypass the reverse proxy and access the application through an internal port;
  • SSL, redirects, and access rules are applied centrally;
  • The configuration remains easier to understand when adding new sites.

This is why, for containerized applications behind a reverse proxy, there is usually no need to publish each internal port separately.

Pointing domains to the VPS

For Nginx Proxy Manager to determine which container to forward a request to, each site needs a separate domain name or subdomain. Both names can point to the same public VPS IP address; traffic will be routed at the reverse proxy level.

In the examples, we use placeholder domains:

site1.example.com

site2.example.com

and the documentation IP address 203.0.113.10 instead of the real VPS address.

DNS records for the first and second sites

In the DNS control panel, create two A records:

TypeNameValue
Asite1203.0.113.10
Asite2203.0.113.10

In a real configuration, replace 203.0.113.10 with the public IPv4 address of your VPS.

Both records point to the same server. This is expected: after receiving a request, Nginx Proxy Manager checks the domain name in the request and selects the appropriate Proxy Host.

If DNS is managed through Cloudflare, it is more convenient to temporarily disable proxying for the DNS record and use DNS only mode during the initial setup and certificate issuance. After the setup is complete, you can enable it again if needed.

DNS changes may not take effect immediately. Propagation speed depends on the DNS provider, the record TTL, and resolver caches.

Checking DNS

Before creating Proxy Hosts, make sure that both names resolve to the VPS address.

You can check the records with the following commands:

getent ahostsv4 site1.example.com

getent ahostsv4 site2.example.com

or:

dig +short A site1.example.com

dig +short A site2.example.com

In a real configuration, both commands should return the server’s public IPv4 address.

In our example, the expected result would look roughly like this:

203.0.113.10

203.0.113.10

If a different address is returned or the record does not resolve yet, you should not proceed with issuing the SSL certificate. First, wait for the DNS update to propagate or verify that the records were created correctly.

Configuring the First Proxy Host

After configuring DNS, you can link the first domain to the site1 container. To do this, create a Proxy Host in Nginx Proxy Manager—a rule that defines where requests for a specific domain name should be forwarded.

Adding the Domain and Container Address

Open Hosts → Proxy Hosts and click Add Proxy Host.

For the first site, specify:

Domain Names: site1.example.com

Scheme: http

Forward Hostname / IP: site1

Forward Port: 80

If necessary, you can also enable the options Block Common Exploits and Websockets Support. Our simple static website does not require WebSocket; however, for applications that use it, this setting must be enabled.

At this stage, you can leave SSL unconfigured for now — we will add the certificate separately after creating both Proxy Hosts.

After saving, the rule will forward requests for site1.example.com to port 80 on the first site’s container.

How Nginx Proxy Manager Accesses a Container by Name

In the field Forward Hostname / IP we specified not the container IP address, but the name site1. This is possible thanks to the shared Docker network proxy.

Docker provides built-in DNS for containers on the same user-defined network. When Nginx Proxy Manager connects to: http://site1:80

Docker automatically resolves the name site1 to the internal address of the corresponding container.

This is preferable to manually specifying the internal IP address. A container’s address can change after it is recreated or updated, while the service name remains the same.

As a result, the route looks like this:

Only the VPS is assigned a public address, while the direct connection between the reverse proxy and the site takes place within the Docker network.

Configuring the Second Proxy Host

After setting up the first site, add a second route. Since both containers are on the same Docker network, proxy, no separate network configuration is required—just specify a different domain name and the name of the second service.

Creating the Second Route

Go to Hosts → Proxy Hosts and once again click Add Proxy Host.

For the second site, specify:

Domain Names: site2.example.com

Scheme: http

Forward Hostname / IP: site2

Forward Port: 80

As with the first site, you can enable Block Common Exploits. The Websockets Support option is not required for our static site.

After saving, Nginx Proxy Manager will be configured with a second route:

site2.example.com

Nginx Proxy Manager

site2:80

A single reverse proxy instance now serves two independent sites. Both use the same external ports, 80 and 443, and the appropriate container is selected based on the domain name in the HTTP request.

Checking the Proxy Hosts List

Let’s return to Hosts → Proxy Hosts. The list should display two rules:

site1.example.com → site1:80

site2.example.com → site2:80

If both Proxy Hosts appear as active, basic routing is configured. When users access different domains, Nginx Proxy Manager will route the requests to different containers, even though both sites are physically hosted on the same VPS.

Configuring HTTPS and HTTP → HTTPS Redirection

At this stage, both sites are already linked to Proxy Hosts. Next, we will enable HTTPS so that the connection between the user’s browser and Nginx Proxy Manager is encrypted.

Each domain will require either a separate certificate or a certificate that covers multiple names. In this example, we will configure certificates using Nginx Proxy Manager’s built-in Let’s Encrypt integration.

Obtaining a Let’s Encrypt Certificate

Open the settings for the first Proxy Host and go to the tab labeled SSL.

In the field SSL Certificate select: Request a new SSL Certificate

Next, specify the email address for Let’s Encrypt, accept the terms of service, and submit the certificate issuance request.

Before issuing the certificate, make sure that:

  • The domain already resolves to the VPS public IP address;
  • Ports 80 and 443 are accessible from the internet;
  • Requests to the domain are actually reaching Nginx Proxy Manager;
  • No other web server on the VPS is using these ports.

After successful issuance, the certificate will be stored by Nginx Proxy Manager and assigned to the selected Proxy Host.

Perform the same operation for the second site.

Enabling Force SSL

After selecting the certificate, enable the parameter Force SSL.

This forces Nginx Proxy Manager to redirect regular HTTP requests to the secure HTTPS version of the site.

For the Proxy Host, the final settings should look something like this:

SSL Certificate: Let’s Encrypt

Force SSL: Enabled

HTTP/2 Support: Enabled

You can enable HTTP/2 along with HTTPS if it is available in the version of Nginx Proxy Manager you are using.

Repeat the configuration for the second Proxy Host.

How redirection to HTTPS works

After enabling Force SSL the user can enter the regular address http://site1.example.com, but Nginx Proxy Manager will automatically redirect them to https://site1.example.com

The interaction flow then looks like this:

It is important to note that in this configuration, HTTPS is terminated at Nginx Proxy Manager. Regular HTTP is used within the Docker network between the reverse proxy and the demo container.

For many applications running on a single VPS, this is sufficient: the internal service is not exposed to the internet, and all external traffic passes through a single HTTPS entry point.

The same pattern works for the second site as well: https://site2.example.com → Nginx Proxy Manager → site2:80

As a result, both sites get separate domain names and HTTPS, without needing to install Certbot separately or manually maintain separate Nginx configuration files for each project.

Configuring Access Lists

Nginx Proxy Manager allows you not only to route traffic but also to restrict access to individual Proxy Hosts. This is done using Access Lists.

This mechanism is useful if the same VPS hosts administrative panels, test versions of websites, internal tools, or other services that should not be accessible to all internet users.

What Access Lists Are Used For

An Access List can be attached to a specific Proxy Host to define who is allowed to access it.

Depending on the use case, you can use:

  • Username and password authentication;
  • Allowing access only from specific IP addresses or networks;
  • Denying access for individual addresses;
  • A combination of multiple rules.

For example, a public website can remain accessible to everyone, while the admin panel can be restricted to the corporate network or VPN.

Access Lists operate at the Nginx Proxy Manager level. The application container itself remains inside the Docker network and does not expose its own port externally.

Creating an access list

Open the section Access Lists in the Nginx Proxy Manager interface and create a new list.

For this example, name it: Admin Only

On the tab Authorization you can add a username and password for HTTP Basic Authentication.

For example:

Username: admin

Password: use a strong password

On the Access tab, you can configure network rules. For example, allow access only from a specific subnet: 192.0.2.0/24

or from a specific public IP address: 192.0.2.10

The addresses above are provided only as examples from documentation ranges. In a production configuration, use actual trusted addresses or subnets.

The order and set of rules depend on the use case. If the list is intended for an internal panel, the usual approach is to allow only explicitly specified sources rather than opening access to the entire internet.

Associating an Access List with a Proxy Host

After saving the list, return to Hosts → Proxy Hosts, open the required Proxy Host, and select the created list in the Access List.

For example:

Proxy Host: site2.example.com

Access List: Admin Only

After saving, requests to this domain will be processed according to the specified access rules.

For a typical public website, the Access List can be left as Publicly Accessible. It makes sense to enable restrictions only where they are actually needed.

Testing Multiple Websites

The main configuration is ready. Now verify that the two domains serve different sites, that HTTPS is working, and that the application containers cannot be accessed directly through the VPS’s public ports.

Verifying the first site

Open the first domain in a browser: https://site1.example.com

The page from the first container should load:

Site One

This website is running behind Nginx Proxy Manager.

This means the entire chain is working end to end:

Next, we will verify the second site.

Checking the second site

Now open the second domain: https://site2.example.com

This time, a different page should appear:

Site Two

This is the second website on the same VPS.

Even though the public IP address and external ports 80 and 443 are the same, Nginx Proxy Manager identifies the correct container by the domain name.

You can add additional websites and applications to a single VPS in the same way: for each new service, simply connect the container to the shared proxy network, create a DNS record, and configure a separate Proxy Host.

Verifying HTTPS and Internal Port Isolation

First, try opening the first site over HTTP: http://site1.example.com

When Force SSL is enabled, the browser should automatically navigate to: https://site1.example.com

Run the same check for the second site.

You can also verify that the application containers are not publishing their ports on the VPS: docker compose ps

For site1 and site2, the ports column should not contain bindings such as:

0.0.0.0:8081->80/tcp

0.0.0.0:8082->80/tcp

Nginx Proxy Manager, however, should have ports 80, 81, and 443 published.

For an additional check, run: docker ps –format “table {{.Names}}\t{{.Ports}}”

The expected output should follow this pattern:

npm0.0.0.0:80->80/tcp, 0.0.0.0:81->81/tcp, 0.0.0.0:443->443/tcp
site180/tcp
site280/tcp
npm-db3306/tcp

Entries such as 80/tcp and 3306/tcp without a binding in the form 0.0.0.0:port->port mean that the corresponding ports exist inside the container network but are not published directly on the VPS network interfaces.

As a result, a single VPS serves multiple websites through a single reverse proxy. Nginx Proxy Manager manages domains, HTTPS, and access rules centrally, while the internal services remain isolated within the Docker network.

Conclusion

Nginx Proxy Manager lets you host multiple websites and web applications on a single VPS without manually creating separate Nginx configurations for each project. The reverse proxy accepts external traffic on ports 80 and 443, identifies the appropriate Proxy Host by domain name, and forwards the request to the corresponding container.

In the configuration described here, two demo sites run independently of each other while sharing a common Docker network and a single Nginx Proxy Manager instance. The applications’ internal ports are not published on the VPS interface, so they cannot be accessed directly from the internet. For each site, you can manage the domain, SSL certificate, HTTPS redirection, and access rules separately.

This setup can be scaled further: when adding a new project, you only need to attach its container to the shared proxy network, create a DNS record, and add a new Proxy Host. However, it is important to control access to the Nginx Proxy Manager administrative interface and avoid publishing internal ports unless necessary.

FAQ

Can I host more than two websites on a single VPS?

Yes. The two websites in the guide are used for demonstration purposes only. A single Nginx Proxy Manager instance can handle many Proxy Hosts, provided the VPS has enough resources to run the reverse proxy itself and the hosted applications.

Does each website need a separate public IP address?

No. Multiple domain names can point to the same public IP address. Nginx Proxy Manager identifies the appropriate Proxy Host by the domain name and routes the request to the corresponding container.

Do Docker container ports need to be exposed to the internet?

Usually not if the application is behind a reverse proxy. It is sufficient to attach the container to a shared user-defined Docker network with Nginx Proxy Manager. Docker allows containers on that network to reach each other by name, so the application can remain inaccessible directly from the outside.

What is port 81 used for?

By default, Nginx Proxy Manager uses port 81 for the administrative web interface. HTTP traffic itself is served over port 80, while HTTPS uses port 443.

Can the Nginx Proxy Manager administrative interface be kept off the internet?

Yes, and this is recommended for production environments. Access to port 81 can be restricted with a firewall, allowed only for trusted IP addresses, or provided through a VPN.

What happens after Force SSL is enabled?

HTTP requests to the corresponding Proxy Host will be redirected to HTTPS. TLS is terminated at Nginx Proxy Manager, while the request may be forwarded to the internal container over HTTP within the Docker network.

Why Are Access Lists Needed?

Access Lists allow you to further restrict access to a Proxy Host, for example by using HTTP Basic Authentication or IP address rules. This is useful for administrative panels, test environments, and other services that should not be fully public. Nginx Proxy Manager supports Access Lists and basic HTTP authentication.

Is Nginx Proxy Manager required when publishing multiple websites?

No, it is not required. Essentially, it is an admin panel with a user-friendly interface for Nginx, which acts as a reverse proxy. Using bare Nginx with text-based configuration files instead is more secure, but it requires a deeper understanding of how it works.

Why might Let’s Encrypt fail to issue a certificate?

One common reason is that the domain does not yet point to the correct server, or the validation process cannot reach it. With HTTP-01 validation, Let’s Encrypt accesses a special resource on the domain over HTTP, so the domain must resolve correctly and the server must be reachable for validation on port 80.

Sources

  1. Nginx Proxy Manager — Full Setup Instructions
  2. Nginx Proxy Manager — Guide
  3. Docker Docs — Bridge network driver
  4. Let’s Encrypt — Challenge Types

Subscribe to our newsletter and receive articles and news

    Check out our other materials