Docker Compose simplifies running multi-container applications by defining every service, network, and volume in a single docker-compose.yml file. Instead of typing long docker run commands for each container, you declare the desired state and let Compose handle the orchestration. On RHEL 8 with Docker CE installed, Docker Compose V2 ships as a plugin and is invoked with docker compose (no hyphen). This tutorial builds a realistic three-tier stack — a web application, a PostgreSQL database, and a Redis cache — and covers environment variables, health checks, scaling, and production considerations.

Prerequisites

  • RHEL 8 with Docker CE and the Docker Compose plugin installed
  • Root or sudo access
  • A working directory for your project files
  • Basic familiarity with YAML syntax

Step 1 — Create the Project Directory and .env File

Separate configuration from the Compose file using a .env file. Docker Compose automatically loads it when you run any docker compose command in the same directory.

mkdir -p ~/myapp && cd ~/myapp

cat > .env <<'EOF'
POSTGRES_DB=appdb
POSTGRES_USER=appuser
POSTGRES_PASSWORD=SecurePass!42
REDIS_PASSWORD=RedisPass!99
APP_PORT=8080
EOF

Step 2 — Write the docker-compose.yml

Create a docker-compose.yml file that ties together the application, database, and cache. The services section defines each container, networks isolates traffic, and volumes persists data.

cat > docker-compose.yml <<'EOF'
version: "3.9"

services:
  app:
    build: .
    image: myapp:latest
    ports:
      - "${APP_PORT}:3000"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      - REDIS_URL=redis://:${REDIS_PASSWORD}@cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy
    networks:
      - frontend
      - backend
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    volumes:
      - pg_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend
    restart: unless-stopped

networks:
  frontend:
  backend:
    internal: true

volumes:
  pg_data:
  redis_data:
EOF

Step 3 — Build and Start the Stack

Build any images defined with a build key and start all services in detached mode. The --build flag forces a rebuild even if the image already exists.

sudo docker compose up -d --build

Check that every service started and passed its health check:

sudo docker compose ps
sudo docker compose logs --tail=30

Step 4 — Interact with Running Services

Use docker compose exec to run commands inside a running container. This is useful for running database migrations, opening a shell, or querying Redis.

# Open a PostgreSQL prompt
sudo docker compose exec db psql -U appuser -d appdb

# Open a Redis CLI session
sudo docker compose exec cache redis-cli -a RedisPass!99

# Open a shell in the app container
sudo docker compose exec app sh

Step 5 — Scale the Application Service

Compose can run multiple replicas of a stateless service. Remove the fixed host port from the app service first (use a load balancer instead), then scale horizontally:

sudo docker compose up -d --scale app=3 --no-recreate

Verify the three replicas are running:

sudo docker compose ps app

Step 6 — Production Considerations

For production deployments, store secrets in a secrets manager rather than a plain .env file, enable Docker log rotation, and pin image tags to prevent unplanned updates:

# Add log rotation to /etc/docker/daemon.json
sudo tee /etc/docker/daemon.json <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5"
  }
}
EOF
sudo systemctl restart docker

# Tear down and remove volumes (destructive — backup first)
sudo docker compose down -v

Conclusion

You have deployed a multi-container application stack on RHEL 8 using Docker Compose, complete with a PostgreSQL database, Redis cache, health checks, isolated networks, and persistent volumes. The .env file keeps credentials out of the Compose definition, making the same file reusable across environments by swapping variable values. Scaling stateless services is a single command, and docker compose exec gives immediate access to any running container for debugging and maintenance.

Next steps: How to Install Portainer for Docker Management on RHEL 8, How to Set Up a Private Docker Registry on RHEL 8, and How to Install Podman as a Rootless Docker Alternative on RHEL 8.