بنقرة واحدة
dokploy-security-hardening
// Security best practices for Dokploy templates: secrets management, network isolation, least privilege, image security, and hardening recommendations.
// Security best practices for Dokploy templates: secrets management, network isolation, least privilege, image security, and hardening recommendations.
| 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 |
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
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
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
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
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"
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"]
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"
${VAR:?message} syntax${password:N} in template.toml${base64:N}:latest tagsWhen 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]
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
Issue: Database accessible to other containers Solution: Only connect to internal app-net
Issue: Exposes sensitive information Solution: Default DEBUG to "false"
Issue: Unexpected updates, security regressions Solution: Pin all image versions
Issue: Secrets in version control
Solution: Use ${VAR:?message} syntax
This skill is part of the skills-first architecture - loaded during Validation phase (Phase 4) to perform security review of generated templates.
dokploy-compose-structure: Network setupdokploy-environment-config: Secret handlingdokploy-cloudflare-integration: Zero Trust/dokploy-create command: Phase 4 (Validation) - Step 11-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
Integrate Cloudflare services with Dokploy templates: R2 storage, DNS challenge for SSL, Zero Trust Access, Workers, WAF, and Tunnel. Default to CF services for external dependencies.
Generate Docker Compose files following Dokploy conventions with proper networking, volumes, and service patterns. Use when creating new Dokploy templates or converting existing compose files.
Environment variable patterns for Dokploy templates including required vs optional syntax, secrets, connection strings, and configuration organization.
Health check patterns for different service types in Dokploy templates. Covers HTTP, PostgreSQL, MongoDB, Redis, MySQL, and custom health checks.
Multi-service architecture patterns for Dokploy templates including dependency chains, service communication, and complex stack design. Use when building templates with 2+ services.
Multi-tenancy patterns for Dokploy templates with network isolation: separate docker networks per tenant, shared infrastructure, and tenant-specific configuration.