Dokploy + Cloudflare Tunnel on a VPS: Setup Guide
A practical guide to setting up a secure, affordable private server using VPS, Dokploy for deployments, and Cloudflared tunnels for secure access without exposing ports
Running a private server doesn’t have to mean expensive cloud bills or complex infrastructure. The sweet spot often lies in combining simple, focused tools rather than reaching for enterprise platforms.
A production-ready private server needs three pieces:
- VPS for affordable compute (~$5-20/month)
- Dokploy for Docker-based deployments with a clean UI
- Cloudflared for secure access without opening ports
Together they cost about $6-8 per month and handle multiple applications securely.
Prerequisites#
Before starting, you’ll need:
- A domain name (for Cloudflare tunnel)
- Basic terminal/SSH knowledge
- A Cloudflare account (free tier works)
- ~30 minutes of setup time
Architecture Overview#
Here’s what we’re building:
The key property: your server never exposes ports directly. All traffic flows through Cloudflare’s encrypted tunnel.
Step 1: VPS Selection and Initial Setup#
Choosing a Provider#
Budget-friendly options worth knowing:
Hetzner ($5-10/month):
- Excellent price/performance
- European data centers (good GDPR compliance)
- Reliable network
- Great for production workloads
Contabo ($4-8/month):
- Very affordable
- More resources for the price
- Network can be inconsistent during peak times
DigitalOcean ($6-12/month):
- Excellent documentation
- Predictable performance
- Great community support
The examples below use Hetzner, but the commands work across providers.
Minimum Specs#
For Dokploy and a few small apps:
- RAM: 2GB minimum (4GB recommended)
- CPU: 1-2 cores
- Storage: 20GB SSD minimum
- OS: Ubuntu 22.04 LTS
Initial Server Setup#
Once your VPS is provisioned, SSH into it:
ssh root@your-server-ip
First, update the system:
apt update && apt upgrade -y
Step 2: Security Hardening#
Spending 15 minutes on security now saves hours of headaches later.
Create a Non-Root User#
# Create user
adduser deploy
usermod -aG sudo deploy
# Test sudo access
su - deploy
sudo ls -la /root
Setup SSH Key Authentication#
On your local machine:
# Generate SSH key if you don't have one
ssh-keygen -t ed25519 -C "your-email@example.com"
# Copy to server
ssh-copy-id deploy@your-server-ip
Harden SSH Configuration#
Back on the server:
sudo nano /etc/ssh/sshd_config
Update these settings:
# Disable root login
PermitRootLogin no
# Disable password authentication
PasswordAuthentication no
PubkeyAuthentication yes
# Change default port (optional but recommended)
Port 2222
# Disable empty passwords
PermitEmptyPasswords no
Restart SSH:
sudo systemctl restart sshd
Warning
Before closing your current SSH session, test the new configuration in a separate terminal. If something’s wrong, you still have access to fix it.
Configure Firewall (UFW)#
# Set defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (use your custom port if changed)
sudo ufw allow 2222/tcp
# Enable firewall
sudo ufw enable
# Check status
sudo ufw status verbose
Install fail2ban#
Protects against brute force attacks:
sudo apt install fail2ban -y
# Create local config
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Update the SSH section:
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
Start fail2ban:
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Step 3: Dokploy Installation#
Dokploy provides a Heroku-like deployment experience with Docker. Here’s what makes it useful: simple web UI, built-in database support, and straightforward app deployment.
Install Docker#
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Add user to docker group
sudo usermod -aG docker deploy
# Enable Docker service
sudo systemctl enable docker
sudo systemctl start docker
# Verify installation
docker --version
Tip
Log out and back in for the docker group change to take effect.
Install Dokploy#
# One-line installation
curl -sSL https://dokploy.com/install.sh | sh
This script:
- Sets up Docker if not installed
- Installs Dokploy services
- Configures the web interface
- Starts the Dokploy dashboard
After installation, Dokploy runs on port 3000. We won’t expose that port directly, though. That’s where Cloudflared comes in.
Step 4: Cloudflared Tunnel Setup#
Instead of opening ports, create a secure tunnel through Cloudflare. This approach has several advantages:
- No port forwarding needed
- Built-in DDoS protection
- Free SSL certificates
- Traffic analytics
Install Cloudflared#
# Download and install
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
Authenticate with Cloudflare#
cloudflared tunnel login
This opens a browser to authorize the tunnel. Select your domain.
Create and Configure Tunnel#
# Create tunnel
cloudflared tunnel create dokploy-server
# Note the tunnel ID from output
Create configuration file:
sudo mkdir -p /etc/cloudflared
sudo nano /etc/cloudflared/config.yml
Add this configuration:
tunnel: YOUR-TUNNEL-ID
credentials-file: /root/.cloudflared/YOUR-TUNNEL-ID.json
ingress:
# Dokploy dashboard
- hostname: dokploy.yourdomain.com
service: http://localhost:3000
# Catch-all rule (required)
- service: http_status:404
Setup DNS#
# Create DNS record
cloudflared tunnel route dns dokploy-server dokploy.yourdomain.com
Run Tunnel as Service#
# Install as service
sudo cloudflared service install
# Start service
sudo systemctl start cloudflared
sudo systemctl enable cloudflared
# Check status
sudo systemctl status cloudflared
Now visit https://dokploy.yourdomain.com. You should see the Dokploy dashboard, served securely through the Cloudflare tunnel.
Step 5: Deploying Your First Application#
Let’s deploy a simple Node.js application to verify everything works.
Create App in Dokploy#
- Open Dokploy dashboard (
https://dokploy.yourdomain.com) - Complete initial setup (create admin account)
- Click “Create Project”
- Choose “Application”
Example: Deploy Node.js App#
Here’s a basic deployment configuration:
Project Structure:
my-app/
├── Dockerfile
├── package.json
└── src/
└── index.js
Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "src/index.js"]
package.json:
{
"name": "my-app",
"version": "1.0.0",
"main": "src/index.js",
"dependencies": {
"express": "^4.18.2"
}
}
src/index.js:
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.json({
message: 'Hello from Dokploy!',
timestamp: new Date().toISOString()
});
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Configure Deployment#
In Dokploy:
- Connect your Git repository (GitHub, GitLab, etc.)
- Set build settings (Dockerfile-based)
- Configure port: 3000
- Deploy!
Add Tunnel Route for Your App#
Update /etc/cloudflared/config.yml:
tunnel: YOUR-TUNNEL-ID
credentials-file: /root/.cloudflared/YOUR-TUNNEL-ID.json
ingress:
# Dokploy dashboard
- hostname: dokploy.yourdomain.com
service: http://localhost:3000
# Your app
- hostname: app.yourdomain.com
service: http://localhost:8080
- service: http_status:404
Create DNS and restart:
cloudflared tunnel route dns dokploy-server app.yourdomain.com
sudo systemctl restart cloudflared
Monitoring and Maintenance#
Resource Monitoring#
These simple checks keep things running smoothly:
# Check disk space
df -h
# Check memory usage
free -h
# Check Docker resource usage
docker stats --no-stream
# Check active containers
docker ps
Log Management#
# View Dokploy logs
docker logs dokploy
# View Cloudflared logs
sudo journalctl -u cloudflared -f
# View specific container logs in Dokploy
docker logs <container-name>
Backup Strategy#
Automate backups early.
# Create backup script
nano ~/backup.sh
#!/bin/bash
BACKUP_DIR="/home/deploy/backups"
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directory
mkdir -p $BACKUP_DIR
# Backup Dokploy data
docker run --rm \
-v dokploy_data:/data \
-v $BACKUP_DIR:/backup \
alpine tar czf /backup/dokploy_$DATE.tar.gz -C /data .
# Keep only last 7 days
find $BACKUP_DIR -name "dokploy_*.tar.gz" -mtime +7 -delete
echo "Backup completed: dokploy_$DATE.tar.gz"
Make executable and schedule:
chmod +x ~/backup.sh
# Add to crontab (daily at 2 AM)
crontab -e
# Add: 0 2 * * * /home/deploy/backup.sh
Update Procedure#
# Update system packages
sudo apt update && sudo apt upgrade -y
# Update Dokploy (check their docs for latest)
docker pull dokploy/dokploy:latest
# Update Cloudflared
sudo cloudflared update
Troubleshooting Common Issues#
Tunnel Connection Issues#
If your Cloudflared tunnel isn’t connecting:
# Check service status
sudo systemctl status cloudflared
# Check logs
sudo journalctl -u cloudflared -n 50
# Verify config syntax
cloudflared tunnel validate /etc/cloudflared/config.yml
# Test tunnel manually
cloudflared tunnel run dokploy-server
Dokploy Not Accessible#
# Check if Dokploy is running
docker ps | grep dokploy
# Check Dokploy logs
docker logs dokploy
# Restart Dokploy
docker restart dokploy
Port Already in Use#
If you get “port already in use” errors:
# Find what's using the port
sudo lsof -i :3000
# Or use netstat
sudo netstat -tulpn | grep :3000
Docker Storage Issues#
# Check Docker disk usage
docker system df
# Clean up unused resources
docker system prune -a
# Remove specific containers/images
docker rm <container-id>
docker rmi <image-id>
Cost Breakdown#
What this setup costs each month:
| Component | Monthly Cost |
|---|---|
| VPS (Hetzner 2GB) | $5-6 |
| Domain name | $1-2 (annual fee, spread over 12 months) |
| Cloudflare Tunnel | Free |
| Dokploy | Free (open source) |
| Total | ~$6-8/month |
Compare this to managed platforms:
- Heroku: $25-50/month for similar resources
- AWS Lightsail: $10-20/month (without tunnel/UI)
- Managed Kubernetes: $50-100/month minimum
Lessons Learned#
A few things stand out after running this stack across several deployments:
What Works Well:
- Security first approach: Cloudflared eliminates a whole class of security concerns. No open ports means no port scanning attacks.
- Simple updates: Dokploy handles container orchestration without Kubernetes complexity.
- Cost control: Fixed monthly cost, no surprise bills from traffic spikes.
- Developer experience: Git push to deploy feels modern without vendor lock-in.
Trade-offs to Consider:
- Single point of failure: One VPS means downtime if hardware fails. For critical apps, consider multiple regions.
- Manual scaling: Unlike managed platforms, scaling means creating additional VPS instances and load balancing.
- Backup responsibility: You own the backup strategy. Automate it or risk data loss.
- Limited resources: A $5 VPS won’t handle thousands of concurrent users. Know your limits.
Worth Doing Earlier:
- Set up monitoring (Uptime Robot or similar)
- Document the initial setup steps immediately
- Create a staging environment from day one
- Automate security updates with unattended-upgrades
Next Steps#
Once your basic setup is running, consider:
- Add monitoring: Set up Uptime Robot or similar for availability checks
- Enable automatic updates: Configure unattended-upgrades for security patches
- Set up databases: Dokploy supports PostgreSQL, MySQL, MongoDB out of the box
- Create staging environment: Clone your setup to a second cheap VPS for testing
- Implement CI/CD: Connect GitHub Actions to trigger Dokploy deployments
Conclusion#
The VPS + Dokploy + Cloudflared combination holds up for side projects, small applications, and cost-conscious production workloads with moderate traffic. Setup runs about 30-45 minutes, and maintenance stays around an hour a month for updates and monitoring. It also fits teams that want deployment simplicity without vendor lock-in, since the patterns underneath (containerization, reverse proxying, secure tunneling) carry over to larger platforms.
Reach for something else when availability matters more than the bill. A single VPS fails as a unit, so any service with an uptime commitment belongs on a platform with managed failover, or at least on a second machine behind a load balancer. The same applies once traffic outgrows one box: past that point the manual scaling work costs more than the managed bill you avoided.
References#
- Cloudflare Tunnel Documentation (opens in new tab) - Official guide for setting up cloudflared to expose services securely without opening inbound ports.
- Dokploy Documentation (opens in new tab) - Official documentation for the self-hosted PaaS, covering installation, application deployment, and database management.
- Dokploy Installation Guide (opens in new tab) - Step-by-step setup instructions for deploying Dokploy on a VPS with Docker.
- Cloudflare Tunnel Setup Guide (opens in new tab) - Reference for configuring tunnels, ingress rules, and routing traffic to local services.
- Traefik Proxy Documentation (opens in new tab) - Documentation for the reverse proxy used internally by Dokploy to route traffic between containers.
- Next.js Self-Hosting Guide (opens in new tab) - Official Next.js documentation for deploying applications on your own infrastructure with Node.js or Docker.
Related posts
Production deploys need a real approval gate: use GitHub Environments with native protection rules and scoped secrets, not workflow if: hacks or marketplace actions.
github-actions · ci-cd · devops +2
A practical guide to deploying Next.js beyond Vercel, covering the cost profile, implementation details, and migration path for each target platform.
nextjs · deployment · aws +4
The exact IAM size, attach, and quota limits you will hit at scale, and the scoped-policy, permission-boundary, and SCP structure that keeps you far from every one.
aws · iam · security +2
How high-performing teams keep security review from becoming a lead-time bottleneck: shift-left automation, risk-based gates, a paved road, dependency cadence.
ci-cd · devops · security +3
A practical guide to building an org-level shared GitHub Actions platform: architecture decisions, security governance, adoption, and 7 costly mistakes.
github-actions · ci-cd · devops +5