MAATRIX / Блог / Strapi в Docker Compose: готовый файл

Strapi в Docker Compose: готовый файл

MAATRIX

Strapi — самый популярный open-source headless CMS на Node.js: гибкий REST/GraphQL API, удобная админка и полный контроль над схемой контента. Разворачивать его вручную через npm run develop неудобно для продакшена — нужен процесс-менеджер, отдельная база и правильные тома для медиафайлов. Ниже — рабочий docker-compose.yml, который поднимает Strapi с PostgreSQL за один docker compose up -d, без лишних танцев с конфигами.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Зачем Strapi в Docker, а не npm install

Голая установка через npm тянет за собой Node.js нужной версии на хосте, конфликтует с другими проектами по портам и требует ручного управления процессом (pm2 или systemd-юнит). Docker Compose решает это одним файлом:

  • версия Node.js фиксируется в образе — не зависит от того, что стоит на сервере;
  • база данных (PostgreSQL) поднимается рядом, в отдельном контейнере, с собственным томом;
  • обновление Strapi — это docker compose pull && docker compose up -d, без ручного пересбора зависимостей;
  • на одном сервере можно держать несколько проектов Strapi с разными версиями Node без конфликтов.

Минус: Strapi — довольно прожорливое Node-приложение, особенно на старте (сборка админ-панели). Под него нужен сервер не слабее 2 vCPU / 2-4 ГБ RAM — на 1 ГБ сборка админки в SWC/webpack может просто упасть по памяти.

Готовый docker-compose.yml

Структура каталога:

strapi-project/
├── docker-compose.yml
├── .env
└── app/          # исходники Strapi (или пустая папка при первом запуске)

Файл .env:

# База данных
POSTGRES_DB=strapi
POSTGRES_USER=strapi
POSTGRES_PASSWORD=change_me_strong_password

# Strapi
STRAPI_ADMIN_URL=https://cms.example.com
NODE_ENV=production

# Секреты Strapi — сгенерируйте свои, см. раздел про секреты ниже
APP_KEYS=key1,key2,key3,key4
API_TOKEN_SALT=your_api_token_salt
ADMIN_JWT_SECRET=your_admin_jwt_secret
TRANSFER_TOKEN_SALT=your_transfer_token_salt
JWT_SECRET=your_jwt_secret

Файл docker-compose.yml:

services:
  strapi:
    image: node:20-bookworm-slim
    container_name: strapi_app
    working_dir: /opt/app
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_CLIENT: postgres
      DATABASE_HOST: strapi_db
      DATABASE_PORT: 5432
      DATABASE_NAME: ${POSTGRES_DB}
      DATABASE_USERNAME: ${POSTGRES_USER}
      DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
      DATABASE_SSL: "false"
    volumes:
      - ./app:/opt/app
      - strapi_uploads:/opt/app/public/uploads
      - /opt/app/node_modules
    command: sh -c "if [ ! -f package.json ]; then npx create-strapi-app@latest . --quickstart --no-run --typescript --use-npm; fi && npm install && npm run build && npm run start"
    ports:
      - "1337:1337"
    depends_on:
      strapi_db:
        condition: service_healthy

  strapi_db:
    image: postgres:16-alpine
    container_name: strapi_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - strapi_pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  strapi_pgdata:
  strapi_uploads:

Такая обвязка удобна для первого запуска «с нуля»: контейнер сам создаст проект Strapi при пустой папке app/. Но для повторяемых деплоев (когда исходники уже в git) лучше собрать собственный образ — см. следующий раздел.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Продакшен-вариант: собственный Dockerfile

Скачивать и собирать Strapi при каждом старте контейнера — плохая практика: долго, непредсказуемо, ломается при недоступности npm-реестра. Правильнее держать исходники Strapi в git-репозитории и собирать образ явно.

Dockerfile в корне проекта Strapi:

FROM node:20-bookworm-slim AS build
WORKDIR /opt/app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-bookworm-slim
WORKDIR /opt/app
ENV NODE_ENV=production
COPY --from=build /opt/app ./
EXPOSE 1337
CMD ["npm", "run", "start"]

И упрощённая часть docker-compose.yml для этого случая:

services:
  strapi:
    build: ./app
    container_name: strapi_app
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_CLIENT: postgres
      DATABASE_HOST: strapi_db
      DATABASE_PORT: 5432
      DATABASE_NAME: ${POSTGRES_DB}
      DATABASE_USERNAME: ${POSTGRES_USER}
      DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
      DATABASE_SSL: "false"
    volumes:
      - strapi_uploads:/opt/app/public/uploads
    ports:
      - "1337:1337"
    depends_on:
      strapi_db:
        condition: service_healthy

Такой образ пересобирается на CI/CD или вручную командой docker compose build strapi при каждом обновлении кода — предсказуемо и воспроизводимо на любом сервере.

Секреты Strapi: как сгенерировать правильно

Strapi требует несколько секретных значений в .env — без них админка либо не запустится, либо будет уязвима с ключами по умолчанию. Сгенерировать случайные строки можно прямо в терминале:

# APP_KEYS — 4 ключа через запятую
for i in 1 2 3 4; do openssl rand -base64 32; done | paste -sd, -

# Остальные секреты — по одному значению
openssl rand -base64 32   # API_TOKEN_SALT
openssl rand -base64 32   # ADMIN_JWT_SECRET
openssl rand -base64 32   # TRANSFER_TOKEN_SALT
openssl rand -base64 32   # JWT_SECRET

Вставьте результат в .env. Никогда не коммитьте этот файл в git — добавьте его в .gitignore и храните только .env.example с пустыми плейсхолдерами.

Nginx как обратный прокси и SSL

Strapi слушает порт 1337 внутри контейнера, наружу его лучше не выставлять напрямую — заворачиваем через Nginx с HTTPS. Базовый конфиг:

server {
    listen 80;
    server_name cms.example.com;

    location / {
        proxy_pass http://127.0.0.1:1337;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 50m;
    }
}

client_max_body_size важно поднять — при дефолтных настройках Nginx загрузка медиафайлов через админку Strapi будет падать с 413. После этого — certbot --nginx -d cms.example.com для сертификата. Подробный разбор самого reverse proxy — в статье про Nginx как обратный прокси, а частые проблемы с SSL разобраны в материале про ошибки Nginx с SSL.

Хранение медиафайлов: локальный том или S3

По умолчанию Strapi сохраняет загруженные файлы в public/uploads — в примере выше это именованный том strapi_uploads, который переживает пересоздание контейнера. Для одного сервера этого достаточно, но есть нюансы:

ВариантПлюсыМинусы
Локальный том (по умолчанию)Просто, не требует внешних сервисовБэкапить нужно вручную, не масштабируется на несколько инстансов
S3-совместимое хранилище (MinIO)Отдельное масштабирование, легче бэкапитьНужна установка провайдера @strapi/provider-upload-aws-s3, дополнительная настройка

Если предпочитаете self-hosted S3, разворачивать MinIO в Docker Compose можно на том же сервере — конфигурация провайдера в config/plugins.js Strapi после этого сводится к указанию endpoint, ключей доступа и bucket.

Бэкап: база данных и медиафайлы

Забыть про бэкап — типичная ошибка на старте проекта. У Strapi два места с данными: PostgreSQL (контент, схема, пользователи) и том с загруженными файлами. Простой скрипт для cron:

#!/bin/bash
BACKUP_DIR=/opt/backups/strapi
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"

# Дамп базы
docker exec strapi_db pg_dump -U strapi strapi | gzip > "$BACKUP_DIR/db_$DATE.sql.gz"

# Архив медиафайлов из тома
docker run --rm -v strapi-project_strapi_uploads:/data -v "$BACKUP_DIR":/backup \
  alpine tar czf "/backup/uploads_$DATE.tar.gz" -C /data .

# Чистим бэкапы старше 14 дней
find "$BACKUP_DIR" -type f -mtime +14 -delete

Добавьте в crontab -e:

0 3 * * * /opt/backups/strapi-backup.sh >> /var/log/strapi-backup.log 2>&1

Для восстановления базы: gunzip -c db_ДАТА.sql.gz | docker exec -i strapi_db psql -U strapi strapi. Общие принципы резервного копирования в контейнерных стеках подробнее разобраны в статье про Docker Compose для продакшена.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Сколько ресурсов нужно серверу под Strapi?

Для одного проекта с умеренной нагрузкой хватает 2 vCPU и 2-4 ГБ RAM — сборка админ-панели (npm run build) на этапе первого деплоя временно требует больше памяти, чем сам работающий процесс. На слабых VPS сборку лучше делать локально или в CI, а на сервер выкладывать уже готовый build/.

Можно ли использовать SQLite вместо PostgreSQL?

Технически да — Strapi поддерживает SQLite «из коробки», но для продакшена с несколькими редакторами и API-нагрузкой PostgreSQL надёжнее: лучше работает с конкурентными запросами и проще бэкапится штатными инструментами.

Как обновить Strapi без потери данных?

База и медиафайлы лежат в отдельных volume и не пересоздаются при обновлении образа. Последовательность: git pull (или пересборка образа) → docker compose build strapidocker compose up -d strapi. Перед мажорным обновлением (например, Strapi 4 → 5) обязательно прочитайте changelog — миграции схемы могут требовать ручных шагов.

Порт 1337 — это обязательно?

Нет, это дефолт Strapi, задаётся переменной PORT в .env и пробрасывается в docker-compose.yml. Наружу его лучше не открывать вовсе — весь трафик должен идти через Nginx на 80/443.

Strapi зависает при первой сборке админки — что делать?

Чаще всего это нехватка памяти: процесс webpack/SWC съедает несколько сотен мегабайт сверх обычного. Проверьте docker stats во время сборки и, если сервер упирается в лимит, временно добавьте swap или соберите образ на более мощной машине, а на прод выложите готовый артефакт.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →