This repository contains Ansible playbooks and GitHub Actions workflows to deploy and manage Dokploy on a Hetzner VPS.
- Automated deployment via GitHub Actions on push to
main - Idempotent playbooks - safe to run multiple times
- Security hardening - separate playbook for post-domain configuration
- Version controlled - all infrastructure changes tracked in Git
- Hetzner VPS (or any Ubuntu server)
- Ubuntu 24.04 LTS
- SSH access configured with key-based authentication
- Minimum 1GB RAM, 20GB storage recommended
- Python 3.11+ (not 3.14 - Ansible doesn't support it yet)
- Ansible 2.14+
- SSH private key for server access
# Using uv (recommended)
uv venv --python 3.11
source .venv/bin/activate
uv pip install ansible
# Or using pip
pip install ansible
# Install required collections
ansible-galaxy collection install -r requirements.ymlIf your SSH key has a passphrase, add it to the ssh-agent:
# Start ssh-agent (if not running)
eval "$(ssh-agent -s)"
# Add your key (enter passphrase once)
ssh-add ~/.ssh/id_ed25519
# Verify key is loaded
ssh-add -lConfigure these secrets in your GitHub repository settings (Settings -> Secrets and variables -> Actions):
| Secret | Description | Required |
|---|---|---|
SSH_PRIVATE_KEY |
Private SSH key content | Yes |
SERVER_IP |
Hetzner VPS public IP address | Yes |
SERVER_USER |
SSH user (typically root) |
Yes |
SSH_KEY_PASSPHRASE |
Passphrase for SSH key (if protected) | No |
Note: The workflows use
ssh-agentto handle SSH keys. If your key has a passphrase, add theSSH_KEY_PASSPHRASEsecret and the workflow will unlock it automatically.
| Workflow | Trigger | Purpose |
|---|---|---|
deploy.yml |
Push to main, manual |
Applies changes to the server |
preview.yml |
Pull requests, manual | Dry-run showing what would change |
lint.yml |
Push, PR | Validates Ansible syntax |
test.yml |
Push, PR | Runs Ansible tests |
It's recommended to use a dedicated SSH key for deployments:
# Generate a new key pair
ssh-keygen -t ed25519 -C "dokploy-deploy" -f ~/.ssh/dokploy_deploy
# Copy the public key to your server
ssh-copy-id -i ~/.ssh/dokploy_deploy.pub root@YOUR_SERVER_IP
# Add the private key content to GitHub Secrets as SSH_PRIVATE_KEY
cat ~/.ssh/dokploy_deployPlease remember to add this key to authorized_keys in your VPS. Below is an example command:
cat ~/.ssh/dokploy_deploy.pub | ssh root@YOUR_SERVER_IP "cat >> ~/.ssh/authorized_keys"Push changes to the main branch to trigger automatic deployment:
git add .
git commit -m "Update Dokploy configuration"
git push origin mainThe workflow runs when:
- Pushing to
mainbranch (changes inansible/or workflow file) - Manual trigger via GitHub Actions UI
For testing or debugging, run Ansible locally:
cd ansible
# Deploy Dokploy
ansible-playbook -i inventory/production.ini playbook.yml \
-e "ansible_host=YOUR_SERVER_IP" \
-e "ansible_user=root" \
-e "ansible_ssh_private_key_file=~/.ssh/your_key"
# Security hardening (after domain is configured)
ansible-playbook -i inventory/production.ini harden.yml \
-e "ansible_host=YOUR_SERVER_IP" \
-e "ansible_user=root" \
-e "ansible_ssh_private_key_file=~/.ssh/your_key"You can manually trigger the workflow from GitHub Actions:
- Go to
Actionstab in your repository - Select
Deploy Dokployworkflow - Click
Run workflow - Choose which playbook to run (
playbook.ymlorharden.yml) - Click
Run workflow
After the first deployment:
- Open
http://YOUR_SERVER_IP:3000in your browser - Create your admin account
- Complete the initial setup wizard
Warning
Security Risk: Your initial password is transmitted over HTTP (unencrypted). Use a temporary/throwaway password during setup. After configuring HTTPS with your domain, change it to a strong, unique password.
Configure your domain in Dokploy:
- Log in to Dokploy dashboard
- Go to
Settings -> Web Server - Enter your domain name
- Save and wait for SSL certificate provisioning
- Verify HTTPS access works:
https://your-domain.com
Important: Only run this AFTER your domain is working!
Via GitHub Actions:
- Go to
Actions -> Deploy Dokploy - Click
Run workflow - Select
harden.yml - Run the workflow
Via CLI:
ansible-playbook -i inventory/production.ini harden.yml \
-e "ansible_host=YOUR_SERVER_IP" \
-e "ansible_user=root"This will:
- Install and configure fail2ban
- Remove port 3000 from the firewall
- Harden SSH configuration
After hardening, Dokploy is only accessible via your domain (not IP:3000).
ansible/
├── inventory/
│ └── production.ini # Server inventory
├── group_vars/
│ └── all.yml # Shared variables
├── roles/
│ ├── dokploy/ # Main Dokploy installation role
│ │ ├── tasks/
│ │ │ └── main.yml
│ │ ├── handlers/
│ │ │ └── main.yml
│ │ └── defaults/
│ │ └── main.yml
│ └── security_hardening/ # Post-domain security role
│ ├── tasks/
│ │ └── main.yml
│ ├── handlers/
│ │ └── main.yml
│ ├── defaults/
│ │ └── main.yml
│ └── templates/
│ └── jail.local.j2
├── playbook.yml # Main deployment playbook
├── harden.yml # Security hardening playbook
└── README.md # This file
Edit ansible/group_vars/all.yml to customize:
# System packages
system_packages:
- curl
- wget
# ... add more as needed
# Firewall ports
ufw_allowed_ports_initial:
- { port: 22, proto: tcp, comment: "SSH" }
- { port: 80, proto: tcp, comment: "HTTP" }
# ... customize ports
# Fail2ban settings
fail2ban_bantime: 3600 # Ban duration in seconds
fail2ban_maxretry: 5 # Failed attempts before ban# Test SSH connection manually
ssh -i ~/.ssh/your_key root@YOUR_SERVER_IP
# Ensure the key is added to the server
ssh-copy-id -i ~/.ssh/your_key.pub root@YOUR_SERVER_IP# Check Docker status on the server
ssh root@YOUR_SERVER_IP "docker ps -a"
# Check Dokploy logs
ssh root@YOUR_SERVER_IP "docker logs dokploy"# Check UFW status
ssh root@YOUR_SERVER_IP "ufw status verbose"
# Temporarily allow a port
ssh root@YOUR_SERVER_IP "ufw allow 3000/tcp"The playbook is idempotent. If Dokploy is already installed, it will skip the installation step. To force reinstall:
# On the server, remove Dokploy container
docker stop dokploy && docker rm dokploy
# Then re-run the playbook- SSH private keys are never committed to the repository
- Server IP is stored as a GitHub secret
- Use dedicated SSH keys for CI/CD (not personal keys)
- After domain setup, port 3000 should be closed
- fail2ban protects against SSH brute force attacks