Dokploy Deployment Automation: Self-Hosted DevOps Platform
Dokploy is a powerful self-hosted deployment platform that simplifies application deployment and management. This comprehensive guide covers setting up Dokploy for automated deployments, Docker orchestration, and complete DevOps workflows.
Why Choose Dokploy?
- Self-Hosted: Complete control over your deployment infrastructure
- Docker Native: Built-in Docker and Docker Compose support
- Git Integration: Automatic deployments from Git repositories
- Multiple Apps: Deploy and manage multiple applications
- Database Management: Built-in database services
- SSL Automation: Automatic SSL certificate management
Step 1: Dokploy Installation and Setup
Install Dokploy on your VPS or dedicated server:
# Update system packages
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Install Dokploy
curl -sSL https://dokploy.com/install.sh | sh
# Start Dokploy services
dokploy start
Step 2: Initial Configuration
Configure Dokploy for your environment:
# Access Dokploy configuration
dokploy config
# Set up domain and SSL
dokploy domain add yourdomain.com
dokploy ssl enable yourdomain.com
# Configure Git integration
dokploy git setup
Create initial configuration file:
version: "1.0"
# Global configuration
global:
domain: "dokploy.yourdomain.com"
ssl:
enabled: true
email: "admin@yourdomain.com"
provider: "letsencrypt"
# Database configuration
database:
postgres:
enabled: true
version: "15"
port: 5432
mysql:
enabled: false
redis:
enabled: true
version: "7"
port: 6379
# Backup configuration
backups:
enabled: true
schedule: "0 2 * * *" # Daily at 2 AM
retention: 30 # days
storage:
type: "s3"
bucket: "dokploy-backups"
region: "us-east-1"
# Network configuration
networks:
dokploy:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
# Default resource limits
resources:
default:
memory: "512m"
cpu: "0.5"
small:
memory: "256m"
cpu: "0.25"
medium:
memory: "1g"
cpu: "1"
large:
memory: "2g"
cpu: "2"
dokploy.config.yml
Step 3: Application Template Configuration
Create reusable application templates:
name: "Node.js Application"
description: "Template for Node.js applications with PostgreSQL"
category: "web"
# Build configuration
build:
context: "."
dockerfile: "Dockerfile"
args:
NODE_ENV: "production"
target: "production"
# Environment variables
environment:
- name: "NODE_ENV"
value: "production"
required: true
- name: "PORT"
value: "3000"
required: true
- name: "DATABASE_URL"
value: "postgresql://{{.postgres_user}}:{{.postgres_password}}@postgres:5432/{{.postgres_db}}"
required: true
- name: "REDIS_URL"
value: "redis://redis:6379"
required: false
- name: "JWT_SECRET"
value: "{{.jwt_secret}}"
required: true
secret: true
# Service configuration
services:
app:
build: .
ports:
- "3000:3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://{{.postgres_user}}:{{.postgres_password}}@postgres:5432/{{.postgres_db}}
- REDIS_URL=redis://redis:6379
- JWT_SECRET={{.jwt_secret}}
volumes:
- app_storage:/app/storage
- app_logs:/app/logs
labels:
- "dokploy.enable=true"
- "dokploy.http.routers.app.rule=Host(`{{.domain}}`)"
- "dokploy.http.routers.app.tls=true"
- "dokploy.http.routers.app.tls.certresolver=letsencrypt"
- "dokploy.http.services.app.loadbalancer.server.port=3000"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: "{{.postgres_db}}"
POSTGRES_USER: "{{.postgres_user}}"
POSTGRES_PASSWORD: "{{.postgres_password}}"
volumes:
- postgres_data:/var/lib/postgresql/data
labels:
- "dokploy.enable=false"
restart: unless-stopped
healthcheck:
test:
["CMD-SHELL", "pg_isready -U {{.postgres_user}} -d {{.postgres_db}}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
labels:
- "dokploy.enable=false"
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# Volume configuration
volumes:
postgres_data:
redis_data:
app_storage:
app_logs:
# Health checks
healthcheck:
path: "/health"
interval: 30
timeout: 10
retries: 3
# Deployment configuration
deployment:
strategy: "rolling"
max_unavailable: 1
max_surge: 1
# Scaling configuration
scaling:
enabled: true
min_replicas: 1
max_replicas: 5
target_cpu: 70
target_memory: 80
templates/nodejs-app.yml
Step 4: Advanced Docker Compose Setup
Create a comprehensive Docker Compose configuration:
version: "3.8"
services:
# Main application
web-app:
build:
context: .
dockerfile: Dockerfile
target: production
args:
NODE_ENV: production
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://app_user:${POSTGRES_PASSWORD}@postgres:5432/app_db
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
- API_KEY=${API_KEY}
- SMTP_HOST=${SMTP_HOST}
- SMTP_PORT=${SMTP_PORT}
- SMTP_USER=${SMTP_USER}
- SMTP_PASS=${SMTP_PASS}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- app_storage:/app/storage
- app_logs:/app/logs
- app_uploads:/app/uploads
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.yourdomain.com`)"
- "traefik.http.routers.app.tls=true"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=3000"
- "traefik.http.routers.app.middlewares=auth,ratelimit"
networks:
- dokploy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
# Background worker
worker:
build:
context: .
dockerfile: Dockerfile.worker
args:
NODE_ENV: production
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://app_user:${POSTGRES_PASSWORD}@postgres:5432/app_db
- REDIS_URL=redis://redis:6379
- QUEUE_NAME=default
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- app_logs:/app/logs
networks:
- dokploy
restart: unless-stopped
deploy:
replicas: 2
# Database
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- dokploy
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
interval: 10s
timeout: 5s
retries: 5
command:
[
"postgres",
"-c",
"shared_preload_libraries=pg_stat_statements",
"-c",
"pg_stat_statements.track=all",
"-c",
"max_connections=200",
"-c",
"shared_buffers=256MB",
"-c",
"effective_cache_size=1GB",
"-c",
"work_mem=4MB",
"-c",
"maintenance_work_mem=64MB",
]
# Redis cache
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
- ./redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
networks:
- dokploy
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# Traefik reverse proxy
traefik:
image: traefik:v3.0
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=admin@yourdomain.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--global.checkNewVersion=false"
- "--log.level=INFO"
- "--accesslog=true"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
- traefik_logs:/var/log/traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.yourdomain.com`)"
- "traefik.http.routers.traefik.tls=true"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$10$$..."
- "traefik.http.middlewares.ratelimit.ratelimit.burst=100"
- "traefik.http.middlewares.ratelimit.ratelimit.average=50"
networks:
- dokploy
restart: unless-stopped
# Monitoring with Prometheus
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--web.console.libraries=/etc/prometheus/console_libraries"
- "--web.console.templates=/etc/prometheus/consoles"
- "--storage.tsdb.retention.time=30d"
- "--web.enable-lifecycle"
networks:
- dokploy
restart: unless-stopped
# Grafana for visualization
grafana:
image: grafana/grafana:latest
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.rule=Host(`grafana.yourdomain.com`)"
- "traefik.http.routers.grafana.tls=true"
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
networks:
- dokploy
restart: unless-stopped
# Log aggregation with Loki
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
volumes:
- ./monitoring/loki.yml:/etc/loki/local-config.yaml
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
networks:
- dokploy
restart: unless-stopped
volumes:
postgres_data:
redis_data:
app_storage:
app_logs:
app_uploads:
traefik_letsencrypt:
traefik_logs:
prometheus_data:
grafana_data:
loki_data:
networks:
dokploy:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
docker-compose.production.yml
Step 5: CI/CD Pipeline Integration
Create GitHub Actions workflow for Dokploy deployment:
name: Deploy to Dokploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
- name: Build application
run: npm run build
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix=main-
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to Dokploy
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.DOKPLOY_HOST }}
username: ${{ secrets.DOKPLOY_USER }}
key: ${{ secrets.DOKPLOY_SSH_KEY }}
script: |
# Navigate to application directory
cd /opt/dokploy/apps/my-app
# Pull latest changes
git pull origin main
# Update environment variables
echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" > .env.production
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env.production
echo "API_KEY=${{ secrets.API_KEY }}" >> .env.production
# Deploy using Dokploy CLI
dokploy app deploy my-app --env production
# Wait for deployment to complete
dokploy app status my-app --wait
# Run database migrations if needed
dokploy app exec my-app "npm run migrate"
# Health check
sleep 30
if ! curl -f https://app.yourdomain.com/health; then
echo "Health check failed, rolling back"
dokploy app rollback my-app
exit 1
fi
- name: Notify deployment status
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: "#deployments"
webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
if: always()
.github/workflows/deploy.yml
Step 6: Monitoring and Alerting Setup
Configure monitoring with Prometheus and Grafana:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "rules/*.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "node-exporter"
static_configs:
- targets: ["node-exporter:9100"]
- job_name: "docker"
static_configs:
- targets: ["dockerd-exporter:9323"]
- job_name: "app"
static_configs:
- targets: ["web-app:3000"]
metrics_path: "/metrics"
scrape_interval: 5s
- job_name: "postgres"
static_configs:
- targets: ["postgres-exporter:9187"]
- job_name: "redis"
static_configs:
- targets: ["redis-exporter:9121"]
- job_name: "traefik"
static_configs:
- targets: ["traefik:8080"]
metrics_path: "/metrics"
monitoring/prometheus.yml
Step 7: Backup and Recovery Automation
Create automated backup scripts:
#!/bin/bash
# Dokploy backup script
BACKUP_DIR="/opt/dokploy-backups"
S3_BUCKET="${BACKUP_S3_BUCKET}"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/dokploy-backup.log"
# Function to log messages
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Backup application data
backup_application() {
local app_name="$1"
log "Backing up application: $app_name"
# Create application backup
dokploy backup create "$app_name" --output "$BACKUP_DIR/app_${app_name}_${DATE}.tar.gz"
if [ $? -eq 0 ]; then
log "✅ Application backup completed for $app_name"
else
log "❌ Application backup failed for $app_name"
fi
}
# Backup database
backup_database() {
log "Backing up PostgreSQL database"
# Create database dump
docker exec dokploy_postgres_1 pg_dump -U app_user app_db > "$BACKUP_DIR/database_${DATE}.sql"
if [ $? -eq 0 ]; then
log "✅ Database backup completed"
else
log "❌ Database backup failed"
fi
}
# Backup Redis data
backup_redis() {
log "Backing up Redis data"
# Create Redis dump
docker exec dokploy_redis_1 redis-cli BGSAVE
docker cp dokploy_redis_1:/data/dump.rdb "$BACKUP_DIR/redis_${DATE}.rdb"
if [ $? -eq 0 ]; then
log "✅ Redis backup completed"
else
log "❌ Redis backup failed"
fi
}
# Backup Docker volumes
backup_volumes() {
log "Backing up Docker volumes"
# Create tar archive of all volumes
docker run --rm \
-v dokploy_postgres_data:/source/postgres:ro \
-v dokploy_app_storage:/source/app_storage:ro \
-v dokploy_app_logs:/source/app_logs:ro \
-v "$BACKUP_DIR":/backup \
alpine \
tar czf "/backup/volumes_${DATE}.tar.gz" -C /source .
if [ $? -eq 0 ]; then
log "✅ Volume backup completed"
else
log "❌ Volume backup failed"
fi
}
# Upload to S3
upload_to_s3() {
if [ -n "$S3_BUCKET" ]; then
log "Uploading backups to S3"
aws s3 sync "$BACKUP_DIR" "s3://$S3_BUCKET/dokploy-backups/" \
--exclude "*" --include "*${DATE}*"
if [ $? -eq 0 ]; then
log "✅ S3 upload completed"
else
log "❌ S3 upload failed"
fi
fi
}
# Cleanup old backups
cleanup_old_backups() {
log "Cleaning up old backups"
# Keep backups for 30 days
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete
find "$BACKUP_DIR" -name "*.sql" -mtime +30 -delete
find "$BACKUP_DIR" -name "*.rdb" -mtime +30 -delete
log "✅ Cleanup completed"
}
# Main backup function
main() {
log "Starting Dokploy backup process"
backup_application "my-app"
backup_database
backup_redis
backup_volumes
upload_to_s3
cleanup_old_backups
log "Backup process completed"
}
# Run backup
main
scripts/backup.sh
Step 8: Security and Optimization
Implement security best practices:
#!/bin/bash
# Dokploy security hardening script
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"
}
# Configure firewall
setup_firewall() {
log "Configuring firewall..."
# Reset firewall
ufw --force reset
# Default policies
ufw default deny incoming
ufw default allow outgoing
# SSH access
ufw allow 22/tcp
# HTTP/HTTPS
ufw allow 80/tcp
ufw allow 443/tcp
# Dokploy dashboard
ufw allow 8080/tcp
# Rate limiting for SSH
ufw limit 22/tcp
# Enable firewall
ufw --force enable
log "✅ Firewall configured"
}
# Configure Docker security
secure_docker() {
log "Securing Docker..."
# Create Docker daemon configuration
cat > /etc/docker/daemon.json << EOF
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"live-restore": true,
"userland-proxy": false,
"no-new-privileges": true,
"seccomp-profile": "/etc/docker/seccomp.json"
}
EOF
# Restart Docker
systemctl restart docker
log "✅ Docker secured"
}
# Configure system limits
configure_limits() {
log "Configuring system limits..."
cat > /etc/security/limits.d/dokploy.conf << EOF
# Dokploy security limits
* soft nofile 65536
* hard nofile 65536
* soft nproc 4096
* hard nproc 4096
dokploy soft nofile 65536
dokploy hard nofile 65536
EOF
log "✅ System limits configured"
}
# Setup log rotation
setup_log_rotation() {
log "Setting up log rotation..."
cat > /etc/logrotate.d/dokploy << EOF
/opt/dokploy/logs/*.log {
daily
missingok
rotate 30
compress
notifempty
create 644 dokploy dokploy
postrotate
systemctl reload dokploy
endscript
}
EOF
log "✅ Log rotation configured"
}
# Main security function
main() {
log "Starting Dokploy security hardening..."
setup_firewall
secure_docker
configure_limits
setup_log_rotation
log "Security hardening completed"
}
main
scripts/security-hardening.sh
Best Practices Summary
- Use templates for consistent application deployments
- Implement health checks for all services
- Set up monitoring with Prometheus and Grafana
- Configure automated backups with retention policies
- Use environment variables for sensitive configuration
- Implement CI/CD pipelines for automated deployments
- Security hardening with firewalls and proper access controls
- Resource management with proper limits and scaling
- Log aggregation for centralized logging
- Regular updates and maintenance procedures
Management Commands
# Deploy application
dokploy app deploy my-app
# Check application status
dokploy app status my-app
# View application logs
dokploy app logs my-app --follow
# Scale application
dokploy app scale my-app --replicas 3
# Rollback deployment
dokploy app rollback my-app
# Create backup
dokploy backup create my-app
# Restore from backup
dokploy backup restore my-app backup-file.tar.gz
Your Dokploy deployment automation platform is now ready for production use with comprehensive monitoring, automated deployments, and robust security configurations!