Переменные окружения, volume, сети

Переменные окружения, volume, сети

Если у тебя конфиги захардкожены - Docker тебя накажет. Один раз. Потом ты исправишь.

env

DATABASE_URL=postgres://...
JWT_SECRET=super-secret

volumes

Volume нужен, чтобы данные жили дольше контейнера.

volumes:
 - dbdata:/var/lib/postgresql/data

networks (обычно по умолчанию)

Compose создаёт сеть сам. Но можно явно, если нужно разделить.

Не коммить `.env` в Git. Используй переменные CI/CD или секреты на сервере. Как настроить `.gitignore` и что делать, если секрет всё-таки уехал в историю - в уроке про [git ignore и секреты](../git/10-ignore.md).

Три типа volumes в Docker: named volume, bind mount, tmpfs

Named volumes vs bind mounts

services:
  db:
    volumes:
 - dbdata:/var/lib/postgresql/data    # named volume (Docker управляет)
 - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # bind mount (твой файл)

volumes:
  dbdata: {}
  • Named volume - данные живут внутри Docker, переживают docker compose down
  • Bind mount - монтируешь файл/каталог с хоста. Удобно для конфигов и dev-режима
В разработке монтируй исходники: `- ./backend:/app`. Код меняется на хосте - виден в контейнере. Для Go нужен live-reload (air), для Node - уже встроен.

.env vs env_file vs Docker secrets

Три способа передать значения в контейнер - у каждого своя ниша:

Подход            Где живёт                Когда выбирать
────────────      ─────────────────         ──────────────────────────────────
.env (compose)    рядом с compose.yaml     dev/local; compose сам подхватит
env_file          указанный путь            staging/prod; разные файлы под
                                            окружения (.env.staging, .env.prod)
Docker secrets    docker swarm/k8s secret   прод-секреты (пароли БД, JWT_KEY)
                  монтируются как файл - не светятся в `docker inspect`
services:
  backend:
    env_file: .env.staging       # переменные из файла
    environment:
 - LOG_LEVEL=debug          # переопределяем точечно
    secrets:
 - db_password              # монтируется в /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_pass.txt
Любая переменная из `environment` или `.env` видна через `docker inspect ` и в логах процесса. Для реальных секретов (БД, JWT, API ключи) в проде используй Docker secrets / Kubernetes secrets / Vault - они монтируются файлом, а не переменной.

Volumes: ловушки с правами на файлы

# Контейнер пишет от root (UID=0):
docker run -v $(pwd)/data:/app/data myapp
ls -la data/
# -rw-r--r-- 1 root root file.log  ← на хосте теперь файл от root

Это классическая боль bind mount: процесс в контейнере запускается от root, и файлы на хосте получают владельца root. Локально мешает (sudo rm), в CI ломает следующие шаги.

# Решение: запускать процесс от обычного пользователя
RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app
USER app

Либо в compose: user: "1000:1000". Совпадение UID хоста и контейнера = одинаковые права.

Bind mount поверх named volume = потеря данных

services:
  db:
    volumes:
 - dbdata:/var/lib/postgresql/data
 - ./fresh-init:/var/lib/postgresql/data  # ← перетрёт named volume!

Второй mount по тому же пути «накроет» первый. Данные в dbdata останутся, но контейнер их не увидит. Симптом: «БД стартует пустой, хотя volume есть». Проверка - docker compose exec db ls /var/lib/postgresql/data.

Пользовательские сети

По умолчанию compose создаёт одну сеть. Но можно разделить:

services:
  frontend:
    networks:
 - frontend-net

  backend:
    networks:
 - frontend-net
 - backend-net

  db:
    networks:
 - backend-net

networks:
  frontend-net: {}
  backend-net: {}

Здесь frontend видит backend, но не видит db напрямую. Это изоляция.

Network modes: bridge / host / none / custom

Network modes: bridge - своя подсеть, host - сеть хоста напрямую, none - изоляция

Режим      Изоляция     Когда использовать
─────      ─────────    ──────────────────────────────────────────────
bridge     полная       дефолт; почти всегда правильный выбор
host       нет          высокая производительность сети (proxy, VPN,
                        мониторинг); порты контейнера = порты хоста
none       полная       контейнер без сети (изолированная обработка
                        данных, security sandboxing)
custom     управляемая  свой DNS, фиксированные подсети, multi-host
                        через overlay (swarm)
services:
  proxy:
    network_mode: host          # nginx видит сетевой интерфейс хоста напрямую
  sandbox:
    network_mode: none          # никакой сети - даже DNS нет
В `network_mode: host` директива `ports:` игнорируется - контейнер слушает порты хоста как обычный процесс. Конфликт двух сервисов на одном порту = краш. На macOS/Windows `host` работает иначе из-за VM (Docker Desktop) - реально host только на Linux.

DNS в Docker network

Docker встроил собственный DNS на адресе 127.0.0.11. В кастомной сети сервисы резолвят друг друга по имени сервиса из compose:

// Из контейнера backend в той же сети:
db, _ := sql.Open("postgres", "host=db port=5432 user=postgres ...")
//                                    ^^ имя сервиса из docker-compose.yml

В PHP то же самое - host берётся из имени сервиса compose. Через PDO:

<?php
declare(strict_types=1);

// Подключение к Postgres внутри той же docker network
$pdo = new PDO(
    'pgsql:host=db;port=5432;dbname=app',
    //          ^^ имя сервиса из docker-compose.yml
    'postgres',
    $_SERVER['POSTGRES_PASSWORD'] ?? '',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ],
);

В Symfony связь конфигурируется через переменную окружения DATABASE_URL - Doctrine DBAL/ORM сам распарсит host из URL:

<?php
declare(strict_types=1);

use Doctrine\DBAL\DriverManager;

// DATABASE_URL=postgresql://postgres:password@db:5432/app?serverVersion=16
$conn = DriverManager::getConnection([
    'url' => $_SERVER['DATABASE_URL'],
]);
# Проверка из контейнера:
docker compose exec backend nslookup db
# Server: 127.0.0.11
# Name:   db
# Address: 172.18.0.3

Это работает только внутри той же docker network. Если frontend-net и backend-net разные - frontend не зарезолвит db.

Best practices

  • .dockerignore обязателен - даже если думаешь, что не нужен. Без него .git, node_modules, __pycache__ улетят в образ.
  • Секреты в Dockerfile через ARG оставляют след - даже multi-stage не спасает: docker history покажет аргумент. Для build-time секретов - --secret (BuildKit).
  • .env всегда в .gitignore + храни рядом .env.example с пустыми значениями как документацию.
  • Volume для БД делается один раз и навсегда - переименование (dbdatapgdata) создаёт новый пустой volume. Симптом тот же: «БД пустая после рестарта».
  • depends_on ≠ «БД готова» - он ждёт только запуск контейнера, не его сервиса. Для надёжности - healthcheck + depends_on: condition: service_healthy (тема следующего урока).

Мини-задание

  • Сделай .env.example с перечнем переменных (POSTGRES_PASSWORD, JWT_SECRET)
  • Подключи env в compose через env_file: .env
  • Добавь named volume для данных PostgreSQL
  • Проверь, что данные сохраняются после docker compose down и docker compose up
  • Проверь, что данные НЕ сохраняются после docker compose down -v

Итог

  • .env - для секретов. env_file подключает их в compose. Никогда не коммить .env.
  • Named volumes хранят данные между перезапусками. Bind mounts - для разработки.
  • Сети изолируют сервисы: frontend не должен иметь прямой доступ к БД.

Типичная ошибка

Хранить секреты в docker-compose.yml и коммитить его. Или забыть named volume для БД - и потерять все данные при docker compose down.

Мини-практика (10 минут)

  1. Создай .env с POSTGRES_PASSWORD=secret123. 2. В docker-compose.yml подключи через env_file: .env. 3. Подними postgres, создай таблицу через docker compose exec db psql -U postgres -c "CREATE TABLE test (id serial);". 4. Выполни docker compose down и docker compose up -d. 5. Проверь, что таблица на месте: docker compose exec db psql -U postgres -c "\dt". 6. Выполни docker compose down -v и повтори - таблицы нет.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.