Skip to content

Deployment Guide

DenAV edited this page Mar 15, 2026 · 4 revisions

Deployment Guide

Step-by-step instructions for deploying Nginx Proxy Manager to cloud providers using Docker and the Ansible role from this project.

Table of contents

Overview and Comparison

Feature Hetzner Cloud (CX22) Azure Container Instances
Type VM + Docker CE Serverless container
vCPU / RAM 2 vCPU / 4 GB 1 vCPU / 2 GB
Storage 40 GB SSD (+ Volumes) Azure File Share
Traffic 20 TB included Pay per GB
Est. cost from EUR 3.49/month ~USD 35/month
Complexity Medium (full server) Low (managed service)
Best for Production, full control Quick PoC, Azure-native

Option 1: Hetzner Cloud (CX22 + Docker CE)

Prerequisites

Step 1: Create Server

# Upload SSH key
hcloud ssh-key create --name my-key --public-key-from-file ~/.ssh/id_ed25519.pub

# Create server with Docker CE app image
hcloud server create \
  --name npm-server \
  --type cx22 \
  --image docker-ce \
  --ssh-key my-key \
  --location nbg1

Note the server IP from output:

hcloud server ip npm-server

Step 2: Configure Firewall

# Create firewall
hcloud firewall create --name npm-fw

# Allow SSH
hcloud firewall add-rule npm-fw --direction in --protocol tcp --port 22 \
  --source-ips 0.0.0.0/0 --source-ips ::/0

# Allow HTTP
hcloud firewall add-rule npm-fw --direction in --protocol tcp --port 80 \
  --source-ips 0.0.0.0/0 --source-ips ::/0

# Allow HTTPS
hcloud firewall add-rule npm-fw --direction in --protocol tcp --port 443 \
  --source-ips 0.0.0.0/0 --source-ips ::/0

# Allow NPM Admin UI
hcloud firewall add-rule npm-fw --direction in --protocol tcp --port 81 \
  --source-ips 0.0.0.0/0 --source-ips ::/0

# Apply to server
hcloud firewall apply-to-resource npm-fw --type server --server npm-server

Security note: Restrict port 81 (admin UI) to your IP only in production: --source-ips <your-ip>/32

Step 3: Deploy NPM

SSH into the server and deploy using the project's docker-compose file:

ssh root@<server-ip>

# Clone the repository
git clone https://github.com/DenAV/nginx-proxy-manager-ansible.git /opt/npm
cd /opt/npm/docker

# (Optional) Review and adjust .env settings
cp .env.example .env
nano .env

# Start NPM
docker compose -f docker-compose_npm.yml up -d

Default admin credentials: admin@example.com / changeme — change immediately after first login at http://<server-ip>:81.

Step 4: DNS and SSL

  1. Point your domain to the server IP:

    A   npm.example.com   →   <server-ip>
    
  2. Log in to NPM admin (http://<server-ip>:81)

  3. Add a proxy host for the admin UI itself:

    • Domain: npm.example.com
    • Forward: localhost:81
    • Enable SSL with Let's Encrypt
  4. After SSL is active, block external access to port 81:

    hcloud firewall delete-rule npm-fw --direction in --protocol tcp --port 81 \
      --source-ips 0.0.0.0/0 --source-ips ::/0

Step 5: Backups and Monitoring

Persistent storage with Hetzner Volumes:

# Create and attach a volume
hcloud volume create --name npm-data --size 10 --server npm-server

# Mount (on server)
mkfs.ext4 /dev/disk/by-id/scsi-0HC_Volume_<id>
mkdir -p /mnt/npm-data
mount /dev/disk/by-id/scsi-0HC_Volume_<id> /mnt/npm-data

# Add to /etc/fstab for persistence
echo "/dev/disk/by-id/scsi-0HC_Volume_<id> /mnt/npm-data ext4 defaults 0 2" >> /etc/fstab

Snapshots:

# Create server snapshot (full backup)
hcloud server create-image npm-server --type snapshot --description "npm-backup-$(date +%F)"

Security Hardening

# Disable root password login (key-only)
sed -i 's/^PermitRootLogin yes/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd

# Install fail2ban
apt-get update && apt-get install -y fail2ban
systemctl enable --now fail2ban

# Configure UFW
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable

Option 2: Azure Container Instances (ACI)

Prerequisites

  • Azure subscription
  • az CLI installed (docs)
  • Logged in: az login

Step 1: Create Resource Group and Storage

# Variables
RG="rg-npm"
LOCATION="westeurope"
STORAGE="stnpm$(openssl rand -hex 4)"

# Create resource group
az group create --name $RG --location $LOCATION

# Create storage account
az storage account create \
  --name $STORAGE \
  --resource-group $RG \
  --location $LOCATION \
  --sku Standard_LRS

# Get storage key
STORAGE_KEY=$(az storage account keys list \
  --resource-group $RG \
  --account-name $STORAGE \
  --query "[0].value" -o tsv)

# Create file shares for persistent data
az storage share create --name npm-data --account-name $STORAGE --account-key $STORAGE_KEY
az storage share create --name npm-letsencrypt --account-name $STORAGE --account-key $STORAGE_KEY

Step 2: Deploy Container Group

az container create \
  --resource-group $RG \
  --name npm \
  --image jc21/nginx-proxy-manager:2.11.3 \
  --cpu 1 \
  --memory 2 \
  --ports 80 443 81 \
  --ip-address Public \
  --dns-name-label npm-$(openssl rand -hex 4) \
  --os-type Linux \
  --restart-policy Always \
  --azure-file-volume-account-name $STORAGE \
  --azure-file-volume-account-key $STORAGE_KEY \
  --azure-file-volume-share-name npm-data \
  --azure-file-volume-mount-path /data

Note: ACI supports one volume mount per az container create command. For multiple mounts, use a YAML deployment file.

YAML deployment (recommended):

The ACI deployment file is available in the repository at deploy/azure/npm-aci.yaml. Before deploying, replace the <storage-account> and <storage-key> placeholders with your values.

Deploy using YAML:

az container create --resource-group $RG --file npm-aci.yaml

Step 3: Configure DNS

Get the assigned FQDN:

az container show --resource-group $RG --name npm \
  --query "ipAddress.fqdn" -o tsv

Point your domain to the container IP or CNAME to the FQDN:

CNAME   npm.example.com   →   npm-instance.westeurope.azurecontainer.io

Step 4: Persistent Storage

Data is stored on Azure File Shares and survives container restarts and redeployments. To back up:

# Create a snapshot of the file share
az storage share snapshot \
  --name npm-data \
  --account-name $STORAGE \
  --account-key $STORAGE_KEY

Step 5: Monitoring and Logs

# View container logs
az container logs --resource-group $RG --name npm

# Attach to container output (live)
az container attach --resource-group $RG --name npm

# Enable Azure Monitor (Log Analytics)
WORKSPACE_ID=$(az monitor log-analytics workspace create \
  --resource-group $RG \
  --workspace-name npm-logs \
  --query "customerId" -o tsv)

WORKSPACE_KEY=$(az monitor log-analytics workspace get-shared-keys \
  --resource-group $RG \
  --workspace-name npm-logs \
  --query "primarySharedKey" -o tsv)

Add to the YAML deployment diagnostics section:

  diagnostics:
    logAnalytics:
      workspaceId: '<workspace-id>'
      workspaceKey: '<workspace-key>'

Security (Managed Identity, Key Vault)

Store the storage account key in Azure Key Vault instead of passing it directly:

# Create Key Vault
az keyvault create --name kv-npm --resource-group $RG --location $LOCATION

# Store secret
az keyvault secret set --vault-name kv-npm --name storage-key --value "$STORAGE_KEY"

# Enable managed identity on ACI (requires YAML deployment with identity block)

Note: ACI managed identity support requires the YAML deployment format. See Azure docs for details.


Ansible Integration

After NPM is running on either provider, use the Ansible role to manage proxy hosts.

Running the Role Against Cloud-Deployed NPM

ansible-playbook pl_npm-management.yml \
  -i inventory/cloud \
  -e "npm_api_url=http://npm.example.com:81/api" \
  -e "npm_api_domain_name=app.example.com" \
  -e "npm_api_host=172.16.1.10" \
  -e "npm_api_state=present"

Inventory Examples

Hetzner:

# inventory/hetzner
[npm]
npm.example.com ansible_host=<server-ip> ansible_user=root

[npm:vars]
npm_api_url=http://localhost:81/api
npm_api_ssl_forced=true
npm_api_letsencrypt_email=admin@example.com

Azure ACI:

# inventory/azure
[npm]
npm.example.com ansible_connection=local

[npm:vars]
npm_api_url=http://npm-instance.westeurope.azurecontainer.io:81/api
npm_api_ssl_forced=true
npm_api_letsencrypt_email=admin@example.com

Note: For ACI, use ansible_connection=local since there is no SSH access. The role connects to the NPM API over HTTP, not SSH.


Troubleshooting

Problem Solution
NPM admin UI not accessible Check firewall rules allow port 81; verify container is running with docker ps or az container show
Let's Encrypt fails Ensure ports 80/443 are open and DNS points to the correct IP; check NPM logs
Container keeps restarting Check logs: docker logs npm-app or az container logs --name npm; verify sufficient memory
Storage not persisting Verify volume mounts; for ACI check Azure File Share exists and credentials are correct
Ansible role cannot connect Verify npm_api_url is reachable from the Ansible control node; check API health endpoint

Clone this wiki locally