How to Set Up and Secure a Dokploy Server on a VPS
With Tailscale, CrowdSec, Hetzner Firewall & System Hardening
This guide provides a complete, security-first setup for running Dokploy on your own VPS. It includes:
- Creating a secure user
- Zero-trust SSH via Tailscale
- Disabling root SSH
- Dokploy installation
- Domain and SSL configuration
- CrowdSec protection
- Strict iptables firewall
- Hetzner Cloud Firewall
- System log optimization
- Docker daemon safety check
1. Connect to Your VPS
ssh root@your_vps_ip2. Update System Packages
sudo apt update && sudo apt upgrade -y3. Create a Non-Root User
adduser sebastian
usermod -aG sudo sebastian4. Enable Passwordless sudo
echo "sebastian ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/sebastian
sudo chmod 0440 /etc/sudoers.d/sebastian5. Copy SSH Keys to the New User
mkdir -p /home/sebastian/.ssh
cp /root/.ssh/authorized_keys /home/sebastian/.ssh/
chown -R sebastian:sebastian /home/sebastian/.ssh
chmod 700 /home/sebastian/.ssh
chmod 600 /home/sebastian/.ssh/authorized_keys6. Test SSH Login and Remove Password
ssh sebastian@your_vps_ip
sudo whoamiRemove password:
sudo passwd -d sebastian7. Install Tailscale (Correct Method)
Visit:
https://login.tailscale.com/admin/machinesClick Add device, copy the generated install command, then run it:
curl -fsSL <your-tailscale-install-url> | sh
sudo tailscale up --sshThe --ssh flag enables Tailscale SSH, giving you a fallback even if OpenSSH breaks.
Tailscale will assign a private IP:
100.x.x.xConnect via:
ssh [email protected]Does Tailscale require open ports?
No inbound ports required.
Outbound UDP 3478 and 41641 are used automatically and are allowed by default.
8. Block Public SSH in Hetzner Firewall
In Hetzner Cloud → Firewall:
Allow:
- TCP 80
- TCP 443
- ICMP (optional but recommended)
Block:
- TCP 22 (SSH)
SSH stays open on the server internally, so you can re-enable port 22 in Hetzner temporarily if Tailscale fails.
9. Disable Root SSH Login
sudo nano /etc/ssh/sshd_configSet:
PermitRootLogin noRestart:
sudo systemctl restart ssh10. Optional: Idle SSH Timeout
sudo nano /etc/ssh/sshd_configAdd:
ClientAliveInterval 900
ClientAliveCountMax 0Restart:
sudo systemctl restart ssh11. Add Swap Space (2 GB)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab12. Install Dokploy
curl -sSL https://dokploy.com/install.sh | sudo shDokploy UI then becomes temporarily available at:
http://your_vps_ip:300013. Create DNS A Record
|
Type |
Name |
Value |
|---|---|---|
|
A |
app |
your_vps_ip |
14. Configure Server Domain in Dokploy
Open:
http://your_vps_ip:3000Navigate to:
Settings → Web Server → Server Domain
Enter:
app.yourdomain.comDokploy will automatically configure HTTPS with Let’s Encrypt.
After this, port 3000 no longer needs to be publicly accessible.
15. Install CrowdSec
curl -s https://install.crowdsec.net | sudo sh
sudo apt update && sudo apt install crowdsec16. Install the CrowdSec Firewall Bouncer
sudo apt install crowdsec-firewall-bouncer-iptables -y17. Configure iptables Firewall
Allow only:
- established traffic
- loopback
- SSH (local, upstream-blocked)
- HTTP
- HTTPS
- ICMP (recommended)
And enforce default DROP:
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -p icmp -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
sudo iptables -P INPUT DROP
sudo iptables -P OUTPUT ACCEPT18. Make Firewall Rules Persistent
sudo apt install iptables-persistent -yFor future updates:
sudo netfilter-persistent save
sudo netfilter-persistent reload19. Fix CrowdSec Boot-Order Issue
Ensure CrowdSec firewall bouncer loads after netfilter:
sudo systemctl edit crowdsec-firewall-bouncerAdd:
[Unit]
After=netfilter-persistent.serviceSave, then:
sudo systemctl daemon-reload
sudo systemctl restart crowdsec-firewall-bouncer20. Optimize journald (Prevents Log Bloat)
sudo nano /etc/systemd/journald.confSet:
SystemMaxUse=200M
Compress=yes
Storage=persistentApply:
sudo systemctl restart systemd-journald21. Check Docker Daemon Exposure (Important)
Ensure Docker is not exposing port 2375:
sudo ss -tulpen | grep dockerdIf you see 0.0.0.0:2375, lock it down immediately.
Default Docker does not expose it — but some tools accidentally enable it.
22. Verify CrowdSec Status
sudo systemctl status crowdsec
sudo cscli collections list
sudo cscli bouncers list23. Inspect CrowdSec Firewall Chains
Then inspect, for example:
sudo iptables -L CROWDSEC_CHAIN -n -v24. Useful CrowdSec Commands
sudo cscli decisions list
sudo cscli alerts list
sudo cscli metrics
sudo tail -f /var/log/crowdsec.logFinal Result
You now have a fully hardened, production-ready server
- Zero-trust Tailscale SSH
- SSH blocked publicly via Hetzner Firewall
- Dokploy installed with HTTPS
- CrowdSec protecting against attacks
- Strict iptables firewall
- Default DROP policy
- Journald optimized
- Docker daemon verified
- Swap enabled
- Root login disabled