-
-
Save Zahorone/984e75c6a3e8c4a1706f81b6c5e55071 to your computer and use it in GitHub Desktop.
| version: '3' | |
| networks: | |
| rustdesk-net: | |
| external: false | |
| services: | |
| nginx-proxy-manager: | |
| image: 'jc21/nginx-proxy-manager:latest' | |
| restart: unless-stopped | |
| ports: | |
| # These ports are in format <host-port>:<container-port> | |
| - '80:80' # Public HTTP Port | |
| - '443:443' # Public HTTPS Port | |
| - '127.0.0.1:8081:81' # Admin Web Port. NPM bypass UFW. 127.0.0.1 is easy setting to fix it from localhost only. | |
| # Add any other Stream port you want to expose | |
| # - '21:21' # FTP | |
| # Ports needed for Rustdesk: | |
| - '21115:21115' | |
| - '21116:21116' | |
| - '21116:21116/udp' | |
| - '21117:21117' | |
| - '21118:21118' | |
| - '21119:21119' | |
| # Uncomment the next line if you uncomment anything in the section | |
| # 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 | |
| - ./letsencrypt:/etc/letsencrypt | |
| networks: | |
| - rustdesk-net | |
| hbbs: | |
| container_name: hbbs | |
| image: rustdesk/rustdesk-server:latest | |
| command: hbbs -r rustdesk.yourDomain.com:21117 | |
| volumes: | |
| - ./data:/root | |
| networks: | |
| - rustdesk-net | |
| depends_on: | |
| - hbbr | |
| restart: unless-stopped | |
| hbbr: | |
| container_name: hbbr | |
| image: rustdesk/rustdesk-server:latest | |
| command: hbbr | |
| volumes: | |
| - ./data:/root | |
| networks: | |
| - rustdesk-net | |
| restart: unless-stopped |
If you want to change NFT (IP) tables instead of changing line 16 - '127.0.0.1:8081:81' in docker-compose.yml
Here is how to do it.
Problem: DOCKER-USER Chain Rules Disappear After Reboot
After server restart, the DOCKER-USER chain is empty and custom firewall rules (e.g., blocking port 8081) disappear.
Root Cause
Docker completely resets its firewall on every startup. The daemon clears all custom rules and recreates the necessary chains (DOCKER, DOCKER-ISOLATION, DOCKER-USER). Since /etc/nftables.conf loads before the Docker service starts, Docker subsequently overwrites any custom modifications in DOCKER-USER.
Solution: Systemd Service with Automatic Rule Injection
Step 1: Create Firewall Rules Script
sudo nano /usr/local/bin/docker-firewall-rules.sh
Script content:
#!/bin/bash
#Add custom rules to DOCKER-USER after Docker starts
# Block port 8081 for IPv4
/usr/sbin/nft add rule ip filter DOCKER-USER tcp dport 8081 drop
# Block port 8081 for IPv6
/usr/sbin/nft add rule ip6 filter DOCKER-USER tcp dport 8081 drop
This version includes all the status messages so it’s easy to see which rules were removed,
left unchanged, newly added, or already present in your firewall.
It's fully idempotent and always logs every action
#!/bin/bash
PORTS=(82 8081 8888) # Add more ports to this list as needed
# First, get all ports currently present in chains (IPv4/IPv6)
# IPv4
for RULE_PORT in $(sudo nft list chain ip filter DOCKER-USER | grep -oP 'tcp dport \K[0-9]+'); do
if [[ ! " ${PORTS[@]} " =~ " ${RULE_PORT} " ]]; then
# Remove old rule
INDEX=$(sudo nft list chain ip filter DOCKER-USER | grep -n "tcp dport $RULE_PORT drop" | awk -F: '{print $1}')
sudo nft delete rule ip filter DOCKER-USER "$INDEX"
echo "Removed rule for IPv4 port $RULE_PORT"
else
echo "Rule for IPv4 port $RULE_PORT left unchanged (present in PORTS)"
fi
done
# IPv6
for RULE_PORT in $(sudo nft list chain ip6 filter DOCKER-USER | grep -oP 'tcp dport \K[0-9]+'); do
if [[ ! " ${PORTS[@]} " =~ " ${RULE_PORT} " ]]; then
INDEX=$(sudo nft list chain ip6 filter DOCKER-USER | grep -n "tcp dport $RULE_PORT drop" | awk -F: '{print $1}')
sudo nft delete rule ip6 filter DOCKER-USER "$INDEX"
echo "Removed rule for IPv6 port $RULE_PORT"
else
echo "Rule for IPv6 port $RULE_PORT left unchanged (present in PORTS)"
fi
done
# Then, add ports idempotently
for PORT in "${PORTS[@]}"; do
# IPv4
if ! sudo nft list chain ip filter DOCKER-USER | grep -q "tcp dport $PORT drop"; then
sudo nft add rule ip filter DOCKER-USER tcp dport $PORT drop
echo "Added: drop for IPv4 port $PORT"
else
echo "Rule already exists for IPv4 port $PORT"
fi
# IPv6
if ! sudo nft list chain ip6 filter DOCKER-USER | grep -q "tcp dport $PORT drop"; then
sudo nft add rule ip6 filter DOCKER-USER tcp dport $PORT drop
echo "Added: drop for IPv6 port $PORT"
else
echo "Rule already exists for IPv6 port $PORT"
fi
done
Make it executable:
sudo chmod +x /usr/local/bin/docker-firewall-rules.sh
Step 2: Create Systemd Service Unit
sudo nano /etc/systemd/system/docker-firewall.service
Service file content:
[Unit]
Description=Docker Firewall Rules
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/docker-firewall-rules.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
This service runs automatically after the Docker daemon starts, ensuring rules are added only after Docker creates its chains.
Step 3: Enable the Service
sudo systemctl daemon-reload
sudo systemctl enable docker-firewall.service
sudo systemctl start docker-firewall.service
Step 4: Verify Functionality
Check if the rule was added:
sudo nft list ruleset | grep -A 5 'chain DOCKER-USER'
Expected output:
chain DOCKER-USER {
tcp dport 8081 drop
}
}
table ip6 filter {
chain ufw6-before-logging-input {
--
chain DOCKER-USER {
tcp dport 8081 drop
}
}
Testing Persistence After Reboot
Reboot the server:
sudo reboot
After reboot, verify the rule persists:
sudo nft list ruleset | grep -A 5 'chain DOCKER-USER'
Troubleshooting
If rules still disappear, check service status:
sudo systemctl status docker-firewall.service
sudo journalctl -u docker-firewall.service -n 20
The service should show Active: active (exited) with status=0/SUCCESS.
Best option is to have new installed server. In this example I will use Ubuntu 24.04.3 LTS.
There are 3 main steps.
a. Setting Docker
b. Setting Firewall
c. Setting Nginx Proxy Manager
A. .......... Setting Docker
Install Docker Engine https://docs.docker.com/engine/install/ubuntu/
Verify that Docker is running
sudo systemctl status dockerCreate directory
sudo mkdir /opt/rustnpmOpen Directory
cd /opt/rustnpmCreate file docker-compose.yml
sudo nano docker-compose.ymlCopy rustdesk.yml code to new file
DO NOT FORGET to change line 44
command: hbbs -r rustdesk.yourDomain.com:21117change yourDomain.whatever:21117 there and save it.Run docker compose
sudo docker compose up -ddetached)Find Docker containers IP addresses
sudo docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(sudo docker ps -aq)/opt/rustnpm/dataFind and open file id_ed25519.pub
nano id_ed25519.pubit will be something like this: HypOPa2nh6njLd1gfQOn8T+CGT7lxlg5t7pkjS1xI9s=This is your ID/Relay server Key
We are done with first part.
B. .......... Setting Firewall. UFW
rustdesk-net is a internal network so all open ports goes to NPM. Personally I deny external access to NPM admin port.
I allow it through SSH tunnelling only.
localhost:8081So allow just necessary ports and deny port 8081:
sudo ufw allow 80sudo ufw allow 443sudo ufw allow 21115:21119/tcpsudo ufw allow 21116/udpsudo ufw deny 8081then reload UFW
sudo ufw reloadWe are done B part
C. .......... Setting Nginx Proxy Manager
I use SSH tunnelling and PublicKey instead of password so in terminal I use .ssh/config file to make access easier a set port forwarding

Host merlin
HostName server IP or domain name
Port 2223 (your OpenSSH port)
User merlin (your user name)
IdentityFile ~/.ssh/merlin.pem (your key file)
LocalForward 9090 127.0.0.1:9090 (not necessary)
LocalForward 8081 127.0.0.1:8081 (NPM will be on localhost:8081 in your browser)
ssh merlinport forwarding is set and just typelocalhost:8081in your browserdefault credentials:
email: admin@example.com
password: changeme
Then edit Administrator and change password
Go to 404 Hosts and Add your domain to 404 Host and request SSL certificate, select Force SSL, and Agree with terms
We looked for Docker containers hbbs and hbbr IP and necessary ports
IP adresses check with
sudo docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(sudo docker ps -aq)In this case:
/hbbs - 172.18.0.4 Ports 21115/tcp, 21116/tcp and udp, 21118/tcp
/hbbr - 172.18.0.3 Ports 21117/tcp, 21119/tcp
Now we set streams go to Streams and set like this
There is a catch. After restart docker there is a change of internal IP. So the most safe way is add streams like this

And WE ARE DONE
Now set Rustdesk application (You can skip API server in free version)
