-
Notifications
You must be signed in to change notification settings - Fork 5
Deployment Guide
Step-by-step instructions for deploying Nginx Proxy Manager to cloud providers using Docker and the Ansible role from this project.
- Overview and Comparison
- Option 1: Hetzner Cloud (CX22 + Docker CE)
- Option 2: Azure Container Instances (ACI)
- Ansible Integration
- Troubleshooting
| 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 |
- Hetzner Cloud account
-
hcloudCLI installed (docs) - SSH key pair generated locally
- Domain name with DNS access
What runs where:
Component Runs on Purpose Docker + NPM Hetzner server Hosts the Nginx Proxy Manager container hcloudCLILocal machine Creates and manages the server Ansible, pip, pytest Local machine Manages NPM via API, development/testing Do not install Ansible, pip, or dev tools on the server — it only needs Docker.
# 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 nbg1Note the server IP from output:
hcloud server ip npm-serverWhen creating a server via the Hetzner Cloud Console, you can pass a cloud-config script to automate initial server hardening. This runs on first boot before you SSH in.
How to apply:
- Go to Hetzner Cloud Console → Servers → Create Server
- Select Image:
docker-ce(Apps tab) - Select Type: CX22
- Under Cloud config (bottom of the page), paste the YAML below
- Add your SSH key and click Create & Buy Now
If using the CLI, pass the cloud-config file with:
hcloud server create ... --user-data-from-file cloud-config.yml
Cloud-config for NPM server:
The cloud-config file is available in the repository at deploy/hetzner/cloud-config.yml. Before using, replace <your-public-ssh-key> with your actual public SSH key.
After cloud-config runs:
- SSH port is 2222 (not 22):
ssh deploy@<server-ip> -p 2222- Root login is disabled — use the
deployuser with sudo- UFW and fail2ban are active
- Restrict port 81 to your IP after initial NPM setup (see Security Hardening)
If you used cloud-config, skip the Security Hardening section — it is already applied. Also update the firewall CLI commands in Step 2 to use port 2222 instead of 22.
# 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-serverSecurity note: Restrict port 81 (admin UI) to your IP only in production:
--source-ips <your-ip>/32
SSH into the server and deploy using the project's docker-compose file:
ssh root@<server-ip>
# If Docker is not installed (plain Ubuntu image instead of docker-ce):
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
# 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 -dTip: If you selected the
docker-ceapp image in Step 1, Docker is already installed — skip the install commands above.
Default admin credentials: admin@example.com / changeme — change immediately after first login at http://<server-ip>:81.
-
Point your domain to the server IP:
A npm.example.com → <server-ip> -
Log in to NPM admin (
http://<server-ip>:81) -
Add a proxy host for the admin UI itself:
- Domain:
npm.example.com - Forward:
localhost:81 - Enable SSL with Let's Encrypt
- Domain:
-
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
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/fstabSnapshots:
# Create server snapshot (full backup)
hcloud server create-image npm-server --type snapshot --description "npm-backup-$(date +%F)"# 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- Azure subscription
-
azCLI installed (docs) - Logged in:
az login
# 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_KEYaz 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 /dataNote: ACI supports one volume mount per
az container createcommand. 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.yamlGet the assigned FQDN:
az container show --resource-group $RG --name npm \
--query "ipAddress.fqdn" -o tsvPoint your domain to the container IP or CNAME to the FQDN:
CNAME npm.example.com → npm-instance.westeurope.azurecontainer.io
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# 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>'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.
After NPM is running on either provider, use the Ansible role to manage proxy hosts.
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"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.comAzure 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.comNote: For ACI, use
ansible_connection=localsince there is no SSH access. The role connects to the NPM API over HTTP, not SSH.
| 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 |
| SSL creation times out | Let's Encrypt certbot can take 10-30s per domain. The module uses 120s timeout for SSL operations. Ensure DNS is correct and ports 80/443 are reachable |