Docker Compose: correct config for MariaDB + MongoDB + Qdrant (ports, env vars, volumes) and how containers should connect?

Summary

This postmortem analyzes a common misconfiguration when orchestrating a multi-database local development environment using Docker Compose. The primary failure observed is a lack of inter-container networking knowledge, leading to containers attempting to connect to databases via localhost rather than the Docker Compose service name. This results in “Connection Refused” errors. The correct approach involves using Docker Compose’s internal DNS resolution where services communicate using service names as hostnames over internal network ports, avoiding host port exposure unless external access is explicitly required.

Root Cause

The root cause of connectivity failures in this scenario is the misunderstanding of Docker’s network isolation.

  1. Incorrect Hostname Usage: Developers often use localhost or 127.0.0.1 within application code or environment variables when connecting to a database container. Inside a Docker container, localhost refers to the container itself, not the Docker host or other containers. This causes connection attempts to fail immediately.
  2. Missing Service Dependencies: Without a depends_on configuration in the docker-compose.yaml, the application container may attempt to connect to the database before the database process has fully initialized (i.e., accepted socket connections).
  3. Improper Volume Mapping: Using anonymous volumes or omitting volumes entirely results in data loss upon container removal or recreation.
  4. Port Binding Conflicts: Binding all database ports to the host (ports: ...) exposes them unnecessarily to the host machine, creating security risks and potential port conflicts.

Why This Happens in Real Systems

In local development, the boundary between the host machine and the container environment is often blurred.

  • Mental Model Gap: Developers are accustomed to running databases directly on localhost. When moving to Docker, the network stack changes, but the mental model often lags, causing configuration errors where the application looks for the database at the wrong address.
  • Rapid Prototyping: During quick setup phases, developers often skip volume definitions to “get something running,” leading to the frustration of losing data every time they run docker compose down.
  • Default Network Behavior: Docker Compose creates a default network for the project. While services can resolve each other by name, this behavior is non-obvious to those new to container orchestration, leading to hardcoded IP addresses or hostnames that do not exist within the internal network.

Real-World Impact

  • Development Velocity Loss: Engineers spend hours debugging connection strings instead of writing features.
  • Data Inconsistency: Without correct volume mounts, local databases reset on every restart, causing loss of seed data, migration history, or user sessions.
  • Environment Parity: Misconfigured local setups rarely match production configurations, leading to “it works on my machine” bugs that are difficult to diagnose.
  • Port Conflicts: Binding standard ports (e.g., 3306, 27017) to the host can block other local services or native installations, causing system-wide instability.

Example or Code

Below is a corrected docker-compose.yaml configuration. Note the use of service names as hostnames in connection strings and the proper volume mappings.

version: '3.8'

services:
  mariadb:
    image: mariadb:latest
    container_name: local_mariadb
    restart: always
    environment:
      MARIADB_ROOT_PASSWORD: rootpassword
      MARIADB_DATABASE: myapp_db
      MARIADB_USER: myapp_user
      MARIADB_PASSWORD: myapp_pass
    volumes:
      - mariadb_data:/var/lib/mysql
    networks:
      - dev_network
    # No ports exposed here unless you need to connect from the laptop.
    # If you must, map a specific host port to avoid conflicts: "3307:3306"

  mongodb:
    image: mongo:latest
    container_name: local_mongodb
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
      MONGO_INITDB_DATABASE: myapp_db
    volumes:
      - mongodb_data:/data/db
    networks:
      - dev_network

  qdrant:
    image: qdrant/qdrant:latest
    container_name: local_qdrant
    restart: always
    volumes:
      - qdrant_data:/qdrant/storage
    networks:
      - dev_network

  # Example application container connecting to the databases
  app:
    build: .
    container_name: local_app
    depends_on:
      - mariadb
      - mongodb
      - qdrant
    environment:
      # Connection strings use SERVICE NAMES, not localhost
      DB_HOST: mariadb
      DB_PORT: 3306
      MONGO_URL: mongodb://root:example@mongodb:27017
      QDRANT_URL: http://qdrant:6333
    networks:
      - dev_network

volumes:
  mariadb_data:
  mongodb_data:
  qdrant_data:

networks:
  dev_network:
    driver: bridge

How Senior Engineers Fix It

Senior engineers approach container orchestration by defining clear boundaries and dependency graphs.

  1. Define Internal Networking: They configure the application to connect to databases using the Docker Compose service name (e.g., mariadb for the host). This leverages Docker’s embedded DNS server.
  2. Implement Health Checks: Instead of relying solely on depends_on (which only checks container start, not readiness), they add healthcheck definitions to database services. The application container then waits for the database to be healthy before attempting connection.
  3. Abstract Secrets: While okay for local dev, seniors often move sensitive variables (passwords) out of the docker-compose.yaml and into .env files (excluded from git) or Docker secrets for production-like environments.
  4. Minimize Exposure: They avoid publishing ports to the host unless debugging is required, reducing attack surface and port collision risks.

Why Juniors Miss It

  • Lack of Container Networking Concepts: Juniors often do not realize that each container has its own isolated network namespace. They assume containers share the host’s loopback interface.
  • Copy-Pasting from Tutorials: Many tutorials run databases with -p 3306:3306 and connect via localhost from a script running on the host, not from another container. Juniors copy this pattern blindly without understanding the distinction.
  • Overlooking depends_on Behavior: They assume depends_on guarantees the database is ready to accept connections, not just that the container has started. This leads to race conditions in application startup.
  • Volume Mapping Complexity: Understanding the correct internal mount paths (e.g., /var/lib/mysql vs /data/db) requires reading specific database image documentation, which is often skipped in favor of trial and error.