I deployed an AI-generated Docker Compose file to staging last month. Within 3 hours, someone was mining crypto on my server.
I spent the next 6 hours learning Docker security the hard way so you don't have to.
What you'll secure: AI-generated Docker Compose files with 7 critical vulnerability fixes Time needed: 20 minutes to audit and fix Difficulty: You need basic Docker knowledge but I'll show you exactly what to change
This guide turns vulnerable AI configs into production-ready setups using security practices that actually prevent attacks.
Why I Built This
ChatGPT gave me a "perfect" Docker Compose file for my Node.js app. It looked clean, started fast, and worked immediately.
My setup:
- Node.js API with PostgreSQL database
- Redis for caching
- Nginx reverse proxy
- Standard three-tier architecture
What didn't work:
- Running everything as root user (classic AI mistake)
- Database exposed to the entire internet on port 5432
- Hardcoded passwords in plain text
- No resource limits (hello, crypto miners)
Time wasted: 6 hours cleaning up the mess, 2 hours learning proper fixes
The 7 Security Holes AI Always Creates
The problem: AI tools prioritize "just working" over security
My solution: Systematic audit checklist that catches every common vulnerability
Time this saves: Prevents production incidents that cost days to resolve
Security Hole 1: Running Everything as Root
AI loves user: root because it avoids permission issues.
# ❌ AI generates this (NEVER deploy)
services:
app:
image: node:18
user: root # Security nightmare
volumes:
- .:/app
# ✅ Production-ready fix
services:
app:
image: node:18
user: "1001:1001" # Non-root user
volumes:
- .:/app:ro # Read-only mount
security_opt:
- no-new-privileges:true
What this does: Creates unprivileged user that can't escalate permissions Expected output: Container starts normally but attackers can't get root access
My production container running as user 1001 - yours should match this UID
Personal tip: "Always use UIDs instead of usernames. User 'node' might not exist in your base image, but 1001 always works."
Security Hole 2: Exposed Database Ports
AI exposes every service port "for easier debugging."
# ❌ AI generates this (database exposed to internet)
services:
postgres:
image: postgres:15
ports:
- "5432:5432" # Visible to entire internet
environment:
POSTGRES_PASSWORD: password123 # Also terrible
# ✅ Secure database configuration
services:
postgres:
image: postgres:15
# No ports section = internal only access
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
networks:
- backend
secrets:
db_password:
file: ./secrets/db_password.txt
networks:
backend:
driver: bridge
internal: true # No external access
What this does: Database only accessible by other containers, secrets loaded from files Expected output: App connects to database, but port 5432 not visible externally
Port scan of my server after fixes - database port completely hidden
Personal tip: "Test this with nmap -p 5432 your-server-ip. If it shows 'open', you're vulnerable."
Security Hole 3: Hardcoded Secrets
AI puts passwords directly in compose files for convenience.
# ❌ Never commit this to git
services:
app:
environment:
DATABASE_URL: postgresql://user:supersecret123@db:5432/myapp
JWT_SECRET: jwt_secret_key_123
API_KEY: sk-1234567890abcdef
# ✅ Proper secrets management
services:
app:
environment:
DATABASE_URL_FILE: /run/secrets/database_url
JWT_SECRET_FILE: /run/secrets/jwt_secret
API_KEY_FILE: /run/secrets/api_key
secrets:
- database_url
- jwt_secret
- api_key
secrets:
database_url:
file: ./secrets/database_url.txt
jwt_secret:
file: ./secrets/jwt_secret.txt
api_key:
file: ./secrets/api_key.txt
Create your secrets directory:
# Create secrets directory (add to .gitignore!)
mkdir -p secrets
echo "postgresql://user:$(openssl rand -base64 32)@db:5432/myapp" > secrets/database_url.txt
echo "$(openssl rand -base64 64)" > secrets/jwt_secret.txt
echo "your-real-api-key-here" > secrets/api_key.txt
chmod 600 secrets/*
What this does: Secrets loaded from files, never stored in git history Expected output: App reads secrets from mounted files at runtime
Personal tip: "Add secrets/ to .gitignore immediately. I once pushed API keys by accident - took 2 hours to rotate everything."
Security Hole 4: No Resource Limits
AI never sets memory or CPU limits, enabling resource exhaustion attacks.
# ❌ AI generates unlimited resource usage
services:
app:
image: node:18
# No resource limits = can consume entire server
# ✅ Resource limits prevent attacks
services:
app:
image: node:18
deploy:
resources:
limits:
cpus: '0.50' # 50% of one CPU core
memory: 512M # 512MB RAM limit
reservations:
cpus: '0.25' # Guaranteed 25% CPU
memory: 256M # Guaranteed 256MB RAM
restart: unless-stopped
What this does: Prevents containers from consuming all server resources Expected output: Container uses exactly the resources you allocate
My containers staying within defined limits - notice the CPU and memory caps
Personal tip: "Start with conservative limits and increase based on monitoring. I killed production twice by setting limits too low."
Security Hole 5: Overly Permissive Volume Mounts
AI mounts entire directories with write access by default.
# ❌ AI gives full filesystem access
services:
app:
volumes:
- .:/app # Full write access to host
- /var/run/docker.sock:/var/run/docker.sock # Docker control (extremely dangerous)
# ✅ Minimal, read-only volume access
services:
app:
volumes:
- ./src:/app/src:ro # Source code read-only
- ./package.json:/app/package.json:ro
- app_node_modules:/app/node_modules # Named volume for dependencies
- app_uploads:/app/uploads # Specific upload directory only
volumes:
app_node_modules:
app_uploads:
What this does: Minimal file access, prevents container from modifying host system Expected output: App works normally but can't write outside designated areas
Personal tip: "Never mount Docker socket unless absolutely necessary. I've seen containers launch other containers for crypto mining."
Security Hole 6: Missing Security Context
AI skips security hardening configurations entirely.
# ❌ AI provides no security context
services:
app:
image: node:18
# Missing all security configurations
# ✅ Comprehensive security hardening
services:
app:
image: node:18
security_opt:
- no-new-privileges:true # Prevent privilege escalation
- apparmor:docker-default # Enable AppArmor protection
cap_drop:
- ALL # Remove all capabilities
cap_add:
- CHOWN # Add only needed capabilities
- SETGID
- SETUID
read_only: true # Read-only root filesystem
tmpfs:
- /tmp # Writable temporary directory
What this does: Enables kernel-level security features and removes dangerous capabilities Expected output: Container runs with maximum security restrictions
Personal tip: "Test with cap_drop: ALL first. Add capabilities back only when you get permission errors."
Security Hole 7: Outdated Base Images with Vulnerabilities
AI uses generic tags like latest or old versions with known CVEs.
# ❌ AI uses vulnerable or unpredictable images
services:
app:
image: node:latest # Unpredictable updates
database:
image: postgres:12 # Old version with vulnerabilities
# ✅ Pinned, secure, updated images
services:
app:
image: node:18.17.1-alpine # Specific version, minimal base
database:
image: postgres:15.4-alpine # Latest stable, security patches
Check your images for vulnerabilities:
# Install trivy for vulnerability scanning
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Scan your images
trivy image node:18.17.1-alpine
trivy image postgres:15.4-alpine
What this does: Uses specific versions with latest security patches Expected output: Trivy reports "no vulnerabilities found" or minimal low-severity issues
Clean security scan after updating base images - aim for zero critical/high severity issues
Personal tip: "Alpine Linux images are 5x smaller and have fewer attack surfaces. Always worth the compatibility testing."
Complete Secure Docker Compose Template
Here's your copy-paste template that fixes all 7 vulnerabilities:
version: '3.8'
services:
app:
image: node:18.17.1-alpine
user: "1001:1001"
working_dir: /app
command: ["node", "server.js"]
environment:
NODE_ENV: production
DATABASE_URL_FILE: /run/secrets/database_url
JWT_SECRET_FILE: /run/secrets/jwt_secret
secrets:
- database_url
- jwt_secret
volumes:
- ./src:/app/src:ro
- ./package.json:/app/package.json:ro
- app_node_modules:/app/node_modules
networks:
- frontend
- backend
depends_on:
- database
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
security_opt:
- no-new-privileges:true
- apparmor:docker-default
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
database:
image: postgres:15.4-alpine
user: "999:999"
environment:
POSTGRES_DB: myapp
POSTGRES_USER: appuser
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- db_data:/var/lib/postgresql/data
networks:
- backend
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
security_opt:
- no-new-privileges:true
restart: unless-stopped
reverse_proxy:
image: nginx:1.25.2-alpine
user: "101:101"
ports:
- "80:8080"
- "443:8443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
networks:
- frontend
depends_on:
- app
deploy:
resources:
limits:
cpus: '0.25'
memory: 128M
security_opt:
- no-new-privileges:true
restart: unless-stopped
secrets:
database_url:
file: ./secrets/database_url.txt
jwt_secret:
file: ./secrets/jwt_secret.txt
db_password:
file: ./secrets/db_password.txt
volumes:
app_node_modules:
db_data:
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true
Security Validation Checklist
Run these commands to verify your fixes worked:
# 1. Check no root processes
docker compose exec app ps aux
# Should show processes running as user 1001, not root
# 2. Verify no exposed database ports
nmap -p 5432 localhost
# Should show "filtered" or "closed", not "open"
# 3. Test resource limits
docker stats --no-stream
# Memory usage should stay under your limits
# 4. Scan for vulnerabilities
trivy config docker-compose.yml
# Should report "no misconfiguration found"
# 5. Check secrets not in environment
docker compose exec app env | grep -i password
# Should show file paths, not actual passwords
Expected results: All checks pass with no security warnings
My Terminal after running all security checks - green across the board
What You Just Built
A production-ready Docker Compose configuration that blocks the 7 most common attack vectors in AI-generated configs.
Key Takeaways (Save These)
- Non-root users: Use UID numbers, never run as root or you're asking for trouble
- Internal networks: Database should never be accessible from internet, period
- Secrets management: Files > environment variables > hardcoded (always)
- Resource limits: Set before deploying or watch your server get crypto-mined
Tools I Actually Use
- Trivy Scanner: aquasecurity/trivy - Best free vulnerability scanner
- Docker Bench Security: docker/docker-bench-security - Automated security audit
- Hadolint: hadolint/hadolint - Dockerfile linting for security
- Official Docker Security Docs: docs.docker.com/engine/security - Comprehensive security reference