-
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
# 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-server# 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:
ssh root@<server-ip>
# Create project directory
mkdir -p /opt/npm && cd /opt/npm
# Create docker-compose.yml
cat > docker-compose.yml << 'EOF'
services:
app:
image: 'jc21/nginx-proxy-manager:2.11.3'
container_name: npm-app
hostname: npm
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- npm-data:/data
- npm-letsencrypt:/etc/letsencrypt
restart: unless-stopped
healthcheck:
test: ["CMD", "/bin/check-health"]
interval: 10s
timeout: 3s
networks:
- npm
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
npm:
name: npm
driver: bridge
volumes:
npm-data:
npm-letsencrypt:
EOF
# Start NPM
docker compose up -dDefault 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):
# npm-aci.yaml
apiVersion: '2021-10-01'
name: npm
location: westeurope
properties:
containers:
- name: npm-app
properties:
image: 'jc21/nginx-proxy-manager:2.11.3'
resources:
requests:
cpu: 1.0
memoryInGb: 2.0
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
- protocol: TCP
port: 81
volumeMounts:
- name: npm-data
mountPath: /data
- name: npm-letsencrypt
mountPath: /etc/letsencrypt
osType: Linux
restartPolicy: Always
ipAddress:
type: Public
dnsNameLabel: npm-instance
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
- protocol: TCP
port: 81
volumes:
- name: npm-data
azureFile:
shareName: npm-data
storageAccountName: '<storage-account>'
storageAccountKey: '<storage-key>'
- name: npm-letsencrypt
azureFile:
shareName: npm-letsencrypt
storageAccountName: '<storage-account>'
storageAccountKey: '<storage-key>'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 |