Skip to content
~/sph.sh

Cost-Effective Private Server Setup with VPS, Dokploy, and Cloudflared

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

Why This Stack?

Running a private server doesn't have to mean expensive cloud bills or complex infrastructure. Working with various deployment setups taught me that the sweet spot often lies in combining simple, focused tools rather than reaching for enterprise platforms.

This guide walks through setting up a production-ready private server using:

  • VPS for affordable compute (~$5-20/month)
  • Dokploy for Docker-based deployments with a clean UI
  • Cloudflared for secure access without opening ports

The total cost? About $5-10/month for a basic setup that handles 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 beauty of this setup: your server never exposes ports directly. All traffic flows through Cloudflare's encrypted tunnel.

Step 1: VPS Selection and Initial Setup

Choosing a Provider

I've worked with several VPS providers. Here's what I've learned about the budget-friendly options:

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

For this guide, I'll use Hetzner, but 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:

bash
ssh root@your-server-ip

First, update the system:

bash
apt update && apt upgrade -y

Step 2: Security Hardening

Here's what I've learned: spend 15 minutes on security now, save yourself hours of headaches later.

Create a Non-Root User

bash
# Create useradduser deployusermod -aG sudo deploy
# Test sudo accesssu - deploysudo ls -la /root

Setup SSH Key Authentication

On your local machine:

bash
# Generate SSH key if you don't have onessh-keygen -t ed25519 -C "[email protected]"
# Copy to serverssh-copy-id deploy@your-server-ip

Harden SSH Configuration

Back on the server:

bash
sudo nano /etc/ssh/sshd_config

Update these settings:

text
# Disable root loginPermitRootLogin no
# Disable password authenticationPasswordAuthentication noPubkeyAuthentication yes
# Change default port (optional but recommended)Port 2222
# Disable empty passwordsPermitEmptyPasswords no

Restart SSH:

bash
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)

bash
# Set defaultssudo ufw default deny incomingsudo ufw default allow outgoing
# Allow SSH (use your custom port if changed)sudo ufw allow 2222/tcp
# Enable firewallsudo ufw enable
# Check statussudo ufw status verbose

Install fail2ban

Protects against brute force attacks:

bash
sudo apt install fail2ban -y
# Create local configsudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.localsudo nano /etc/fail2ban/jail.local

Update the SSH section:

ini
[sshd]enabled = trueport = 2222filter = sshdlogpath = /var/log/auth.logmaxretry = 3bantime = 3600

Start fail2ban:

bash
sudo systemctl enable fail2bansudo 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

bash
# Install Dockercurl -fsSL https://get.docker.com -o get-docker.shsudo sh get-docker.sh
# Add user to docker groupsudo usermod -aG docker deploy
# Enable Docker servicesudo systemctl enable dockersudo systemctl start docker
# Verify installationdocker --version

Tip: Log out and back in for the docker group change to take effect.

Install Dokploy

bash
# One-line installationcurl -sSL https://dokploy.com/install.sh | sh

This script:

  1. Sets up Docker if not installed
  2. Installs Dokploy services
  3. Configures the web interface
  4. Starts the Dokploy dashboard

After installation, Dokploy runs on port 3000. But here's the thing: we're not going to expose this port directly. That's where Cloudflared comes in.

Step 4: Cloudflared Tunnel Setup

Instead of opening ports, we'll create a secure tunnel through Cloudflare. This approach has a few advantages I've come to appreciate:

  • No port forwarding needed
  • Built-in DDoS protection
  • Free SSL certificates
  • Traffic analytics

Install Cloudflared

bash
# Download and installwget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.debsudo dpkg -i cloudflared-linux-amd64.deb

Authenticate with Cloudflare

bash
cloudflared tunnel login

This opens a browser to authorize the tunnel. Select your domain.

Create and Configure Tunnel

bash
# Create tunnelcloudflared tunnel create dokploy-server
# Note the tunnel ID from output

Create configuration file:

bash
sudo mkdir -p /etc/cloudflaredsudo nano /etc/cloudflared/config.yml

Add this configuration:

yaml
tunnel: YOUR-TUNNEL-IDcredentials-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

bash
# Create DNS recordcloudflared tunnel route dns dokploy-server dokploy.yourdomain.com

Run Tunnel as Service

bash
# Install as servicesudo cloudflared service install
# Start servicesudo systemctl start cloudflaredsudo systemctl enable cloudflared
# Check statussudo systemctl status cloudflared

Now visit https://dokploy.yourdomain.com - you should see the Dokploy dashboard, accessed 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

  1. Open Dokploy dashboard (https://dokploy.yourdomain.com)
  2. Complete initial setup (create admin account)
  3. Click "Create Project"
  4. Choose "Application"

Example: Deploy Node.js App

Here's a basic deployment configuration:

Project Structure:

my-app/├── Dockerfile├── package.json└── src/    └── index.js

Dockerfile:

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:

json
{  "name": "my-app",  "version": "1.0.0",  "main": "src/index.js",  "dependencies": {    "express": "^4.18.2"  }}

src/index.js:

javascript
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:

  1. Connect your Git repository (GitHub, GitLab, etc.)
  2. Set build settings (Dockerfile-based)
  3. Configure port: 3000
  4. Deploy!

Add Tunnel Route for Your App

Update /etc/cloudflared/config.yml:

yaml
tunnel: YOUR-TUNNEL-IDcredentials-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:

bash
cloudflared tunnel route dns dokploy-server app.yourdomain.comsudo systemctl restart cloudflared

Monitoring and Maintenance

Resource Monitoring

I've found these simple checks keep things running smoothly:

bash
# Check disk spacedf -h
# Check memory usagefree -h
# Check Docker resource usagedocker stats --no-stream
# Check active containersdocker ps

Log Management

bash
# View Dokploy logsdocker logs dokploy
# View Cloudflared logssudo journalctl -u cloudflared -f
# View specific container logs in Dokploydocker logs <container-name>

Backup Strategy

What I've learned about backups: automate them early.

bash
# Create backup scriptnano ~/backup.sh
bash
#!/bin/bashBACKUP_DIR="/home/deploy/backups"DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directorymkdir -p $BACKUP_DIR
# Backup Dokploy datadocker run --rm \  -v dokploy_data:/data \  -v $BACKUP_DIR:/backup \  alpine tar czf /backup/dokploy_$DATE.tar.gz -C /data .
# Keep only last 7 daysfind $BACKUP_DIR -name "dokploy_*.tar.gz" -mtime +7 -delete
echo "Backup completed: dokploy_$DATE.tar.gz"

Make executable and schedule:

bash
chmod +x ~/backup.sh
# Add to crontab (daily at 2 AM)crontab -e# Add: 0 2 * * * /home/deploy/backup.sh

Update Procedure

bash
# Update system packagessudo apt update && sudo apt upgrade -y
# Update Dokploy (check their docs for latest)docker pull dokploy/dokploy:latest
# Update Cloudflaredsudo cloudflared update

Troubleshooting Common Issues

Tunnel Connection Issues

If your Cloudflared tunnel isn't connecting:

bash
# Check service statussudo systemctl status cloudflared
# Check logssudo journalctl -u cloudflared -n 50
# Verify config syntaxcloudflared tunnel validate /etc/cloudflared/config.yml
# Test tunnel manuallycloudflared tunnel run dokploy-server

Dokploy Not Accessible

bash
# Check if Dokploy is runningdocker ps | grep dokploy
# Check Dokploy logsdocker logs dokploy
# Restart Dokploydocker restart dokploy

Port Already in Use

If you get "port already in use" errors:

bash
# Find what's using the portsudo lsof -i :3000
# Or use netstatsudo netstat -tulpn | grep :3000

Docker Storage Issues

bash
# Check Docker disk usagedocker system df
# Clean up unused resourcesdocker system prune -a
# Remove specific containers/imagesdocker rm <container-id>docker rmi <image-id>

Cost Breakdown

Here's what this setup actually costs:

ComponentMonthly Cost
VPS (Hetzner 2GB)$5-6
Domain name$1-2 (yearly, amortized)
Cloudflare TunnelFree
DokployFree (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

Working with this stack over several deployments, here's what stands out:

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.

What I'd Do Differently:

  • Set up monitoring earlier (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:

  1. Add monitoring: Set up Uptime Robot or similar for availability checks
  2. Enable automatic updates: Configure unattended-upgrades for security patches
  3. Set up databases: Dokploy supports PostgreSQL, MySQL, MongoDB out of the box
  4. Create staging environment: Clone your setup to a second cheap VPS for testing
  5. Implement CI/CD: Connect GitHub Actions to trigger Dokploy deployments

Conclusion

This VPS + Dokploy + Cloudflared stack provides a practical middle ground between expensive managed platforms and complex self-hosted setups. The total setup time runs about 30-45 minutes, and ongoing maintenance is minimal - maybe an hour per month for updates and monitoring.

The approach works particularly well for:

  • Side projects and small applications
  • Learning DevOps without cloud platform complexity
  • Teams wanting deployment simplicity without vendor lock-in
  • Cost-conscious production workloads with moderate traffic

Is it perfect? No. You're trading managed platform convenience for cost savings and control. But for many use cases, that's exactly the right trade-off.

The infrastructure patterns used here - containerization, reverse proxying, secure tunneling - are the same patterns used at scale. You're learning production-grade concepts on a budget-friendly platform.

Start simple, monitor your resources, and scale when you actually need it. That's often more practical than over-engineering from day one.

Related Posts