← All Articles ⚙️ Configuration & DevOps High By Nishant Kumar Verma Updated

Docker Compose Security for Solo Founders: Exposed Ports, Secrets, and Defaults

Your docker-compose.yml exposes database ports, hardcodes passwords, and runs containers as root. Here is how to secure it for production.

CFG-004 SEC-014 CFG-005

Quick Answer

AI-generated Docker Compose files ship with three critical security flaws: databases exposed on public ports, default passwords like "postgres" or "password", and no network isolation between services. On a VPS, these defaults mean your database is publicly accessible from the internet with a password that every bot already knows. Fix it by removing external port mappings for internal services, using Docker secrets or strong environment variables, and creating internal networks.

The Default Docker Compose is a Security Hole

When you ask an AI tool to generate a Docker Compose file, the output is optimized for local development convenience, not production security. The AI generates port mappings so you can access everything from your browser, uses simple passwords so the setup works immediately, and puts everything on the default network.

This is fine on localhost. But when you copy the same file to a VPS and run docker compose up -d, every mapped port is now publicly accessible on the internet. In 2024, Aqua Security's Shodan search found more than 800,000 PostgreSQL databases reachable from the internet, and in 2025 Wiz Research counted about 60,000 internet-exposed Redis instances with no authentication configured. Wiz also notes that the official Redis container image does not require authentication by default.

Attackers do not need a sophisticated exploit here. A default password or an exposed port is enough.

The Three Critical Flaws

Flaw 1: Exposed Database Ports

// ❌ BAD - Database accessible from the internet on port 5432
services:
  postgres:
    image: postgres:15
    ports:
      - "5432:5432"    # Anyone on the internet can connect
    environment:
      POSTGRES_PASSWORD: postgres

  redis:
    image: redis:7
    ports:
      - "6379:6379"    # Redis has no auth by default
// ✅ GOOD - No external ports, internal network only
services:
  postgres:
    image: postgres:15
    # No 'ports' - only accessible from other containers
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}  # From .env file
    networks:
      - internal

  redis:
    image: redis:7
    command: redis-server --requirepass ${REDIS_PASSWORD}
    # No 'ports' - only accessible from other containers
    networks:
      - internal

  api:
    build: .
    ports:
      - "3000:3000"    # Only the API is publicly accessible
    networks:
      - internal

networks:
  internal:
    driver: bridge

Flaw 2: Default Passwords

AI tools generate passwords like postgres, password, admin, or changeme. Bots scan for exactly these credentials. If your database port is exposed, they will find it and try them.

// ❌ BAD - Hardcoded default passwords
environment:
  POSTGRES_PASSWORD: postgres
  REDIS_PASSWORD: password
  MONGO_INITDB_ROOT_PASSWORD: admin123

// ✅ GOOD - Strong passwords from .env file (not committed to git)
environment:
  POSTGRES_PASSWORD: ${DB_PASSWORD}
  REDIS_PASSWORD: ${REDIS_PASSWORD}  # for your app; the official redis image needs --requirepass (Flaw 1)

// .env (add to .gitignore!) - fill each value with the output of: openssl rand -hex 24
DB_PASSWORD=your-48-character-hex-value
REDIS_PASSWORD=a-different-48-character-hex-value

Keep $ out of unquoted .env values: Compose reads $name as a variable, so kP9$mN2x becomes kP9 with only a warning in the terminal. Hex passwords also avoid @ and #, which break database connection URLs unless you percent-encode them.

Flaw 3: No Network Isolation

By default, all services in a Docker Compose file share a single network. This means if one service is compromised, the attacker has direct access to every other service. Create separate networks for public-facing services and internal services.

Service Should Be Public Should Have External Port Network
Web app / API Yes Yes (80/443 via reverse proxy) Public + Internal
PostgreSQL No No Internal only
Redis No No Internal only
MongoDB No No Internal only
Workers / Queues No No Internal only

Production Docker Compose Checklist

Before deploying your Docker Compose file to a VPS, check every item:

  1. Remove all database port mappings - PostgreSQL, Redis, MongoDB, MySQL should have no ports: section
  2. Use strong passwords from .env - generate long random values, for example with openssl rand -hex 24
  3. Add .env to .gitignore - never commit secrets to Git
  4. Create a .env.example - document required variables without real values
  5. Pin image versions - use postgres:15-alpine, not postgres:latest
  6. Add health checks - every service should have a healthcheck: block
  7. Set resource limits - add mem_limit and cpus to prevent one container from starving others
  8. Use a reverse proxy - Caddy, Nginx, or Traefik in front of your app for HTTPS termination
  9. Enable restart policies - restart: unless-stopped for production services

Tools like VibeDoctor's Vibe Check flag images on the :latest tag in the Dockerfile and docker-compose.yml at your repository root (CFG-004) and leaked secrets anywhere in the repository. Use the checklist above for the rest. Free to sign up.

For the steps around the containers, such as secrets, HTTPS and monitoring, follow our guide to deploying a vibe-coded app to production.

FAQ

How do I access my database if I remove the port mapping?

Your app containers access the database through the internal Docker network using the service name as the hostname (e.g., postgres://user:pass@postgres:5432/db). For admin access, use docker compose exec postgres psql -U postgres to connect from inside the container, or add a temporary port mapping only while debugging and remove it immediately after.

Is Docker Compose secure enough for production?

Docker Compose is fine for single-server deployments. Most startups and solo founders do not need Kubernetes. The security issues are not with Docker Compose itself but with the default configuration that AI tools generate. A properly configured Compose file with internal networks, strong passwords, and no exposed internal ports is production-ready.

Should I use Docker secrets instead of environment variables?

Compose secrets are more secure because they are mounted as files under /run/secrets/ instead of environment variables, so they do not show up in docker inspect (Swarm also stores them encrypted). For a solo founder on a single VPS, environment variables from a properly secured .env file are pragmatically sufficient. If you handle extremely sensitive data (financial, healthcare), consider Docker secrets or a secrets manager like Vault.

What image tag should I use if not "latest"?

Pin to a specific major version: postgres:15-alpine, redis:7-alpine, node:24-alpine (Node 20 is end-of-life). This keeps you on one major version while still getting patch updates. For byte-identical rebuilds, pin a full version tag or an image digest. Never use :latest in production because it can pull breaking changes without warning during a rebuild.

Diagnose your codebase - free

VibeDoctor checks for CFG-004, SEC-014, CFG-005 and 148 other issues across 21 diagnostic areas - security, performance, code quality, and more.

SCAN MY APP →