Setting Up a Multi-Service Server with Security-First Approach
Introduction
This article will guide you through deploying multiple services using containers and setting up reverse proxies with Nginx Proxy Manager (NPM) to expose these services via different subdomains. We’ll use Uptime Kuma (server monitoring tool) and Heimdall (application dashboard) as examples to demonstrate the complete setup process.
This deployment emphasizes security as the top priority. The following security-focused features are key characteristics of this guide:
Rootless Containers: All containers run in rootless mode to minimize the security attack surface by avoiding root privilegesHTTPS Protection: All services are protected by HTTPS with automatic SSL certificate management through Let’s EncryptMinimal Attack Surface: Firewall configuration ensures only necessary ports (80 and 443) are exposed, with all service ports kept closedSecurity-First Architecture: The entire setup is designed with security as the primary consideration, prioritizing protection over convenience
This guide uses CentOS Stream as an example. The commands and steps are largely compatible with other Red Hat-based distributions such as AlmaLinux, Rocky Linux, and Fedora.
For Ubuntu or Debian-based systems, or if you prefer to use Docker instead of Podman, most steps are similar, but some commands (package manager usage, service names, etc.) will vary. Where applicable, substitute with
dnf, and replace
apt with
podman.
docker
The core concepts of networking, reverse proxy configuration, and SSL certificate setup remain the same across platforms and container runtimes.
Environment Preparation
Before starting, ensure your system meets the following requirements:
Red Hat Enterprise Linux (RHEL) 8 or higherUser with sudo privilegesSystem connected to the internetAt least one domain name (for configuring subdomains)
Note: This guide is based on deployment on a Cloud Instance. The steps and configurations are applicable to cloud instances from various providers, as well as physical servers and local virtual machines.
System Update
First, update the system packages:
sudo dnf update -y
Installing Podman
Podman is a daemonless container engine that is compatible with Docker but lighter and more secure.
Installing Podman
sudo dnf install -y podman
Note:
If your Podman version is below 4.0 (check with), it may not have built-in
podman --versionfunctionality. You can install the
podman composepackage separately for Docker Compose-like functionality.
podman-compose
Verifying Installation
podman --version
Deploying Nginx Proxy Manager
Nginx Proxy Manager (NPM) is a web-based reverse proxy management tool that provides a user-friendly graphical interface for managing Nginx configurations.
Official Reference:
For more details and advanced configuration, please see the Nginx Proxy Manager official documentation.
Preparing Data Directories
Before starting, create the necessary directories for persistent data and SSL certificates:
mkdir -p ~/applications/nginx-proxy-manager/data
mkdir -p ~/applications/nginx-proxy-manager/letsencrypt
Writing the Podman Compose File
Below is an example file for Nginx Proxy Manager, located at
container-compose.yml:
~/applications/nginx-proxy-manager/container-compose.yml
services:
app:
image: 'docker.io/jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
# These ports are in format <host-port>:<container-port>
- '0.0.0.0:80:80' # Public HTTP Port
- '0.0.0.0:443:443' # Public HTTPS Port
- '0.0.0.0:81:81' # Admin Web Port
# Add any other Stream port you want to expose
# - '21:21' # FTP
# environment:
# Uncomment this if you want to change the location of
# the SQLite DB file within the container
# DB_SQLITE_FILE: "/data/database.sqlite"
# Uncomment this if IPv6 is not enabled on your host
# DISABLE_IPV6: 'true'
volumes:
- ./data:/data:Z
- ./letsencrypt:/etc/letsencrypt:Z
Note on
:
0.0.0.0:
Specifyingin the port mapping ensures that Nginx Proxy Manager (NPM) is accessible from all network interfaces. This is crucial when you want to serve the application to the internet.
0.0.0.0:About
in volume mounting:
:Z
Thesuffix is specific to Podman and sets the proper SELinux context on mounted volumes. It is required by Podman to ensure that containers have appropriate permissions to access the host directories. Without
:Z, you may encounter permission denied errors due to SELinux policies, so it is recommended to always include it when binding host directories into containers under Podman.
:Z
Starting the Nginx Proxy Manager Container with Podman Compose
Change to the application directory and start Nginx Proxy Manager using Podman Compose:
cd ~/applications/nginx-proxy-manager
podman compose up -d
Accessing the NPM Management Interface
Open your browser and navigate to:
http://your-server-ip:81
Default login credentials:
Email: Password:
admin@example.com
changeme
Important: Change the default password immediately after first login!
Deploying Uptime Kuma
Uptime Kuma is an open-source monitoring tool for tracking the availability of websites and services.
Official Reference:
Uptime Kuma Docker Compose Setup (Official)
Preparing Data Directory
Create a directory for persistent Uptime Kuma data:
mkdir -p ~/applications/uptime-kuma/data
Writing the Podman Compose File
Below is an example file for Nginx Proxy Manager, located at
container-compose.yml:
~/applications/uptime-kuma/container-compose.yml
services:
uptime-kuma:
image: louislam/uptime-kuma:2
restart: unless-stopped
volumes:
- ./data:/app/data:Z
ports:
# <Host IP>:<Host Port>:<Container Port>
- "0.0.0.0:13001:3001"
Starting Uptime Kuma with Podman Compose
From the directory containing your , run:
container-compose.yml
podman compose up -d
Accessing Uptime Kuma
You can now access Uptime Kuma at .
http://your-server-ip:13001
The first visit will take you to the initialization wizard.
Deploying Heimdall
Heimdall is a beautiful application dashboard that can consolidate quick access links to all commonly used services.
Official Reference:
Heimdall Docker Compose Setup (Official)
Preparing Data Directory
Create a directory for persistent Heimdall data:
mkdir -p ~/applications/heimdall/heimdall_data/
Writing the Podman Compose File
Below is an example file for Heimdall, located at
container-compose.yml:
~/applications/heimdall/container-compose.yml
services:
heimdall:
image: lscr.io/linuxserver/heimdall:latest
container_name: heimdall
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- ALLOW_INTERNAL_REQUESTS=false # optional
volumes:
- ./heimdall_data/config:/config:Z
ports:
# <Host IP>:<Host Port>:<Container Port>
- "0.0.0.0:10080:80"
- "0.0.0.0:10443:443"
restart: unless-stopped
Starting Heimdall with Podman Compose
From the directory containing your , run:
container-compose.yml
podman compose up -d
This will start Heimdall in the background.
Accessing Heimdall
You can now access Heimdall at .
http://your-server-ip:10080
On your first visit, you will be guided through the initialization wizard.
Configuring Subdomain Reverse Proxies
Now we need to bind different services to different subdomains through NPM, eliminating the need to remember port numbers.
Prerequisites
Ensure your domain DNS is configured:
Point A record to your server IPOr create individual A records for each subdomain:
*.yourdomain.com
→ Server IP
npm.yourdomain.com → Server IP
uptime.yourdomain.com → Server IP
heimdall.yourdomain.com
Configuring Reverse Proxy in NPM
1. Configuring Uptime Kuma Subdomain
Log in to the NPM management interface ()Click “Proxy Hosts” → “Add Proxy Host”Fill in the following information:
http://your-server-ip:81
Details:
Domain Names: Scheme:
uptime.yourdomain.comForward Hostname / IP:
http (use your server’s local network IP address, e.g.,
your-server-lan-ip or
192.168.1.100)Forward Port:
10.0.0.50 (matches the port configured in Uptime Kuma’s container-compose.yml)Block Common Exploits: CheckWebsockets Support: Check (required for Uptime Kuma)
13001
SSL:
Check “Request a new SSL Certificate”Check “Force SSL”Check “HTTP/2 Support”Check “Accept Terms of Service”
Advanced Tab (Important for Uptime Kuma):
Click on the “Advanced” tabIn the “Custom Nginx Configuration” section, add the following configuration:
# Custom headers for Uptime Kuma
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
Why these headers are needed:
: Tells Uptime Kuma that the original request was HTTPS, ensuring proper protocol detection
X-Forwarded-Proto: Preserves the original client IP address through the proxy chain
X-Forwarded-For: Provides the real client IP address
X-Real-IP: Ensures the correct host header is passed to the backend
Host and
X-Forwarded-Host: Additional information for proper URL generation
X-Forwarded-Port
Click “Save”
2. Configuring Heimdall Subdomain
Repeat the above steps to configure Heimdall:
Details:
Domain Names: Scheme:
heimdall.yourdomain.comForward Hostname / IP:
http (use your server’s local network IP address, same as above)Forward Port:
your-server-lan-ip (matches the port configured in Heimdall’s container-compose.yml)Block Common Exploits: CheckWebsockets Support: Check (optional, not required for Heimdall)
10080
SSL:
Check “Request a new SSL Certificate”Check “Force SSL”Check “HTTP/2 Support”Check “Accept Terms of Service”
Advanced Tab (Optional for Heimdall):
Similar header configuration can be added if needed, though Heimdall typically works fine with default NPM headers
3. Configuring NPM Management Interface Subdomain (Optional)
You can also configure a subdomain for the NPM management interface:
Details:
Domain Names: Scheme:
npm.yourdomain.comForward Hostname / IP:
httpForward Port:
localhostBlock Common Exploits: CheckWebsockets Support: Check (optional, not required for NPM)
81
SSL:
Check “Request a new SSL Certificate”Check “Force SSL”Check “HTTP/2 Support”Check “Accept Terms of Service”
Note: The NPM management interface should ideally be accessed only from internal networks, or set strong passwords and firewall rules.
Configuring Uptime Kuma Reverse Proxy Settings
After setting up the reverse proxy in NPM, you need to configure Uptime Kuma to trust the proxy headers. This ensures Uptime Kuma correctly detects HTTPS connections and client IP addresses when accessed through the reverse proxy.
Access Uptime Kuma Settings:
Navigate to (or
https://uptime.yourdomain.com if not using HTTPS yet)Go to Settings → Reverse Proxy
http://your-server-ip:13001
Configure Trust Proxy:
In the “HTTP Headers” section, you’ll find the “Trust Proxy” optionSet “Trust Proxy” to “Yes”This tells Uptime Kuma to trust the headers sent by NPM
X-Forwarded-*
Why this is important:
When “Trust Proxy” is enabled, Uptime Kuma will:
Use to detect HTTPS connectionsUse
X-Forwarded-Proto or
X-Forwarded-For to get the real client IP addressUse
X-Real-IP to get the correct host information Without this setting, Uptime Kuma might show incorrect URLs (with IP and port) and log wrong client IPs
X-Forwarded-Host
Save Settings:
Click “Save” to apply the changes
Note: The “Trust Proxy” setting works together with the headers we configured in NPM’s Advanced tab:
NPM’s Advanced tab: Configures which headers to send to Uptime Kuma (
directives)Uptime Kuma’s Trust Proxy: Tells Uptime Kuma to trust and use those headers
proxy_set_headerBoth configurations are necessary for Uptime Kuma to work correctly behind the reverse proxy.
Verifying Configuration
After configuration, you should be able to access each service via:
– Uptime Kuma
https://uptime.yourdomain.com – Heimdall
https://heimdall.yourdomain.com – NPM management interface (if configured)
https://npm.yourdomain.com
All connections should automatically use HTTPS, with SSL certificates automatically requested and renewed by Let’s Encrypt.
Firewall Configuration
For enhanced security, you should enable your server’s firewall. This protects your server by only allowing necessary network traffic.
Firewall Strategy
Since all your services are proxied through NPM, the firewall configuration follows this principle:
Open ports 80 and 443: Required for NPM to receive HTTP and HTTPS trafficKeep service ports closed: Ports 81 (NPM management), 13001 (Uptime Kuma), and 10080 (Heimdall) should remain closedAccess via HTTPS: All services should be accessed through the reverse proxy using HTTPS subdomains
This approach ensures that:
All services are protected by HTTPS and SSL certificatesThe attack surface is minimized (fewer exposed ports)Access is centralized through NPMBetter security and centralized access control
Configuring Firewall Rules
Open ports 80 (HTTP) and 443 (HTTPS) to allow NPM to receive traffic:
# Open HTTP and HTTPS ports
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --reload
Verifying Firewall Configuration
Verify that only the required ports are open and service ports are not exposed:
# Check which ports are currently open
sudo firewall-cmd --list-ports
Expected output: Only and
80/tcp should be listed.
443/tcp
You should NOT see:
(NPM management interface – should be accessed via
81/tcp if reverse proxy is configured)
https://npm.yourdomain.com (Uptime Kuma – should be accessed via
13001/tcp)
https://uptime.yourdomain.com (Heimdall – should be accessed via
10080/tcp)
https://heimdall.yourdomain.com
If you find any of these service ports in the allowed ports list, remove them for better security:
# Remove service ports if they are exposed (only if they appear in the list)
sudo firewall-cmd --permanent --remove-port=81/tcp
sudo firewall-cmd --permanent --remove-port=13001/tcp
sudo firewall-cmd --permanent --remove-port=10080/tcp
sudo firewall-cmd --reload
Accessing NPM Management Interface
If you have configured a reverse proxy for the NPM management interface, access it via . Port 81 should remain closed in the firewall.
https://npm.yourdomain.com
If the NPM reverse proxy is not working, or if you haven’t configured a reverse proxy for the management interface, you can temporarily open port 81 to access the NPM management interface directly:
# Temporarily open port 81 for direct access to NPM management interface
sudo firewall-cmd --add-port=81/tcp
sudo firewall-cmd --reload
After accessing the management interface at , remember to close port 81 for security:
http://your-server-ip:81
# Close port 81 after use
sudo firewall-cmd --remove-port=81/tcp
sudo firewall-cmd --reload
Security Best Practice: It is strongly recommended to configure a reverse proxy for the NPM management interface and access it via HTTPS (
) rather than exposing port 81 directly. This provides encryption and better security.
https://npm.yourdomain.com
Troubleshooting
If you temporarily cannot access services, you can temporarily disable the firewall for debugging purpose (not recommended for production):
sudo systemctl stop firewalld # Temporarily disable for debugging
# Remember to re-enable it after debugging:
sudo systemctl start firewalld
Appendix: Understanding Container Networking
Note: The following section provides in-depth knowledge about container networking and explains the technical reasoning behind the configuration choices made in this tutorial. This is supplementary information for readers who want to understand the “why” behind the configuration steps.
Forward Hostname / IP Configuration in Reverse Proxies
When configuring reverse proxies in NPM, you need to specify the “Forward Hostname / IP” address for your backend services. This section explains why using your server’s local network IP address is the recommended approach, and why other options may not work in containerized environments.
Why Use the Local Network IP Address?
When configuring reverse proxies in NPM, you should use your server’s local network IP address (e.g., or
192.168.1.100) for the “Forward Hostname / IP” field. This allows NPM to access services mapped to the host ports reliably, especially in rootless Podman environments.
10.0.0.50
Finding Your Local Network IP:
# Find your server's local network IP address
ip addr show | grep "inet " | grep -v "127.0.0.1"
# Or
hostname -I
Why Not
localhost?
localhost
In NPM, refers to the NPM container’s loopback interface, not the container host’s loopback interface. Since the services (Uptime Kuma, Heimdall) are running on the host with port mappings, NPM cannot reach them via
localhost from within the container.
localhost
Key Point: Containers have isolated network namespaces. When you use inside a container, it refers to the container’s own network interface, not the host’s interface.
localhost
Why Not
host.containers.internal?
host.containers.internal
While may work from within the container when testing manually, Nginx (OpenResty) – which powers NPM – runs in a different namespace context. This can cause issues for several reasons:
curl http://host.containers.internal:13001
Namespace Context Differences: The socket used by (user-space socket) is not available to the Nginx process running in the container’s namespaceRootless Podman Limitations: In rootless Podman environments with
curl, containers may not reliably access the host IP via DNS aliases like
slirp4netnsNetwork Resolution: DNS-based resolution may not work consistently across different container runtime contexts
host.containers.internal
Key Point: Just because a command works in one context (like from a shell) doesn’t mean it will work for all processes (like Nginx) running in different namespace contexts.
curl
Why Not the Public Server IP?
While technically possible, using the public server IP address is not recommended for security reasons:
Security Concerns: We don’t want to expose backend service ports directly to the internetHTTPS Termination: HTTPS termination happens at NPM, while backend services expose unencrypted HTTPAttack Surface: Using the public IP would expose these unencrypted services to the internet, significantly reducing securityNetwork Isolation: The firewall configuration blocks external access to service ports, which is a security best practice
Key Point: Even though the firewall blocks external access, using the public IP can create unnecessary network paths and potential security risks.
Rootless Podman and Network Isolation
In rootless Podman mode, the networking configuration becomes more complex:
: Podman uses
slirp4netns (userspace network virtualization) in rootless modeDNS Resolution: Container network DNS resolution (
slirp4netns) is not availableDynamic IPs: Container IPs are dynamically assigned by
dnsname and may change on restartStable Port Mappings: However, port mappings (hostPort → containerPort) remain stable and reliableSecurity Priority: This configuration prioritizes security by not using root-mode containers and networks
slirp4netns
The complexity of this configuration is due to rootless container limitations, but this deployment prioritizes security over convenience. By using rootless containers, we avoid running containers with root privileges, which significantly reduces the security attack surface.
Why This Configuration Works
Using the host’s local network IP address in the NPM reverse proxy configuration works because:
Direct Host Access: NPM can directly access services mapped to host ports via the host’s network interfaceReliability: This is the most reliable method, especially in rootless Podman environmentsSecurity: Combined with firewall configuration (which blocks external access to service ports), this ensures security while maintaining functionalityConsistency: The host’s local network IP remains stable across container restarts
Future Considerations
As Podman continues to evolve, newer versions may provide more convenient and secure solutions for container-to-host networking. However, as of Podman 4.x, using the host’s local network IP address remains one of the most reliable and secure implementation methods for this type of deployment.
Key Takeaway: Understanding container networking isolation is crucial when configuring reverse proxies. The local network IP address provides a reliable bridge between containerized services and host-mapped ports while maintaining security boundaries.



