Table of Contents


Start with the Reproducibility Problem in Local Environments

Docker Compose is a tool for defining and running multi-container applications. Instead of long runtime commands, you declare service configuration in compose.yaml and reproduce the same environment repeatedly. The impact varies by team, but Compose usually helps reduce local environment drift and unifies start/stop/log workflows under one command system.

When running a single container, docker run is often enough. Once the application, MySQL, and Redis must run together, option and startup-order management gets complex quickly. Compose fixes this configuration in a file and allows teams to reproduce the same local environment.


Core Components of the Compose Model

Compose centers on three major components.

  • services: runnable container units
  • networks: communication network among services
  • volumes: persistent storage that survives container restart
  • configs and secrets are also used when required

A basic structure:

services:
  app:
    image: eclipse-temurin:17-jre

  mysql:
    image: mysql:8.0

volumes:
  mysql-data:

The structure is simple, but stable startup usually requires ports, environment, depends_on, and healthcheck. For databases in particular, data can disappear on container recreation if volume mapping is missing.


How to Read compose.yaml

Start from the fields most frequently used in production.

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: order-api
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: local
      DB_HOST: mysql
      DB_PORT: "3306"
    depends_on:
      mysql:
        condition: service_healthy

  mysql:
    image: mysql:8.0
    container_name: order-mysql
    environment:
      MYSQL_DATABASE: order
      MYSQL_USER: order_user
      MYSQL_PASSWORD: order_pw
      MYSQL_ROOT_PASSWORD: root_pw
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -proot_pw"]
      interval: 10s
      timeout: 3s
      retries: 10

volumes:
  mysql-data:

The key combination is depends_on + healthcheck. The app container can start first, but if MySQL is not ready, connection failures occur. Declaring both startup order and readiness often improves boot stability. healthcheck runs inside the target container, so 127.0.0.1 works in this context.

This combination only controls Compose startup behavior. It does not replace reconnect, retry, and timeout logic in the application. Also, fixing container_name can limit service scaling (multiple instances).


Common Docker Compose Commands

These commands cover most early usage.

# Run in background
$ docker compose up -d

# Check status
$ docker compose ps

# Follow logs of a specific service
$ docker compose logs -f app

# Run command inside a container
$ docker compose exec mysql mysql -u root -p

# Stop and clean up
$ docker compose down

docker compose exec mysql mysql -u root -p prompts password interactively. In production, avoid -ppassword style because credentials can remain in shell history.

down cleans up the project network and related resources. To remove volumes as well, add -v. Confirm first whether local DB data should be preserved. Named volumes are generally preserved, but data state can still vary based on bind mounts or DB initialization scripts.


Boundary Between Compose and Application Logic

Even with correct Compose settings, startup failures repeat if the application ignores dependency readiness. A practical approach is to separate transient from permanent errors and keep clear log context.

@Slf4j
@Component
public class DatabaseStartupVerifier {

    public void verify(String requestId) {
        try {
            // Check DB readiness with a short timeout
            pingWithTimeout(1000);
            log.info("mysql-ready requestId={}", requestId);
        } catch (SocketTimeoutException e) {
            // Transient error: delayed DB startup
            log.warn("mysql-timeout requestId={} message={}", requestId, e.getMessage());
            throw new RetryableDependencyException(e);
        } catch (AuthenticationException e) {
            // Permanent error: account or permission configuration issue
            log.error("mysql-auth-failed requestId={} message={}", requestId, e.getMessage());
            throw new NonRetryableDependencyException(e);
        }
    }

    private void pingWithTimeout(int timeoutMs) {
        // Example implementation
    }
}

Apply retries only where needed. Retrying permanent errors increases startup time and delays incident signals.


Role of the Compose Specification

If Compose is treated only as a tool, syntax differences by implementation can look confusing. The Compose Specification is the common contract that defines the compose.yaml format.

It does not just describe Docker Compose CLI behavior. It defines how keys such as services, networks, volumes, configs, and secrets are interpreted. Using the spec as the baseline improves team consistency and reduces migration cost when tooling changes.


Summary

Docker Compose is more than a convenience command for running multiple containers. It is a declarative way to freeze local execution environments. A practical path is to first understand concepts and file structure, then move to specification-based production-like composition.

The next post continues with a Compose Specification-based compose.yaml for MySQL and Redis local backend setup.

References