| 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:
environment:
DATABASE_PASSWORD: supersecretpassword123
environment:
DATABASE_PASSWORD: ${DATABASE_PASSWORD:?Set database password}
Use Proper Variable Generation in template.toml:
[variables]
db_password = "${password:32}"
secret_key = "${base64:64}"
jwt_secret = "${base64:48}"
internal_token = "${password:48}"
Mask Sensitive Output:
environment:
API_KEY: ${API_KEY:?Set API key}
Pattern 2: Network Isolation
Database/Cache Services (Internal Only):
services:
postgres:
image: postgres:16-alpine
networks:
- app-net
redis:
image: redis:7-alpine
networks:
- app-net
Web Services (External + Internal):
services:
app:
image: myapp:1.0.0
networks:
- app-net
- dokploy-network
labels:
- "traefik.enable=true"
Network Definition:
networks:
app-net:
driver: bridge
dokploy-network:
external: true
Pattern 3: Image Security
Pin Image Versions:
image: postgres:16-alpine
image: mongo:7
image: redis:7-alpine
image: wardpearce/paaster:3.1.7
image: postgres:latest
image: mongo
image: myapp
Use Official/Trusted Images:
image: postgres:16-alpine
image: redis:7-alpine
image: mongo:7
image: ghcr.io/paperless-ngx/paperless-ngx:2.13
image: codeberg.org/forgejo/forgejo:9
Alpine Images (Smaller Attack Surface):
image: postgres:16-alpine
image: redis:7-alpine
image: node:20-alpine
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
Drop Capabilities:
services:
app:
image: myapp:1.0.0
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
No Privileged Mode:
services:
app:
image: myapp:1.0.0
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:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
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"
- "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
Network
Images
Configuration
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
restart: always
depends_on:
postgres:
condition: service_healthy
environment:
APP_DOMAIN: ${DOMAIN:?Set your domain}
APP_URL: https://${DOMAIN}
DATABASE_URL: postgresql://${DB_USER:-app}:${DB_PASS}@postgres:5432/${DB_NAME:-app}
SECRET_KEY: ${SECRET_KEY:?Set secret key}
JWT_SECRET: ${JWT_SECRET:?Set JWT secret}
DEBUG: "false"
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"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.routers.app.middlewares=security-headers@docker"
- "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
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
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 setup
dokploy-environment-config: Secret handling
dokploy-cloudflare-integration: Zero Trust
Invoked By
/dokploy-create command: 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