<p><!-- META DESCRIPTION (до 155 символов): Как установить Docker Compose на Ubuntu, <a class="wpil_keyword_link" href="https://it-apteka.com/tag/debian/" target="_blank" rel="noopener" title="Debian" data-wpil-keyword-link="linked" data-wpil-monitor-id="1605">Debian</a>, CentOS и macOS. Команды, примеры docker-compose.yml, типичные ошибки и их решения от практикующего SRE. --></p>
<h1>Установка Docker Compose на Linux: от нуля до рабочего стека</h1>
"Коротко:
<br />
Docker Compose устанавливается как плагин для Docker CLI командой apt install docker-compose-plugin (Ubuntu/Debian) или через ручное скачивание бинарника с GitHub. После <a href="https://it-apteka.com/n8n-ustanovka-i-nastrojka-docker-telegram-ai-agenty/" title="n8n установка и настройка: Docker, Telegram, AI-агенты" target="_blank" rel="noopener" data-wpil-monitor-id="1606">установки проверяй командой docker</a> compose version — без дефиса. Весь процесс занимает 3-5 минут при наличии <a class="wpil_keyword_link" href="https://it-apteka.com/tag/docker/" target="_blank" rel="noopener" title="Docker" data-wpil-keyword-link="linked" data-wpil-monitor-id="1618">Docker</a> Engine.<br />
<h2>1. Диагноз: зачем вообще это нужно</h2>
<p>Поднял приложение. Nginx стоит отдельно, база — отдельно, Redis — третьей командой в другом терминале. Через неделю добавился Celery. Потом RabbitMQ. Потом ты забыл, с какими флагами запускал базу, и она поднялась без volume. Данные потеряны. Знакомо?</p>
<p>Docker Compose решает именно это. Весь стек описывается в одном YAML-файле. Одна команда — весь стек поднят. Ещё одна — убран. Никаких <a class="wpil_keyword_link" href="https://it-apteka.com/tag/bash/" target="_blank" rel="noopener" title="Bash" data-wpil-keyword-link="linked" data-wpil-monitor-id="1604">bash</a>-скриптов на 400 строк, которые никто не трогал с 2019 года и которые «просто работают.</p>
<p>Что будет в статье:</p>
<ul>
<li>Установка на Ubuntu/Debian, CentOS/RHEL, <a href="https://it-apteka.com/ssh-kljuchi-podkljuchaemsja-bez-parolja-i-ne-panikuem/" title="SSH-ключи: подключение без пароля — полный гайд для Linux, Windows и macOS" target="_blank" rel="noopener" data-wpil-monitor-id="1607">macOS и Windows</a></li>
<li>Разница между старым docker-compose и новым docker compose</li>
<li>Настройка прав без sudo</li>
<li>Три готовых примера docker-compose.yml — от простого к сложному</li>
<li>Типичные ошибки и их решения</li>
</ul>
<p>Времени нужно: 15-20 минут на <a href="https://it-apteka.com/install-asterisk-full-manual/" title="Asterisk с нуля: установка, настройка и запуск IP-телефонии на сервере" target="_blank" rel="noopener" data-wpil-monitor-id="1608">установку и первый запуск</a>. Предполагается, что Docker Engine уже стоит. Если нет — сначала туда.</p>
<h2>2. Базовые требования</h2>
<p>Перед тем как что-то ставить — проверь среду. Это не занудство, это экономия времени.</p>
<h3>Системные требования</h3>
<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse; width:100%;">
<thead>
<tr>
<th>Компонент</th>
<th>Минимум</th>
<th>Рекомендуется</th>
</tr>
</thead>
<tbody>
<tr>
<td>Docker Engine</td>
<td>19.03.0+</td>
<td>последняя стабильная</td>
</tr>
<tr>
<td>ОС</td>
<td>64-bit Linux/macOS/<a class="wpil_keyword_link" href="https://it-apteka.com/category/windows-server/" target="_blank" rel="noopener" title="Windows Server" data-wpil-keyword-link="linked" data-wpil-monitor-id="1619">Windows</a></td>
<td>Ubuntu 22.04 LTS</td>
</tr>
<tr>
<td>RAM</td>
<td>2 ГБ</td>
<td>4+ ГБ</td>
</tr>
<tr>
<td>Диск</td>
<td>2 ГБ свободно</td>
<td>20+ ГБ</td>
</tr>
<tr>
<td>Права</td>
<td>sudo</td>
<td>пользователь в группе docker</td>
</tr>
</tbody>
</table>
<h3>Проверка Docker Engine</h3>
<p>Сначала убедись, что Docker Engine работает:</p>
<pre><code class="language-bash">
docker --version
docker ps
sudo systemctl status docker
</code></pre>
<p>Должно быть: версия Docker и статус «active (running)». Если Docker не запущен:</p>
<pre><code class="language-bash">
sudo systemctl start docker
sudo systemctl enable docker
</code></pre>
<p>Команда enable нужна для автозапуска при загрузке системы. Без неё после ребута будешь гадать, почему контейнеры не поднялись.</p>
<h3>Совместимость версий</h3>
<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse; width:100%;">
<thead>
<tr>
<th>ОС</th>
<th>Версия</th>
<th>Метод установки</th>
<th>Compose версия</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ubuntu</td>
<td>20.04, 22.04, 24.04</td>
<td>apt plugin</td>
<td>v2.x</td>
</tr>
<tr>
<td>Debian</td>
<td>11, 12</td>
<td>apt plugin</td>
<td>v2.x</td>
</tr>
<tr>
<td>CentOS/RHEL</td>
<td>8, 9</td>
<td>dnf plugin</td>
<td>v2.x</td>
</tr>
<tr>
<td>Fedora</td>
<td>38+</td>
<td>dnf plugin</td>
<td>v2.x</td>
</tr>
<tr>
<td>macOS</td>
<td>12+</td>
<td>Docker Desktop / brew</td>
<td>v2.x</td>
</tr>
<tr>
<td>Windows</td>
<td>10/11 + WSL2</td>
<td>Docker Desktop</td>
<td>v2.x</td>
</tr>
</tbody>
</table>
<p>На момент публикации актуальна версия Compose v2.27+. Перед установкой проверяй свежие релизы на GitHub.</p>
<h2>3. Архитектура: как это работает</h2>
<pre class="mermaid">
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["docker compose up"] --> B["Docker CLI Plugin"]
B --> C["docker-compose.yml"]
C --> D["Service: web"]
C --> E["Service: db"]
C --> F["Service: redis"]
D --> G["Container: nginx"]
E --> H["Container: postgres"]
F --> I["Container: redis"]
G --> J["Network: app-network"]
H --> J
I --> J
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style C fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
style J fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
</pre>
<p>Docker Compose читает твой YAML, создаёт контейнеры, настраивает сеть между ними и запускает всё в нужном порядке. Контейнеры видят друг друга по именам сервисов — db, redis, web — без hardcode IP-адресов.</p>
<h2>4. Установка: выбери свою ОС</h2>
<h3>Ubuntu / Debian — способ 1: через apt (рекомендую)</h3>
<p>Если Docker ставился через официальный репозиторий — плагин уже может быть установлен. Проверь:</p>
<pre><code class="language-bash">
docker compose version
</code></pre>
<p>Видишь версию — можно пропускать. Нет — ставим:</p>
<pre><code class="language-bash">
sudo apt update
sudo apt install docker-compose-plugin
docker compose version
</code></pre>
<p>Должно вывести что-то вроде: Docker Compose version v2.27.1</p>
<h3>Ubuntu / Debian — способ 2: ручная установка</h3>
<p>Нужна конкретная версия или репозиторий недоступен — берём бинарник напрямую:</p>
<pre><code class="language-bash">
mkdir -p ~/.docker/cli-plugins/
DOCKER_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d'"' -f4)
curl -SL "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" \
-o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker compose version
</code></pre>
<p>Скрипт сам определяет последнюю версию через GitHub API. Не нужно лезть на сайт руками.</p>
<h3>CentOS / RHEL / Fedora</h3>
<pre><code class="language-bash">
sudo dnf update
sudo dnf install docker-compose-plugin
docker compose version
</code></pre>
<p>Для старых версий CentOS 7 через прямое скачивание:</p>
<pre><code class="language-bash">
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" \
-o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
docker-compose --version
</code></pre>
<h3>macOS</h3>
<p><a href="https://it-apteka.com/tag/docker-compose/" title="docker compose" target="_blank" rel="noopener" data-wpil-monitor-id="1609">Docker Desktop для Mac уже включает Compose</a>. Поставил Desktop — Compose есть. Если нужна отдельная установка через Homebrew:</p>
<pre><code class="language-bash">
brew install docker-compose
</code></pre>
<p>Для Apple Silicon (M1/M2/M3) — отдельный бинарник:</p>
<pre><code class="language-bash">
mkdir -p ~/.docker/cli-plugins
curl -SL "https://github.com/docker/compose/releases/latest/download/docker-compose-darwin-aarch64" \
-o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker compose version
</code></pre>
<h3>Windows</h3>
<p><a href="https://it-apteka.com/1243-2/" title="Сервер активации Windows (KMS) в Docker: варианты развёртывания и интеграция с Active Directory" target="_blank" rel="noopener" data-wpil-monitor-id="1610">Docker Desktop для Windows</a> включает Compose из коробки. Если нужна ручная установка через <a class="wpil_keyword_link" href="https://it-apteka.com/tag/powershell/" target="_blank" rel="noopener" title="PowerShell" data-wpil-keyword-link="linked" data-wpil-monitor-id="1601">PowerShell</a>:</p>
<pre><code class="language-powershell">
mkdir -p "$env:USERPROFILE\.docker\cli-plugins"
Invoke-WebRequest "https://github.com/docker/compose/releases/latest/download/docker-compose-windows-x86_64.exe" `
-OutFile "$env:USERPROFILE\.docker\cli-plugins\docker-compose.exe"
docker compose version
</code></pre>
<p>Если используешь WSL2 — рекомендую. Устанавливай Compose внутри WSL по инструкции для Ubuntu.</p>
<h2>5. Настройка прав — убираем sudo</h2>
<p>По умолчанию Docker требует sudo. Это безопасно, но неудобно. Добавляем пользователя в группу docker:</p>
<pre><code class="language-bash">
sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
docker ps
</code></pre>
"Важно
<br />
Пользователи в группе docker фактически имеют root-доступ к системе через монтирование томов. Не добавляй туда всех подряд. В продакшене — лучше настроить sudoers с конкретными разрешёнными командами.<br />
<p>Если не хочешь добавлять пользователя в группу — создай алиасы:</p>
<pre><code class="language-bash">
echo "alias dc='sudo docker compose'" >> ~/.bashrc
source ~/.bashrc
dc up -d
</code></pre>
<h2>6. Структура docker-compose.yml</h2>
<p>Прежде чем запускать примеры — понимай, что читаешь. Базовая структура файла:</p>
<pre><code class="language-text">
version: '3.8'
services:
имя-сервиса:
image: образ:тег
ports:
- "порт-хоста:порт-контейнера"
volumes:
- путь-хоста:путь-контейнера
environment:
- ПЕРЕМЕННАЯ=значение
depends_on:
- другой-сервис
restart: unless-stopped
networks:
имя-сети:
driver: bridge
volumes:
имя-тома:
driver: local
</code></pre>
<p>Ключевые секции: services — обязательна, networks и volumes — опциональны. В services описываешь каждый контейнер. В networks — как они между собой общаются. В volumes — где хранятся данные.</p>
<p>Один критический момент: YAML использует пробелы, не табы. Два пробела на уровень. Перепутаешь — получишь синтаксическую ошибку и полчаса дебага.</p>
<h2>7. Три практических примера</h2>
<h3>Пример 1: Nginx со статикой</h3>
<p>Простейший старт. Создай папку проекта и файл docker-compose.yml:</p>
<pre><code class="language-text">
version: '3.8'
services:
web:
image: nginx:alpine
container_name: my-nginx
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
restart: unless-stopped
</code></pre>
<p>Создай папку html и тестовый файл:</p>
<pre><code class="language-bash">
mkdir html
echo '<h1>Docker Compose работает</h1>' > html/index.html
docker compose up -d
curl http://localhost:8080
</code></pre>
<p>Результат: HTML-ответ в терминале и страница в браузере по адресу localhost:8080. Флаг -d запускает в фоне.</p>
<h3>Пример 2: WordPress + MySQL</h3>
<p>Классика жанра. Два сервиса с зависимостью и персистентными данными:</p>
<pre><code class="language-text">
version: '3.8'
services:
db:
image: mysql:8.0
container_name: wp-db
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
restart: unless-stopped
networks:
- wp-net
wordpress:
depends_on:
- db
image: wordpress:latest
container_name: wp-app
ports:
- "8000:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
restart: unless-stopped
networks:
- wp-net
networks:
wp-net:
driver: bridge
volumes:
db_data:
wp_data:
</code></pre>
<p>Создай файл .env рядом с docker-compose.yml:</p>
<pre><code class="language-bash">
MYSQL_ROOT_PASSWORD=strong_root_pass_here
MYSQL_PASSWORD=strong_wp_pass_here
</code></pre>
<p>Добавь .env в .gitignore — обязательно. Пароли в публичных репозиториях это не инцидент, это катастрофа.</p>
<pre><code class="language-bash">
echo ".env" >> .gitignore
docker compose up -d
docker compose ps
</code></pre>
<p>Через 30-40 секунд (пока <a class="wpil_keyword_link" href="https://it-apteka.com/tag/mysql/" target="_blank" rel="noopener" title="MySQL" data-wpil-keyword-link="linked" data-wpil-monitor-id="1603">MySQL</a> инициализируется) открывай localhost:8000 — будет мастер установки WordPress. Данные сохраняются в именованных volumes и переживут пересоздание контейнеров.</p>
<h3>Пример 3: Fullstack — React + Node.js + PostgreSQL + Redis</h3>
<p>Реальный рабочий стек с healthcheck-ами и правильными зависимостями:</p>
<pre class="mermaid">
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Nginx :80"] --> B["Frontend :3000"]
A --> C["Backend :3001"]
C --> D["PostgreSQL :5432"]
C --> E["Redis :6379"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style C fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
style E fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
</pre>
<pre><code class="language-text">
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: app-postgres
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: appdb
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: app-redis
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- app-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
backend:
build:
context: ./backend
container_name: app-backend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
NODE_ENV: production
DATABASE_URL: postgresql://appuser:${DB_PASSWORD}@postgres:5432/appdb
REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET}
ports:
- "3001:3000"
networks:
- app-net
restart: unless-stopped
frontend:
build:
context: ./frontend
container_name: app-frontend
depends_on:
- backend
ports:
- "3000:80"
networks:
- app-net
restart: unless-stopped
nginx:
image: nginx:alpine
container_name: app-nginx
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
depends_on:
- frontend
- backend
networks:
- app-net
restart: unless-stopped
networks:
app-net:
driver: bridge
volumes:
postgres_data:
redis_data:
</code></pre>
<p>Вот тут важно: condition: service_healthy в depends_on. Без healthcheck сервис запускается как только контейнер стартовал — но база данных ещё может быть не готова принимать подключения. С healthcheck backend ждёт <a href="https://it-apteka.com/rukovodstvo-po-optimizacii-postgresql-i-mysql-5-realnyh-primerov-s-gotovymi-skriptami/" title="Руководство по оптимизации PostgreSQL и MySQL: 5 реальных примеров с готовыми скриптами" target="_blank" rel="noopener" data-wpil-monitor-id="1611">реальной готовности PostgreSQL</a>. Это решает 90% проблем с «база не успела подняться».</p>
<pre><code class="language-bash">
docker compose up -d --build
docker compose ps
docker compose logs -f backend
</code></pre>
<h2>8. Порты сервисов</h2>
<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse; width:100%;">
<thead>
<tr>
<th>Сервис</th>
<th>Порт контейнера</th>
<th>Порт хоста (пример)</th>
<th>Протокол</th>
</tr>
</thead>
<tbody>
<tr>
<td>Nginx</td>
<td>80, 443</td>
<td>80, 443</td>
<td>HTTP/HTTPS</td>
</tr>
<tr>
<td>PostgreSQL</td>
<td>5432</td>
<td>5432</td>
<td>TCP</td>
</tr>
<tr>
<td>MySQL/MariaDB</td>
<td>3306</td>
<td>3306</td>
<td>TCP</td>
</tr>
<tr>
<td>Redis</td>
<td>6379</td>
<td>6379</td>
<td>TCP</td>
</tr>
<tr>
<td>Node.js</td>
<td>3000</td>
<td>3001</td>
<td>HTTP</td>
</tr>
<tr>
<td>Flask/Django</td>
<td>5000 / 8000</td>
<td>5000 / 8000</td>
<td>HTTP</td>
</tr>
<tr>
<td>Prometheus</td>
<td>9090</td>
<td>9090</td>
<td>HTTP</td>
</tr>
<tr>
<td>Grafana</td>
<td>3000</td>
<td>3000</td>
<td>HTTP</td>
</tr>
</tbody>
</table>
<h2>9. Проверка установки и работы</h2>
<p>После установки Compose и первого запуска — проверяй так:</p>
<pre><code class="language-bash">
# Версия Compose
docker compose version
# Статус всех сервисов
docker compose ps
# Логи в реальном времени
docker compose logs -f
# Логи конкретного сервиса
docker compose logs -f web
# Ресурсы контейнеров
docker stats --no-stream
# Проверка конфига на ошибки
docker compose config
</code></pre>
<p><a href="https://it-apteka.com/docker-compose-ustanovka-komandy-i-nastrojka-kontejnerov/" title="Docker Compose — установка, команды и настройка контейнеров" target="_blank" rel="noopener" data-wpil-monitor-id="1612">Команда docker compose</a> config особенно полезна — она парсит твой YAML, подставляет переменные из .env и показывает итоговый конфиг. Если там ошибка синтаксиса — найдёт сразу.</p>
<h2>10. Безопасность</h2>
<p>Несколько правил, которые обязательны в продакшене.</p>
<h3>Файрвол</h3>
<pre><code class="language-bash">
# UFW - разреши только нужные порты
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
</code></pre>
"Внимание:
<br />
Docker по умолчанию обходит правила UFW через прямые правила iptables. Если пробросил порт в docker-compose.yml — он открыт снаружи, даже если UFW говорит иначе. Чтобы это исправить, используй 127.0.0.1:8080:80 вместо просто 8080:80 для портов, которые не должны быть публичными.<br />
<h3>Непривилегированный пользователь в контейнере</h3>
<pre><code class="language-text">
# В Dockerfile
RUN addgroup -g 1000 appgroup && \
adduser -D -u 1000 -G appgroup appuser
USER appuser
# В docker-compose.yml
services:
app:
user: "1000:1000"
</code></pre>
<h3>Ротация логов</h3>
<pre><code class="language-text">
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
</code></pre>
<p>Без ротации логи заполнят диск. Это случается быстрее, чем думаешь.</p>
<h3>Лимиты ресурсов</h3>
<pre><code class="language-text">
services:
app:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
</code></pre>
<h2>11. Резервное копирование</h2>
<p>Данные в named volumes переживают пересоздание контейнеров. Но не пересоздание volumes.</p>
<pre><code class="language-bash">
# Бэкап PostgreSQL
docker compose exec db pg_dump -U appuser appdb > backup_$(date +%Y%m%d_%H%M%S).sql
# Бэкап MySQL
docker compose exec db mysqldump -u wpuser -p${MYSQL_PASSWORD} wordpress > backup_$(date +%Y%m%d_%H%M%S).sql
# Бэкап volume целиком
docker run --rm \
-v имя_проекта_volume:/data \
-v $(pwd):/backup \
alpine tar czf /backup/volume_backup.tar.gz -C /data .
# Восстановление PostgreSQL
docker compose exec -T db psql -U appuser appdb < backup.sql
</code></pre>
<p>Что бэкапить: volumes с данными баз, volumes с файлами приложения, сам docker-compose.yml и .env.example (не .env с паролями). Частота: для баз - ежедневно минимум. Для файлов приложения - при каждом деплое. Хранить: не на том же сервере.</p>
<h2>12. Обновление Docker Compose</h2>
<pre><code class="language-bash">
# Через apt
sudo apt update && sudo apt upgrade docker-compose-plugin
# Проверка текущей версии
docker compose version
# Ручное обновление бинарника
DOCKER_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d'"' -f4)
curl -SL "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" \
-o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker compose version
</code></pre>
<p>Перед обновлением: проверь changelog на предмет breaking changes. Сделай бэкап данных. Протестируй на staging. Compose v2 обратно совместим с синтаксисом v1 файлов, но иногда бывают нюансы.</p>
<h2>13. Миграция с docker-compose v1 на v2</h2>
<p>Если ещё используешь старый docker-compose с дефисом - переходи. Он больше не получает обновлений.</p>
<p>Основное изменение - команды:</p>
<pre><code class="language-bash">
# Старый синтаксис (v1)
docker-compose up -d
docker-compose logs
docker-compose down
# Новый синтаксис (v2)
docker compose up -d
docker compose logs
docker compose down
</code></pre>
<p>Для плавного перехода создай алиас:</p>
<pre><code class="language-bash">
echo "alias docker-compose='docker compose'" >> ~/.bashrc
source ~/.bashrc
</code></pre>
<p>Конфиги docker-compose.yml полностью совместимы. Менять файлы не нужно. Проверь конфиг на совместимость:</p>
<pre><code class="language-bash">
docker compose config
</code></pre>
<p>Если команда выполнилась без ошибок и показала итоговый конфиг - всё в порядке.</p>
<h2>14. Шпаргалка по командам</h2>
<pre><code class="language-bash">
# Запуск
docker compose up -d # фоновый режим
docker compose up --build # с пересборкой образов
docker compose up --force-recreate # пересоздать контейнеры
# Остановка
docker compose down # остановить и удалить контейнеры
docker compose down -v # + удалить volumes (осторожно!)
docker compose stop # только остановить
# Информация
docker compose ps # статус сервисов
docker compose ps -a # включая остановленные
docker compose logs -f # логи всех сервисов
docker compose logs --tail=100 web # последние 100 строк
docker compose top # процессы в контейнерах
docker compose stats # использование ресурсов
# Выполнение команд
docker compose exec web bash # интерактивная оболочка
docker compose exec web ls -la # выполнить команду
docker compose run --rm web python test.py # одноразовый контейнер
# Сборка
docker compose build --no-cache # пересборка без кэша
docker compose pull # обновить образы
docker compose config # проверить конфиг
# Масштабирование
docker compose up -d --scale web=3 # 3 инстанса web
</code></pre>
<h2>15. Осложнения: типичные ошибки и решения</h2>
<h3>command not found: docker compose</h3>
<p>Причина: плагин не установлен или не в PATH.</p>
<pre><code class="language-bash">
# Проверяем где плагины
ls ~/.docker/cli-plugins/
ls /usr/lib/docker/cli-plugins/
# Устанавливаем заново
sudo apt install docker-compose-plugin
# Проверяем
docker compose version
</code></pre>
<h3>permission denied при запуске</h3>
<p>Причина: пользователь не в группе docker.</p>
<pre><code class="language-bash">
# Проверяем группы текущего пользователя
groups $USER
# Добавляем в группу docker
sudo usermod -aG docker $USER
# Применяем без перелогина
newgrp docker
# Проверяем
docker ps
</code></pre>
<h3>Cannot connect to Docker daemon</h3>
<p>Причина: Docker Engine не запущен.</p>
<pre><code class="language-bash">
sudo systemctl status docker
sudo systemctl start docker
sudo systemctl enable docker
</code></pre>
<h3>Контейнер постоянно рестартует</h3>
<p>Причина: ошибка в приложении или неправильный healthcheck.</p>
<pre><code class="language-bash">
# Смотрим логи падающего сервиса
docker compose logs --tail=50 имя-сервиса
# Смотрим статус с причиной
docker compose ps
# Запускаем без restart для дебага
docker compose run --rm имя-сервиса
</code></pre>
<h3>depends_on не помогает - сервис стартует раньше базы</h3>
<p>Причина: depends_on без condition только ждёт старта контейнера, не готовности сервиса внутри.</p>
<pre><code class="language-text">
# Неправильно - просто ждёт старта контейнера
depends_on:
- db
# Правильно - ждёт healthcheck
depends_on:
db:
condition: service_healthy
</code></pre>
<p>Плюс добавь healthcheck в сервис db (пример для PostgreSQL выше в статье).</p>
<h3>Порты заняты</h3>
<p>Причина: на хосте уже что-то слушает этот порт.</p>
<pre><code class="language-bash">
# Найти что занимает порт
sudo ss -tlnp | grep :8080
sudo lsof -i :8080
# Убить процесс или поменять порт в compose
ports:
- "8081:80" # поменяли 8080 на 8081
</code></pre>
<h3>Volume не монтируется - файлы не видны</h3>
<p>Причина: неправильный путь или проблемы с правами.</p>
<pre><code class="language-bash">
# Проверяем абсолютный путь
pwd
ls -la ./html
# Смотрим что примонтировано в контейнере
docker compose exec web ls -la /usr/share/nginx/html
# Проверяем права
ls -la ./html/index.html
chmod 644 ./html/index.html
</code></pre>
<h3>Ошибка синтаксиса YAML</h3>
<p>Причина: табы вместо пробелов или неправильные отступы.</p>
<pre><code class="language-bash">
# Проверяем конфиг
docker compose config
# Онлайн-валидатор: yamllint.com
# Или через CLI
sudo apt install yamllint
yamllint docker-compose.yml
</code></pre>
<h2>16. Альтернативные решения</h2>
<p>Docker Compose - не единственный вариант. Для понимания картины:</p>
<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse; width:100%;">
<thead>
<tr>
<th>Инструмент</th>
<th>Когда использовать</th>
<th>Сложность</th>
</tr>
</thead>
<tbody>
<tr>
<td>Docker Compose</td>
<td>1 сервер, dev/staging/простой prod</td>
<td>низкая</td>
</tr>
<tr>
<td>Docker Swarm</td>
<td>несколько серверов, знакомый синтаксис Compose</td>
<td>средняя</td>
</tr>
<tr>
<td>Kubernetes</td>
<td>большой кластер, автомасштабирование, сложные требования</td>
<td>высокая</td>
</tr>
<tr>
<td>Podman Compose</td>
<td>нужен rootless по умолчанию, RHEL-среда</td>
<td>низкая</td>
</tr>
<tr>
<td>Nomad</td>
<td>гетерогенная инфраструктура, не только контейнеры</td>
<td>средняя</td>
</tr>
</tbody>
</table>
<p>Kubernetes - мощный инструмент. Но его сложность оправдана не всегда. Если Compose покрывает твои задачи - используй Compose. Я знаю продакшен-системы с миллионами запросов в сутки на Docker Compose. Всё нормально работает.</p>
<h2>17. Профилактика: как не сломать</h2>
<p>Несколько правил, которые сэкономят тебе нервы в будущем.</p>
<p>Первое - всегда указывай конкретный тег образа в продакшене. latest сегодня и latest через три месяца - это разные образы. Обновления без тестирования прилетают в самый неподходящий момент.</p>
<p>Второе - именованные volumes для данных. Не bind-mount к случайной папке, а нормальный named volume. Данные должны жить дольше контейнеров.</p>
<p>Третье - .env.example в репозитории, .env в .gitignore. Всегда. Без исключений. Одна утечка пароля к базе данных в публичный репозиторий - и ты потратишь день на смену всех учёток и проверку логов.</p>
<p>Четвёртое - настрой ротацию логов. Диск заполняется быстрее, чем думаешь, особенно если приложение многословное.</p>
<p>Пятое - проверяй конфиг перед деплоем командой docker compose config. Поймаешь синтаксические ошибки раньше, чем они проявятся в продакшене в три часа ночи.</p>
<p>Шестое - документируй нестандартные решения прямо в комментариях docker-compose.yml. Ты не запомнишь через полгода, почему именно так.</p>
<pre><code class="language-text">
# Плохо - загадочная конфигурация
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
# Хорошо - понятно зачем
# appendonly yes - WAL для надёжности данных
# maxmemory 256mb - ограничение памяти для кэша
# allkeys-lru - вытеснение редко используемых ключей при достижении лимита
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
</code></pre>
<h2>18. FAQ</h2>
<h3>Почему docker compose вместо docker-compose?</h3>
<p>В версии 2.x Docker Compose переехал из отдельной утилиты в плагин Docker CLI. <a href="https://it-apteka.com/docker-shpargalka-i-primery/" title="Команды Docker: шпаргалка с примерами для сервера и продакшена" target="_blank" rel="noopener" data-wpil-monitor-id="2229">Команда стала docker</a> compose (без дефиса). Старый docker-compose (с дефисом, версия 1.x) устарел и больше не получает обновлений. Создай алиас alias docker-compose='docker compose' если привык к старому синтаксису - файлы конфигурации совместимы.</p>
<h3>Как проверить, что Docker Compose установлен правильно?</h3>
<pre><code class="language-bash">
# Проверка версии
docker compose version
# Проверка работоспособности - запускаем тестовый контейнер
docker compose -f - up -d <<EOF
version: '3'
services:
test:
image: hello-world
EOF
# Смотрим логи
docker compose logs test
# Убираем
docker compose down
</code></pre>
<h3>Что если сервисы поднялись, но не видят друг друга по сети?</h3>
<p>Скорее всего, они в разных сетях или сеть не указана. Проверь:</p>
<pre><code class="language-bash">
# Какие сети созданы
docker network ls | grep название_проекта
# К каким сетям подключён контейнер
docker inspect имя-контейнера | grep -A 20 '"Networks"'
# Проверь ping между контейнерами
docker compose exec web ping db
</code></pre>
<p>Если сервисы в одном docker-compose.yml и сеть не указана явно - Compose создаёт default сеть и подключает все сервисы к ней автоматически. Проблема возникает когда ты явно указал networks для одних сервисов, но забыл про другие.</p>
<h3>Как откатиться если обновление сломало приложение?</h3>
<pre><code class="language-bash">
# Остановить новую версию
docker compose down
# Откатить образ в docker-compose.yml к предыдущей версии
# image: myapp:v1.2.3 (было v1.2.4)
# Поднять предыдущую версию
docker compose up -d
# Если нет конкретных тегов - ищи предыдущий образ локально
docker images myapp
docker tag myapp:old-hash myapp:rollback
</code></pre>
<p>Именно поэтому в продакшене никогда не используй latest - невозможно откатиться к "прошлому latest".</p>
<h3>Почему данные исчезают после docker compose down -v?</h3>
<p>Флаг -v удаляет volumes вместе с контейнерами. Это деструктивная операция. Используй docker compose down без флага -v для сохранения данных. Volumes удаляются только явно: через docker compose down -v или docker volume rm.</p>
<h2>19. Итог</h2>
<p>Docker Compose установлен. Первый стек поднят. Теперь у тебя есть инструмент, который описывает инфраструктуру как код, запускает всё одной командой и сохраняет данные между перезапусками.</p>
<p>Следующий шаг - добавь <a class="wpil_keyword_link" href="https://it-apteka.com/category/monitoring/" target="_blank" rel="noopener" title="Мониторинг" data-wpil-keyword-link="linked" data-wpil-monitor-id="1602">мониторинг</a>. Prometheus + Grafana + Node Exporter поднимаются за 10 минут через Compose и дают полную картину того, что происходит с твоими контейнерами. Без мониторинга ты узнаёшь о проблемах от пользователей.</p>
"Не
<br />
Если что-то пошло не так — смотри секцию "Осложнения" выше. Если не помогло — пиши в комментарии: версию ОС, версию Docker, текст ошибки из docker compose logs. Разберём конкретную ситуацию.<br />
<!-- META DESCRIPTION (до 155 символов): Как установить Docker Compose на Ubuntu, Debian, CentOS и macOS. Команды, примеры docker-compose.yml, типичные ошибки и их решения от практикующего SRE. -->
Установка Docker Compose на Linux: от нуля до рабочего стека
Коротко: что получишь на выходе
Docker Compose устанавливается как плагин для Docker CLI командой apt install docker-compose-plugin (Ubuntu/Debian) или через ручное скачивание бинарника с GitHub. После
установки проверяй командой docker compose version — без дефиса. Весь процесс занимает 3-5 минут при наличии
Docker Engine.
1. Диагноз: зачем вообще это нужно
Поднял приложение. Nginx стоит отдельно, база — отдельно, Redis — третьей командой в другом терминале. Через неделю добавился Celery. Потом RabbitMQ. Потом ты забыл, с какими флагами запускал базу, и она поднялась без volume. Данные потеряны. Знакомо?
Docker Compose решает именно это. Весь стек описывается в одном YAML-файле. Одна команда — весь стек поднят. Ещё одна — убран. Никаких bash-скриптов на 400 строк, которые никто не трогал с 2019 года и которые «просто работают.
Что будет в статье:
- Установка на Ubuntu/Debian, CentOS/RHEL, macOS и Windows
- Разница между старым docker-compose и новым docker compose
- Настройка прав без sudo
- Три готовых примера docker-compose.yml — от простого к сложному
- Типичные ошибки и их решения
Времени нужно: 15-20 минут на установку и первый запуск. Предполагается, что Docker Engine уже стоит. Если нет — сначала туда.
2. Базовые требования
Перед тем как что-то ставить — проверь среду. Это не занудство, это экономия времени.
Системные требования
| Компонент |
Минимум |
Рекомендуется |
| Docker Engine |
19.03.0+ |
последняя стабильная |
| ОС |
64-bit Linux/macOS/Windows |
Ubuntu 22.04 LTS |
| RAM |
2 ГБ |
4+ ГБ |
| Диск |
2 ГБ свободно |
20+ ГБ |
| Права |
sudo |
пользователь в группе docker |
Проверка Docker Engine
Сначала убедись, что Docker Engine работает:
docker --version
docker ps
sudo systemctl status docker
Должно быть: версия Docker и статус «active (running)». Если Docker не запущен:
sudo systemctl start docker
sudo systemctl enable docker
Команда enable нужна для автозапуска при загрузке системы. Без неё после ребута будешь гадать, почему контейнеры не поднялись.
Совместимость версий
| ОС |
Версия |
Метод установки |
Compose версия |
| Ubuntu |
20.04, 22.04, 24.04 |
apt plugin |
v2.x |
| Debian |
11, 12 |
apt plugin |
v2.x |
| CentOS/RHEL |
8, 9 |
dnf plugin |
v2.x |
| Fedora |
38+ |
dnf plugin |
v2.x |
| macOS |
12+ |
Docker Desktop / brew |
v2.x |
| Windows |
10/11 + WSL2 |
Docker Desktop |
v2.x |
На момент публикации актуальна версия Compose v2.27+. Перед установкой проверяй свежие релизы на GitHub.
3. Архитектура: как это работает
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["docker compose up"] --> B["Docker CLI Plugin"]
B --> C["docker-compose.yml"]
C --> D["Service: web"]
C --> E["Service: db"]
C --> F["Service: redis"]
D --> G["Container: nginx"]
E --> H["Container: postgres"]
F --> I["Container: redis"]
G --> J["Network: app-network"]
H --> J
I --> J
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style C fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
style J fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
Docker Compose читает твой YAML, создаёт контейнеры, настраивает сеть между ними и запускает всё в нужном порядке. Контейнеры видят друг друга по именам сервисов — db, redis, web — без hardcode IP-адресов.
4. Установка: выбери свою ОС
Ubuntu / Debian — способ 1: через apt (рекомендую)
Если Docker ставился через официальный репозиторий — плагин уже может быть установлен. Проверь:
docker compose version
Видишь версию — можно пропускать. Нет — ставим:
sudo apt update
sudo apt install docker-compose-plugin
docker compose version
Должно вывести что-то вроде: Docker Compose version v2.27.1
Ubuntu / Debian — способ 2: ручная установка
Нужна конкретная версия или репозиторий недоступен — берём бинарник напрямую:
mkdir -p ~/.docker/cli-plugins/
DOCKER_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d'"' -f4)
curl -SL "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" \
-o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker compose version
Скрипт сам определяет последнюю версию через GitHub API. Не нужно лезть на сайт руками.
CentOS / RHEL / Fedora
sudo dnf update
sudo dnf install docker-compose-plugin
docker compose version
Для старых версий CentOS 7 через прямое скачивание:
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" \
-o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
docker-compose --version
macOS
Docker Desktop для Mac уже включает Compose. Поставил Desktop — Compose есть. Если нужна отдельная установка через Homebrew:
brew install docker-compose
Для Apple Silicon (M1/M2/M3) — отдельный бинарник:
mkdir -p ~/.docker/cli-plugins
curl -SL "https://github.com/docker/compose/releases/latest/download/docker-compose-darwin-aarch64" \
-o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker compose version
Windows
Docker Desktop для Windows включает Compose из коробки. Если нужна ручная установка через PowerShell:
mkdir -p "$env:USERPROFILE\.docker\cli-plugins"
Invoke-WebRequest "https://github.com/docker/compose/releases/latest/download/docker-compose-windows-x86_64.exe" `
-OutFile "$env:USERPROFILE\.docker\cli-plugins\docker-compose.exe"
docker compose version
Если используешь WSL2 — рекомендую. Устанавливай Compose внутри WSL по инструкции для Ubuntu.
5. Настройка прав — убираем sudo
По умолчанию Docker требует sudo. Это безопасно, но неудобно. Добавляем пользователя в группу docker:
sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
docker ps
Важно про безопасность
Пользователи в группе docker фактически имеют root-доступ к системе через монтирование томов. Не добавляй туда всех подряд. В продакшене — лучше настроить sudoers с конкретными разрешёнными командами.
Если не хочешь добавлять пользователя в группу — создай алиасы:
echo "alias dc='sudo docker compose'" >> ~/.bashrc
source ~/.bashrc
dc up -d
6. Структура docker-compose.yml
Прежде чем запускать примеры — понимай, что читаешь. Базовая структура файла:
version: '3.8'
services:
имя-сервиса:
image: образ:тег
ports:
- "порт-хоста:порт-контейнера"
volumes:
- путь-хоста:путь-контейнера
environment:
- ПЕРЕМЕННАЯ=значение
depends_on:
- другой-сервис
restart: unless-stopped
networks:
имя-сети:
driver: bridge
volumes:
имя-тома:
driver: local
Ключевые секции: services — обязательна, networks и volumes — опциональны. В services описываешь каждый контейнер. В networks — как они между собой общаются. В volumes — где хранятся данные.
Один критический момент: YAML использует пробелы, не табы. Два пробела на уровень. Перепутаешь — получишь синтаксическую ошибку и полчаса дебага.
7. Три практических примера
Пример 1: Nginx со статикой
Простейший старт. Создай папку проекта и файл docker-compose.yml:
version: '3.8'
services:
web:
image: nginx:alpine
container_name: my-nginx
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
restart: unless-stopped
Создай папку html и тестовый файл:
mkdir html
echo 'Docker Compose работает
' > html/index.html
docker compose up -d
curl http://localhost:8080
Результат: HTML-ответ в терминале и страница в браузере по адресу localhost:8080. Флаг -d запускает в фоне.
Пример 2: WordPress + MySQL
Классика жанра. Два сервиса с зависимостью и персистентными данными:
version: '3.8'
services:
db:
image: mysql:8.0
container_name: wp-db
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
restart: unless-stopped
networks:
- wp-net
wordpress:
depends_on:
- db
image: wordpress:latest
container_name: wp-app
ports:
- "8000:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
restart: unless-stopped
networks:
- wp-net
networks:
wp-net:
driver: bridge
volumes:
db_data:
wp_data:
Создай файл .env рядом с docker-compose.yml:
MYSQL_ROOT_PASSWORD=strong_root_pass_here
MYSQL_PASSWORD=strong_wp_pass_here
Добавь .env в .gitignore — обязательно. Пароли в публичных репозиториях это не инцидент, это катастрофа.
echo ".env" >> .gitignore
docker compose up -d
docker compose ps
Через 30-40 секунд (пока MySQL инициализируется) открывай localhost:8000 — будет мастер установки WordPress. Данные сохраняются в именованных volumes и переживут пересоздание контейнеров.
Пример 3: Fullstack — React + Node.js + PostgreSQL + Redis
Реальный рабочий стек с healthcheck-ами и правильными зависимостями:
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Nginx :80"] --> B["Frontend :3000"]
A --> C["Backend :3001"]
C --> D["PostgreSQL :5432"]
C --> E["Redis :6379"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style C fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
style E fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: app-postgres
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: appdb
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: app-redis
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- app-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
backend:
build:
context: ./backend
container_name: app-backend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
NODE_ENV: production
DATABASE_URL: postgresql://appuser:${DB_PASSWORD}@postgres:5432/appdb
REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET}
ports:
- "3001:3000"
networks:
- app-net
restart: unless-stopped
frontend:
build:
context: ./frontend
container_name: app-frontend
depends_on:
- backend
ports:
- "3000:80"
networks:
- app-net
restart: unless-stopped
nginx:
image: nginx:alpine
container_name: app-nginx
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
depends_on:
- frontend
- backend
networks:
- app-net
restart: unless-stopped
networks:
app-net:
driver: bridge
volumes:
postgres_data:
redis_data:
Вот тут важно: condition: service_healthy в depends_on. Без healthcheck сервис запускается как только контейнер стартовал — но база данных ещё может быть не готова принимать подключения. С healthcheck backend ждёт реальной готовности PostgreSQL. Это решает 90% проблем с «база не успела подняться».
docker compose up -d --build
docker compose ps
docker compose logs -f backend
8. Порты сервисов
| Сервис |
Порт контейнера |
Порт хоста (пример) |
Протокол |
| Nginx |
80, 443 |
80, 443 |
HTTP/HTTPS |
| PostgreSQL |
5432 |
5432 |
TCP |
| MySQL/MariaDB |
3306 |
3306 |
TCP |
| Redis |
6379 |
6379 |
TCP |
| Node.js |
3000 |
3001 |
HTTP |
| Flask/Django |
5000 / 8000 |
5000 / 8000 |
HTTP |
| Prometheus |
9090 |
9090 |
HTTP |
| Grafana |
3000 |
3000 |
HTTP |
9. Проверка установки и работы
После установки Compose и первого запуска — проверяй так:
# Версия Compose
docker compose version
# Статус всех сервисов
docker compose ps
# Логи в реальном времени
docker compose logs -f
# Логи конкретного сервиса
docker compose logs -f web
# Ресурсы контейнеров
docker stats --no-stream
# Проверка конфига на ошибки
docker compose config
Команда docker compose config особенно полезна — она парсит твой YAML, подставляет переменные из .env и показывает итоговый конфиг. Если там ошибка синтаксиса — найдёт сразу.
10. Безопасность
Несколько правил, которые обязательны в продакшене.
Файрвол
# UFW - разреши только нужные порты
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Внимание: Docker и UFW
Docker по умолчанию обходит правила UFW через прямые правила iptables. Если пробросил порт в docker-compose.yml — он открыт снаружи, даже если UFW говорит иначе. Чтобы это исправить, используй 127.0.0.1:8080:80 вместо просто 8080:80 для портов, которые не должны быть публичными.
Непривилегированный пользователь в контейнере
# В Dockerfile
RUN addgroup -g 1000 appgroup && \
adduser -D -u 1000 -G appgroup appuser
USER appuser
# В docker-compose.yml
services:
app:
user: "1000:1000"
Ротация логов
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Без ротации логи заполнят диск. Это случается быстрее, чем думаешь.
Лимиты ресурсов
services:
app:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
11. Резервное копирование
Данные в named volumes переживают пересоздание контейнеров. Но не пересоздание volumes.
# Бэкап PostgreSQL
docker compose exec db pg_dump -U appuser appdb > backup_$(date +%Y%m%d_%H%M%S).sql
# Бэкап MySQL
docker compose exec db mysqldump -u wpuser -p${MYSQL_PASSWORD} wordpress > backup_$(date +%Y%m%d_%H%M%S).sql
# Бэкап volume целиком
docker run --rm \
-v имя_проекта_volume:/data \
-v $(pwd):/backup \
alpine tar czf /backup/volume_backup.tar.gz -C /data .
# Восстановление PostgreSQL
docker compose exec -T db psql -U appuser appdb < backup.sql
Что бэкапить: volumes с данными баз, volumes с файлами приложения, сам docker-compose.yml и .env.example (не .env с паролями). Частота: для баз - ежедневно минимум. Для файлов приложения - при каждом деплое. Хранить: не на том же сервере.
12. Обновление Docker Compose
# Через apt
sudo apt update && sudo apt upgrade docker-compose-plugin
# Проверка текущей версии
docker compose version
# Ручное обновление бинарника
DOCKER_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d'"' -f4)
curl -SL "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" \
-o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker compose version
Перед обновлением: проверь changelog на предмет breaking changes. Сделай бэкап данных. Протестируй на staging. Compose v2 обратно совместим с синтаксисом v1 файлов, но иногда бывают нюансы.
13. Миграция с docker-compose v1 на v2
Если ещё используешь старый docker-compose с дефисом - переходи. Он больше не получает обновлений.
Основное изменение - команды:
# Старый синтаксис (v1)
docker-compose up -d
docker-compose logs
docker-compose down
# Новый синтаксис (v2)
docker compose up -d
docker compose logs
docker compose down
Для плавного перехода создай алиас:
echo "alias docker-compose='docker compose'" >> ~/.bashrc
source ~/.bashrc
Конфиги docker-compose.yml полностью совместимы. Менять файлы не нужно. Проверь конфиг на совместимость:
docker compose config
Если команда выполнилась без ошибок и показала итоговый конфиг - всё в порядке.
14. Шпаргалка по командам
# Запуск
docker compose up -d # фоновый режим
docker compose up --build # с пересборкой образов
docker compose up --force-recreate # пересоздать контейнеры
# Остановка
docker compose down # остановить и удалить контейнеры
docker compose down -v # + удалить volumes (осторожно!)
docker compose stop # только остановить
# Информация
docker compose ps # статус сервисов
docker compose ps -a # включая остановленные
docker compose logs -f # логи всех сервисов
docker compose logs --tail=100 web # последние 100 строк
docker compose top # процессы в контейнерах
docker compose stats # использование ресурсов
# Выполнение команд
docker compose exec web bash # интерактивная оболочка
docker compose exec web ls -la # выполнить команду
docker compose run --rm web python test.py # одноразовый контейнер
# Сборка
docker compose build --no-cache # пересборка без кэша
docker compose pull # обновить образы
docker compose config # проверить конфиг
# Масштабирование
docker compose up -d --scale web=3 # 3 инстанса web
15. Осложнения: типичные ошибки и решения
command not found: docker compose
Причина: плагин не установлен или не в PATH.
# Проверяем где плагины
ls ~/.docker/cli-plugins/
ls /usr/lib/docker/cli-plugins/
# Устанавливаем заново
sudo apt install docker-compose-plugin
# Проверяем
docker compose version
permission denied при запуске
Причина: пользователь не в группе docker.
# Проверяем группы текущего пользователя
groups $USER
# Добавляем в группу docker
sudo usermod -aG docker $USER
# Применяем без перелогина
newgrp docker
# Проверяем
docker ps
Cannot connect to Docker daemon
Причина: Docker Engine не запущен.
sudo systemctl status docker
sudo systemctl start docker
sudo systemctl enable docker
Контейнер постоянно рестартует
Причина: ошибка в приложении или неправильный healthcheck.
# Смотрим логи падающего сервиса
docker compose logs --tail=50 имя-сервиса
# Смотрим статус с причиной
docker compose ps
# Запускаем без restart для дебага
docker compose run --rm имя-сервиса
depends_on не помогает - сервис стартует раньше базы
Причина: depends_on без condition только ждёт старта контейнера, не готовности сервиса внутри.
# Неправильно - просто ждёт старта контейнера
depends_on:
- db
# Правильно - ждёт healthcheck
depends_on:
db:
condition: service_healthy
Плюс добавь healthcheck в сервис db (пример для PostgreSQL выше в статье).
Порты заняты
Причина: на хосте уже что-то слушает этот порт.
# Найти что занимает порт
sudo ss -tlnp | grep :8080
sudo lsof -i :8080
# Убить процесс или поменять порт в compose
ports:
- "8081:80" # поменяли 8080 на 8081
Volume не монтируется - файлы не видны
Причина: неправильный путь или проблемы с правами.
# Проверяем абсолютный путь
pwd
ls -la ./html
# Смотрим что примонтировано в контейнере
docker compose exec web ls -la /usr/share/nginx/html
# Проверяем права
ls -la ./html/index.html
chmod 644 ./html/index.html
Ошибка синтаксиса YAML
Причина: табы вместо пробелов или неправильные отступы.
# Проверяем конфиг
docker compose config
# Онлайн-валидатор: yamllint.com
# Или через CLI
sudo apt install yamllint
yamllint docker-compose.yml
16. Альтернативные решения
Docker Compose - не единственный вариант. Для понимания картины:
| Инструмент |
Когда использовать |
Сложность |
| Docker Compose |
1 сервер, dev/staging/простой prod |
низкая |
| Docker Swarm |
несколько серверов, знакомый синтаксис Compose |
средняя |
| Kubernetes |
большой кластер, автомасштабирование, сложные требования |
высокая |
| Podman Compose |
нужен rootless по умолчанию, RHEL-среда |
низкая |
| Nomad |
гетерогенная инфраструктура, не только контейнеры |
средняя |
Kubernetes - мощный инструмент. Но его сложность оправдана не всегда. Если Compose покрывает твои задачи - используй Compose. Я знаю продакшен-системы с миллионами запросов в сутки на Docker Compose. Всё нормально работает.
17. Профилактика: как не сломать
Несколько правил, которые сэкономят тебе нервы в будущем.
Первое - всегда указывай конкретный тег образа в продакшене. latest сегодня и latest через три месяца - это разные образы. Обновления без тестирования прилетают в самый неподходящий момент.
Второе - именованные volumes для данных. Не bind-mount к случайной папке, а нормальный named volume. Данные должны жить дольше контейнеров.
Третье - .env.example в репозитории, .env в .gitignore. Всегда. Без исключений. Одна утечка пароля к базе данных в публичный репозиторий - и ты потратишь день на смену всех учёток и проверку логов.
Четвёртое - настрой ротацию логов. Диск заполняется быстрее, чем думаешь, особенно если приложение многословное.
Пятое - проверяй конфиг перед деплоем командой docker compose config. Поймаешь синтаксические ошибки раньше, чем они проявятся в продакшене в три часа ночи.
Шестое - документируй нестандартные решения прямо в комментариях docker-compose.yml. Ты не запомнишь через полгода, почему именно так.
# Плохо - загадочная конфигурация
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
# Хорошо - понятно зачем
# appendonly yes - WAL для надёжности данных
# maxmemory 256mb - ограничение памяти для кэша
# allkeys-lru - вытеснение редко используемых ключей при достижении лимита
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
18. FAQ
Почему docker compose вместо docker-compose?
В версии 2.x Docker Compose переехал из отдельной утилиты в плагин Docker CLI. Команда стала docker compose (без дефиса). Старый docker-compose (с дефисом, версия 1.x) устарел и больше не получает обновлений. Создай алиас alias docker-compose='docker compose' если привык к старому синтаксису - файлы конфигурации совместимы.
Как проверить, что Docker Compose установлен правильно?
# Проверка версии
docker compose version
# Проверка работоспособности - запускаем тестовый контейнер
docker compose -f - up -d <<EOF
version: '3'
services:
test:
image: hello-world
EOF
# Смотрим логи
docker compose logs test
# Убираем
docker compose down
Что если сервисы поднялись, но не видят друг друга по сети?
Скорее всего, они в разных сетях или сеть не указана. Проверь:
# Какие сети созданы
docker network ls | grep название_проекта
# К каким сетям подключён контейнер
docker inspect имя-контейнера | grep -A 20 '"Networks"'
# Проверь ping между контейнерами
docker compose exec web ping db
Если сервисы в одном docker-compose.yml и сеть не указана явно - Compose создаёт default сеть и подключает все сервисы к ней автоматически. Проблема возникает когда ты явно указал networks для одних сервисов, но забыл про другие.
Как откатиться если обновление сломало приложение?
# Остановить новую версию
docker compose down
# Откатить образ в docker-compose.yml к предыдущей версии
# image: myapp:v1.2.3 (было v1.2.4)
# Поднять предыдущую версию
docker compose up -d
# Если нет конкретных тегов - ищи предыдущий образ локально
docker images myapp
docker tag myapp:old-hash myapp:rollback
Именно поэтому в продакшене никогда не используй latest - невозможно откатиться к "прошлому latest".
Почему данные исчезают после docker compose down -v?
Флаг -v удаляет volumes вместе с контейнерами. Это деструктивная операция. Используй docker compose down без флага -v для сохранения данных. Volumes удаляются только явно: через docker compose down -v или docker volume rm.
19. Итог
Docker Compose установлен. Первый стек поднят. Теперь у тебя есть инструмент, который описывает инфраструктуру как код, запускает всё одной командой и сохраняет данные между перезапусками.
Следующий шаг - добавь мониторинг. Prometheus + Grafana + Node Exporter поднимаются за 10 минут через Compose и дают полную картину того, что происходит с твоими контейнерами. Без мониторинга ты узнаёшь о проблемах от пользователей.
Не заработало - разберёмся
Если что-то пошло не так — смотри секцию «Осложнения» выше. Если не помогло — пиши в комментарии: версию ОС, версию Docker, текст ошибки из docker compose logs. Разберём конкретную ситуацию.