| name | dokploy-security-hardening |
| description | Security best practices for Dokploy templates: secrets management, network isolation, least privilege, image security, and hardening recommendations. |
| version | 1.0.0 |
| author | Home Lab Infrastructure Team |
Dokploy Security Hardening
When to Use This Skill
- When reviewing templates for security issues
- When adding security configurations to templates
- When user asks about "security" or "hardening"
- As final review step before template completion
When NOT to Use This Skill
- For application-level security (auth, input validation)
- For host-level security (not managed by Dokploy)
Prerequisites
- Completed docker-compose.yml
- Understanding of application security requirements
Security Principles
- Least Privilege: Services only have access they need
- Defense in Depth: Multiple security layers
- Secrets Protection: No hardcoded secrets
- Network Isolation: Internal services not exposed
- Image Security: Pinned versions, trusted sources
Core Patterns
Pattern 1: Secrets Management
Never Hardcode Secrets:
# WRONG - Secret in compose file
environment:
DATABASE_PASSWORD: supersecretpassword123
# CORRECT - Use variable with required syntax
environment:
DATABASE_PASSWORD: ${DATABASE_PASSWORD:?Set database password}
Use Proper Variable Generation in template.toml:
[variables]
# Passwords - random alphanumeric
db_password = "${password:32}"
# Secrets - base64 encoded
secret_key = "${base64:64}"
jwt_secret = "${base64:48}"
# Internal tokens - high entropy
internal_token = "${password:48}"
Mask Sensitive Output:
# In docker-compose, sensitive vars are hidden in Dokploy UI
environment:
API_KEY: ${API_KEY:?Set API key} # Treated as sensitive
Pattern 2: Network Isolation
Database/Cache Services (Internal Only):
services:
postgres:
image: postgres:16-alpine
networks:
- app-net # Internal ONLY - no dokploy-network
# NO labels - not exposed via Traefik
redis:
image: redis:7-alpine
networks:
- app-net # Internal ONLY
Web Services (External + Internal):
services:
app:
image: myapp:1.0.0
networks:
- app-net # Internal (to reach database)
- dokploy-network # External (for Traefik)
labels:
- "traefik.enable=true"
# ... routing labels
Network Definition:
networks:
app-net:
driver: bridge
# Internal network, not externally accessible
dokploy-network:
external: true
# Managed by Dokploy, shared with Traefik
Pattern 3: Image Security
Pin Image Versions:
# CORRECT - Specific versions
image: postgres:16-alpine
image: mongo:7
image: redis:7-alpine
image: wardpearce/paaster:3.1.7
# WRONG - Floating tags
image: postgres:latest
image: mongo
image: myapp # Implies :latest
Use Official/Trusted Images:
# Prefer official images
image: postgres:16-alpine # Official
image: redis:7-alpine # Official
image: mongo:7 # Official
# For third-party, use verified sources
image: ghcr.io/paperless-ngx/paperless-ngx:2.13 # GitHub verified
image: codeberg.org/forgejo/forgejo:9 # Codeberg verified
Alpine Images (Smaller Attack Surface):
# Prefer Alpine variants when available
image: postgres:16-alpine # vs postgres:16
image: redis:7-alpine # vs redis:7
image: node:20-alpine # vs node:20
Pattern 4: Container Security
Read-Only Filesystem (Where Possible):
services:
app:
image: myapp:1.0.0
read_only: true
tmpfs:
- /tmp
- /var/run
volumes:
- app-data:/app/data # Only writable location
Drop Capabilities:
services:
app:
image: myapp:1.0.0
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if needed for port < 1024
No Privileged Mode:
# NEVER use privileged mode for application containers
services:
app:
image: myapp:1.0.0
# privileged: true # NEVER DO THIS
Pattern 5: Resource Limits
Memory and CPU Limits:
services:
app:
image: myapp:1.0.0
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
reservations:
memory: 128M
cpus: "0.25"
Pattern 6: Health Check Security
Don't Expose Sensitive Info:
healthcheck:
# CORRECT - Simple endpoint
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
# WRONG - Exposes internal state
test: ["CMD", "curl", "-f", "http://localhost:8080/debug/vars"]
Pattern 7: Security Headers (Traefik Middleware)
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.routers.app.middlewares=security-headers@docker"
# Security headers middleware
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
- "traefik.http.middlewares.security-headers.headers.browserXssFilter=true"
- "traefik.http.middlewares.security-headers.headers.referrerPolicy=strict-origin-when-cross-origin"
- "traefik.http.services.app.loadbalancer.server.port=8080"
- "traefik.docker.network=dokploy-network"
Security Checklist
Secrets
- No hardcoded passwords in compose file
- All secrets use
${VAR:?message}syntax - Passwords generated with
${password:N}in template.toml - Encryption keys generated with
${base64:N} - API keys from external services left blank for user input
Network
- Databases on internal network only
- Caches on internal network only
- Only web-facing services on dokploy-network
- No exposed debug/admin ports
Images
- All images have pinned versions
- No
:latesttags - Using official or verified images
- Alpine variants where available
Configuration
- Sensitive env vars not logged
- Health endpoints don't expose sensitive data
- Debug mode disabled by default
- Production-safe defaults
Security Review Template
When reviewing a template, check each category:
## Security Review: [Template Name]
### Secrets Management
- [ ] Secrets: All secrets use variable syntax
- [ ] Passwords: Generated in template.toml
- [ ] External APIs: Left blank for user input
### Network Isolation
- [ ] Databases: Internal network only
- [ ] Web services: dokploy-network attached
- [ ] No debug ports exposed
### Image Security
- [ ] Versions: All images pinned
- [ ] Sources: Official/verified images
- [ ] Alpine: Used where available
### Container Security
- [ ] Privileges: No privileged mode
- [ ] Resources: Limits defined (optional but recommended)
- [ ] Health: Secure health endpoints
### HTTPS/TLS
- [ ] TLS: Using letsencrypt certresolver
- [ ] Entrypoint: websecure (HTTPS)
- [ ] Headers: Security headers middleware (recommended)
### Findings
- [ ] Issue 1: [Description] - [Severity]
- [ ] Issue 2: [Description] - [Severity]
### Recommendations
1. [Recommendation]
2. [Recommendation]
Complete Example: Secure Template
services:
app:
image: myapp:1.2.3 # Pinned version
restart: always
depends_on:
postgres:
condition: service_healthy
environment:
# Domain (required)
APP_DOMAIN: ${DOMAIN:?Set your domain}
APP_URL: https://${DOMAIN}
# Database (secure connection)
DATABASE_URL: postgresql://${DB_USER:-app}:${DB_PASS}@postgres:5432/${DB_NAME:-app}
# Secrets (all use variables)
SECRET_KEY: ${SECRET_KEY:?Set secret key}
JWT_SECRET: ${JWT_SECRET:?Set JWT secret}
# Security settings
DEBUG: "false" # Production default
SECURE_COOKIES: "true"
networks:
- app-net
- dokploy-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.app.entrypoints=websecure" # HTTPS only
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.routers.app.middlewares=security-headers@docker"
# Security headers
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
- "traefik.http.services.app.loadbalancer.server.port=8080"
- "traefik.docker.network=dokploy-network"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
postgres:
image: postgres:16-alpine # Alpine, pinned version
restart: always
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${DB_NAME:-app}
POSTGRES_USER: ${DB_USER:-app}
POSTGRES_PASSWORD: ${DB_PASS:?Set database password}
networks:
- app-net # Internal ONLY
# NO dokploy-network - not exposed
# NO Traefik labels - not routed
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-app} -d ${DB_NAME:-app}"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
postgres-data:
driver: local
networks:
app-net:
driver: bridge
dokploy-network:
external: true
Common Pitfalls
Pitfall 1: Database on external network
Issue: Database accessible to other containers Solution: Only connect to internal app-net
Pitfall 2: Debug enabled in production
Issue: Exposes sensitive information Solution: Default DEBUG to "false"
Pitfall 3: Floating image tags
Issue: Unexpected updates, security regressions Solution: Pin all image versions
Pitfall 4: Hardcoded secrets in compose
Issue: Secrets in version control
Solution: Use ${VAR:?message} syntax
Integration
Skills-First Approach (v2.0+)
This skill is part of the skills-first architecture - loaded during Validation phase (Phase 4) to perform security review of generated templates.
Related Skills
dokploy-compose-structure: Network setupdokploy-environment-config: Secret handlingdokploy-cloudflare-integration: Zero Trust
Invoked By
/dokploy-createcommand: Phase 4 (Validation) - Step 1
Order in Workflow (Progressive Loading)
1-3. Phase 3: Generation skills (all files created)
4. This skill: Security review and hardening (Phase 4, Step 1)
5. dokploy-template-validation: Convention compliance validation
6. docker compose config: Final syntax validation
See: .claude/commands/dokploy-create.md for full workflow