Тут дві різні речі, які часто плутають: змінні для самого файлу Compose і змінні всередині контейнера.
1. Підстановка (interpolation) - Compose заміняє ${...} у compose.yaml до запуску контейнерів. Значення беруться з оточення shell і з файлу .env поруч із compose.yaml:
services:
app:
image: myapp:${APP_VERSION:-latest} # за замовчуванням latest
ports:
- "${APP_PORT:-8000}:8000"
environment:
DB_HOST: ${DB_HOST:?DB_HOST is required} # помилка, якщо не задано
${VAR:-default}- значення за замовчуванням, якщо змінна порожня чи не задана;${VAR:?повідомлення}- зупинити з помилкою, якщо не задана;$$- буквальний знак долара.
2. Змінні контейнера - потрапляють в оточення процесу всередині:
services:
app:
environment: # явно в compose.yaml
APP_ENV: local
env_file: # з файлу - усі змінні файлу йдуть у контейнер
- .env.docker
Ключова різниця: .env поруч із compose.yaml не передається в контейнер автоматично - він лише використовується для підстановки ${...}. А env_file передає змінні в контейнер, але не бере участі в підстановці.
Плутанина з Laravel: у Laravel-проєкті .env - це ще й файл конфігурації застосунку. Laravel Sail використовує це: той самий .env і для підстановки в compose.yaml (${APP_PORT}, ${FORWARD_DB_PORT}), і для застосунку, який читає його з каталогу проєкту через bind mount.
Пріоритет змінних контейнера (від вищого): docker compose run -e, environment у файлі, env_file, ENV в образі.
Перевірка: docker compose config показує файл після підстановки - видно, яке значення реально потрапило.
Безпека: .env з секретами - у .gitignore. Для продакшену секрети краще передавати механізмом секретів, а не змінними (їх видно в docker inspect).