How to Set Up and Secure a Dokploy Server on a VPS

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_ip

2. Update System Packages

sudo apt update && sudo apt upgrade -y

3. Create a Non-Root User

adduser sebastian
usermod -aG sudo sebastian

4. Enable Passwordless sudo

echo "sebastian ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/sebastian
sudo chmod 0440 /etc/sudoers.d/sebastian

5. 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_keys

6. Test SSH Login and Remove Password

ssh sebastian@your_vps_ip
sudo whoami

Remove password:

sudo passwd -d sebastian

7. Install Tailscale (Correct Method)

Visit:

https://login.tailscale.com/admin/machines

Click Add device, copy the generated install command, then run it:

curl -fsSL <your-tailscale-install-url> | sh
sudo tailscale up --ssh

The --ssh flag enables Tailscale SSH, giving you a fallback even if OpenSSH breaks.

Tailscale will assign a private IP:

100.x.x.x

Connect 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_config

Set:

PermitRootLogin no

Restart:

sudo systemctl restart ssh

10. Optional: Idle SSH Timeout

sudo nano /etc/ssh/sshd_config

Add:

ClientAliveInterval 900
ClientAliveCountMax 0

Restart:

sudo systemctl restart ssh

11. 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/fstab

12. Install Dokploy

curl -sSL https://dokploy.com/install.sh | sudo sh

Dokploy UI then becomes temporarily available at:

http://your_vps_ip:3000

13. Create DNS A Record

Type

Name

Value

A

app

your_vps_ip

14. Configure Server Domain in Dokploy

Open:

http://your_vps_ip:3000

Navigate to:

Settings → Web Server → Server Domain

Enter:

app.yourdomain.com

Dokploy 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 crowdsec

16. Install the CrowdSec Firewall Bouncer

sudo apt install crowdsec-firewall-bouncer-iptables -y

17. 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 ACCEPT

18. Make Firewall Rules Persistent

sudo apt install iptables-persistent -y

For future updates:

sudo netfilter-persistent save
sudo netfilter-persistent reload

19. Fix CrowdSec Boot-Order Issue

Ensure CrowdSec firewall bouncer loads after netfilter:

sudo systemctl edit crowdsec-firewall-bouncer

Add:

[Unit]
After=netfilter-persistent.service

Save, then:

sudo systemctl daemon-reload
sudo systemctl restart crowdsec-firewall-bouncer

20. Optimize journald (Prevents Log Bloat)

sudo nano /etc/systemd/journald.conf

Set:

SystemMaxUse=200M
Compress=yes
Storage=persistent

Apply:

sudo systemctl restart systemd-journald

21. Check Docker Daemon Exposure (Important)

Ensure Docker is not exposing port 2375:

sudo ss -tulpen | grep dockerd

If 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 list

23. Inspect CrowdSec Firewall Chains

Then inspect, for example:

sudo iptables -L CROWDSEC_CHAIN -n -v

24. Useful CrowdSec Commands

sudo cscli decisions list
sudo cscli alerts list
sudo cscli metrics
sudo tail -f /var/log/crowdsec.log

Final 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