Claude Code Plugins

Community-maintained marketplace

Feedback

dokploy-security-hardening

@enuno/dokploy
0
0

Security best practices for Dokploy templates: secrets management, network isolation, least privilege, image security, and hardening recommendations.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

  1. Least Privilege: Services only have access they need
  2. Defense in Depth: Multiple security layers
  3. Secrets Protection: No hardcoded secrets
  4. Network Isolation: Internal services not exposed
  5. 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 :latest tags
  • 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 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