Seafile + OnlyOffice + Docker (Готовый стек для self-hosted альтернативы Nextcloud)

Делюсь полностью рабочими конфигурационными файлами, которые я выстрадал и проверил в бою.
Это полноценная замена Nextcloud на базе Seafile — более лёгкая, быстрая и с отличной синхронизацией. В комплекте:

  • Seafile (Community Edition 13.0.18) — само хранилище, работает как часы.

  • OnlyOffice (8.1.0.1) — онлайн-редактор документов, таблиц и презентаций, идеально встраивается в Seafile.

  • SeaDoc — редактор документов (ещё одна фича Seafile).

  • Notification Server — для уведомлений и мониторинга (ping-эндпоинт).

Всё завернуто в Docker Compose, все версии образов зафиксированы (исключить внезапные обновы).
Настройки вынесены в единый .env — легко править под себя.

:rocket: Как использовать

  1. Подготовьте чистую VM (Debian 12) с достаточным дисковым пространством (минимум 20–30 ГБ, или столько, сколько вам нужно облачного пространства).

  2. Установите Docker и Docker Compose (если ещё не сделано).

  3. Создайте директорию ( /opt/seafile) и поместите в неё все файлы:

    • .env (переименуйте, подставьте свои данные)

    • seafile-server.yml

    • seadoc.yml

    • onlyoffice.yml

    • notification-server.yml

  4. Отредактируйте .env:

    • Замените your.domain.ru на ваш реальный домен (например, drive.example.com).

    • Сгенерируйте надёжные пароли для JWT_PRIVATE_KEY, SEAFILE_MYSQL_DB_PASSWORD, REDIS_PASSWORD, INIT_SEAFILE_MYSQL_ROOT_PASSWORD, ONLYOFFICE_JWT_SECRET.
      Можно использовать команду (в терминале):
      openssl rand -base64 32 | tr -dc 'a-zA-Z' | head -c 32; echo

    • Укажите ваш email и пароль администратора Seafile (INIT_SEAFILE_ADMIN_EMAIL, INIT_SEAFILE_ADMIN_PASSWORD).

    • Если у вас другой часовой пояс, измените TIME_ZONE.

  5. Запустите стек:

cd /opt/seafile
docker compose up -d
  1. Проверьте логи и доступность сервисов:
  • Seafile: http://ваш-ip:8080 (после настройки Traefik будет доступен по домену)

  • OnlyOffice: http://ваш-ip:6233/welcome/

  • Notification Server: http://ваш-ip:8083/ping (должен ответить {"ret":"pong"})

:globe_with_meridians: Доступ из интернета (опционально)

Я использую Traefik как внешний прокси с автоматическими SSL-сертификатами.
Вы можете адаптировать конфигурацию под свой прокси (nginx, Caddy и т.д.).
Для Traefik пример конфига:

http:
  routers:
    seafile-main:
      rule: "Host(`seafile.example.com`) && !PathPrefix(`/notification`)"
      entryPoints: ["websecure"]
      tls:
        certResolver: letsencrypt
        options: modern@file
      middlewares:
        - chain-public@file
        - seafile-headers@file
      service: seafile-main

    seafile-notification:
      rule: "Host(`seafile.example.com`) && PathPrefix(`/notification`)"
      entryPoints: ["websecure"]
      tls:
        certResolver: letsencrypt
        options: modern@file
      middlewares:
        - chain-public@file
        - notification-headers@file
      service: seafile-notification

    onlyoffice:
      rule: "Host(`office.example.com`)"
      entryPoints: ["websecure"]
      tls:
        certResolver: letsencrypt
        options: modern@file
      middlewares:
        - chain-public@file
        - onlyoffice-headers@file
      service: onlyoffice

  services:
    seafile-main:
      loadBalancer:
        servers:
          - url: "http://<IP_вашего_сервера_Seafile>:8080"

    seafile-notification:
      loadBalancer:
        servers:
          - url: "http://<IP_вашего_сервера_Seafile>:8083"
        healthCheck:
          path: /ping
          interval: 10s
          timeout: 3s

    onlyoffice:
      loadBalancer:
        passHostHeader: true
        servers:
          - url: "http://<IP_вашего_сервера_Seafile>:6233"

  middlewares:
    seafile-headers:
      headers:
        customRequestHeaders:
          X-Forwarded-Proto: "https"
          X-Forwarded-Host: "seafile.example.com"
        customResponseHeaders:
          X-Frame-Options: "SAMEORIGIN"
          Content-Security-Policy: "frame-ancestors 'self' https://seafile.example.com"

    notification-headers:
      headers:
        customRequestHeaders:
          X-Forwarded-Proto: "https"
          X-Forwarded-Host: "seafile.example.com"

    onlyoffice-headers:
      headers:
        customRequestHeaders:
          X-Forwarded-Proto: "https"
          X-Forwarded-Host: "office.example.com"
        customResponseHeaders:
          X-Frame-Options: ""
          Content-Security-Policy: "frame-ancestors 'self' https://seafile.example.com"
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        stsPreload: true
        forceSTSHeader: true
        browserXssFilter: true
        contentTypeNosniff: true
        referrerPolicy: "strict-origin-when-cross-origin"
        permissionsPolicy: "geolocation=(self), camera=(self), microphone=(self)"

:pushpin: Пояснения для сообщества

  • Замените seafile.example.com и office.example.com на ваши реальные домены.

  • Вместо <IP_вашего_сервера_Seafile> укажите локальный IP машины, на которой запущен Seafile (например, 192.168.1.100).

  • chain-public@file – это ссылка на цепочку middleware, которая обычно включает CrowdSec, rate limit и базовые заголовки безопасности. Вы можете определить её в отдельном файле или заменить на свои middleware.

  • modern@file – TLS-опции, например:

tls:
  options:
    modern:
      minVersion: VersionTLS12
  • Middleware seafile-headers, notification-headers и onlyoffice-headers уже описаны здесь же – они добавляют необходимые заголовки для корректной работы Seafile и OnlyOffice (в частности, для работы OnlyOffice в iframe).

:file_folder: Состав файлов

Файл Назначение
.env Все настройки (пароли, пути, домены)
seafile-server.yml Основной стек: MariaDB, Redis, Seafile
seadoc.yml Сервис SeaDoc
onlyoffice.yml OnlyOffice Document Server
notification-server.yml Сервер уведомлений (нужен для мониторинга)

:warning: Важно

  • В файлах нет чувствительных данных — только шаблоны. Все секреты вы задаёте сами в .env.

  • Версии образов жёстко зафиксированы (никаких latest), чтобы ничего не сломалось при обновлениях.

  • Если вам нужен антивирус (ClamAV) — официальная интеграция в Docker-версии Seafile не работает (отсутствует clamdscan в контейнере). Я отказался от неё, но вы можете попробовать самостоятельно.

:backhand_index_pointing_right: Версия от 28.02.2026, проверено в работе.

notification-server.yml

services:
  notification-server:
    image: ${NOTIFICATION_SERVER_IMAGE}
    container_name: notification-server
    restart: unless-stopped
    volumes:
      - ${SEAFILE_VOLUME}/seafile/logs:/shared/seafile/logs
    ports:
      - "8083:8083"
    environment:
      - SEAFILE_MYSQL_DB_HOST=${SEAFILE_MYSQL_DB_HOST}
      - SEAFILE_MYSQL_DB_PORT=${SEAFILE_MYSQL_DB_PORT}
      - SEAFILE_MYSQL_DB_USER=${SEAFILE_MYSQL_DB_USER}
      - SEAFILE_MYSQL_DB_PASSWORD=${SEAFILE_MYSQL_DB_PASSWORD:?Variable is not set or empty}
      - SEAFILE_MYSQL_DB_CCNET_DB_NAME=${SEAFILE_MYSQL_DB_CCNET_DB_NAME}
      - SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=${SEAFILE_MYSQL_DB_SEAFILE_DB_NAME}
      - JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:?Variable is not set or empty}
      - SEAFILE_LOG_TO_STDOUT=${SEAFILE_LOG_TO_STDOUT}
      - NOTIFICATION_SERVER_LOG_LEVEL=${NOTIFICATION_SERVER_LOG_LEVEL}
      - NOTIFICATION_SERVER_URL=${NOTIFICATION_SERVER_URL}
      - INNER_NOTIFICATION_SERVER_URL=${INNER_NOTIFICATION_SERVER_URL}
    depends_on:
      db:
        condition: service_healthy
      seafile:
        condition: service_healthy
    networks:
      - seafile-net

networks:
  seafile-net:
    name: seafile-net
artidev@debian-external:/opt/seafile$ cat onlyoffice.yml
services:
  onlyoffice:
    image: ${ONLYOFFICE_IMAGE}
    restart: unless-stopped
    container_name: seafile-onlyoffice
    ports:
      - "${ONLYOFFICE_PORT}:80"
    environment:
      - JWT_ENABLED=true
      - JWT_SECRET=${ONLYOFFICE_JWT_SECRET:?Variable is not set or empty}
      - PROXY_TRUSTED=true
      - PROXY_PROTOCOL=https
      - PROXY_HOST=office.yourdomain.ru # заменить на свой реальный
      - PROXY_PORT=443
    volumes:
      - ${ONLYOFFICE_VOLUME}/logs:/var/log/onlyoffice
      - ${ONLYOFFICE_VOLUME}/data:/var/www/onlyoffice/Data
      - ${ONLYOFFICE_VOLUME}/lib:/var/lib/onlyoffice
    networks:
      - seafile-net

networks:
  seafile-net:
    name: seafile-net

seadoc.yml

services:
  seadoc:
    image: ${SEADOC_IMAGE}
    container_name: seadoc
    restart: unless-stopped
    volumes:
      - ${SEADOC_VOLUME}:/shared
    # ports:
    #   - "80:80"
    environment:
      - DB_HOST=${SEAFILE_MYSQL_DB_HOST}
      - DB_PORT=${SEAFILE_MYSQL_DB_PORT}
      - DB_USER=${SEAFILE_MYSQL_DB_USER}
      - DB_PASSWORD=${SEAFILE_MYSQL_DB_PASSWORD:?Variable is not set or empty}
      - DB_NAME=${SEADOC_MYSQL_DB_NAME}
      - TIME_ZONE=${TIME_ZONE}
      - JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:?Variable is not set or empty}
      - NON_ROOT=${NON_ROOT}
      - SEAHUB_SERVICE_URL=${SEAFILE_SERVICE_URL}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - seafile-net

networks:
  seafile-net:
    name: seafile-net

seafile-server.yml

services:
  db:
    image: ${SEAFILE_DB_IMAGE}
    container_name: seafile-mysql
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=${INIT_SEAFILE_MYSQL_ROOT_PASSWORD}
      - MYSQL_LOG_CONSOLE=true
      - MARIADB_AUTO_UPGRADE=1
    volumes:
      - "${SEAFILE_MYSQL_VOLUME}:/var/lib/mysql"
    networks:
      - seafile-net
    healthcheck:
      test:
        [
          "CMD",
          "/usr/local/bin/healthcheck.sh",
          "--connect",
          "--mariadbupgrade",
          "--innodb_initialized",
        ]
      interval: 20s
      start_period: 30s
      timeout: 5s
      retries: 10

  redis:
    image: ${SEAFILE_REDIS_IMAGE}
    container_name: seafile-redis
    restart: unless-stopped
    command:
      - /bin/sh
      - -c
      - redis-server --requirepass "$$REDIS_PASSWORD"
    environment:
      - REDIS_PASSWORD=${REDIS_PASSWORD}
    networks:
      - seafile-net

  seafile:
    image: ${SEAFILE_IMAGE}
    container_name: seafile
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ${SEAFILE_VOLUME}:/shared
    environment:
      - SEAFILE_MYSQL_DB_HOST=${SEAFILE_MYSQL_DB_HOST}
      - SEAFILE_MYSQL_DB_PORT=${SEAFILE_MYSQL_DB_PORT}
      - SEAFILE_MYSQL_DB_USER=${SEAFILE_MYSQL_DB_USER}
      - SEAFILE_MYSQL_DB_PASSWORD=${SEAFILE_MYSQL_DB_PASSWORD:?Variable is not set or empty}
      - INIT_SEAFILE_MYSQL_ROOT_PASSWORD=${INIT_SEAFILE_MYSQL_ROOT_PASSWORD}
      - SEAFILE_MYSQL_DB_CCNET_DB_NAME=${SEAFILE_MYSQL_DB_CCNET_DB_NAME}
      - SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=${SEAFILE_MYSQL_DB_SEAFILE_DB_NAME}
      - SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=${SEAFILE_MYSQL_DB_SEAHUB_DB_NAME}
      - TIME_ZONE=${TIME_ZONE}
      - INIT_SEAFILE_ADMIN_EMAIL=${INIT_SEAFILE_ADMIN_EMAIL}
      - INIT_SEAFILE_ADMIN_PASSWORD=${INIT_SEAFILE_ADMIN_PASSWORD}
      - SEAFILE_SERVER_HOSTNAME=${SEAFILE_SERVER_HOSTNAME:?Variable is not set or empty}
      - SEAFILE_SERVER_PROTOCOL=${SEAFILE_SERVER_PROTOCOL}
      - SITE_ROOT=${SITE_ROOT}
      - NON_ROOT=${NON_ROOT}
      - JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:?Variable is not set or empty}
      - SEAFILE_LOG_TO_STDOUT=${SEAFILE_LOG_TO_STDOUT}
      - ENABLE_GO_FILESERVER=${ENABLE_GO_FILESERVER}
      - ENABLE_SEADOC=${ENABLE_SEADOC}
      - SEADOC_SERVER_URL=${SEAFILE_SERVER_PROTOCOL}://${SEAFILE_SERVER_HOSTNAME}/sdoc-server
      - CACHE_PROVIDER=${CACHE_PROVIDER}
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}
      - REDIS_PASSWORD=${REDIS_PASSWORD}
      - MEMCACHED_HOST=${MEMCACHED_HOST}
      - MEMCACHED_PORT=${MEMCACHED_PORT}
      - ENABLE_NOTIFICATION_SERVER=${ENABLE_NOTIFICATION_SERVER}
      - INNER_NOTIFICATION_SERVER_URL=${INNER_NOTIFICATION_SERVER_URL}
      - NOTIFICATION_SERVER_URL=${NOTIFICATION_SERVER_URL}
      - ENABLE_SEAFILE_AI=${ENABLE_SEAFILE_AI}
      - SEAFILE_AI_SERVER_URL=${SEAFILE_AI_SERVER_URL}
      - SEAFILE_AI_SECRET_KEY=${JWT_PRIVATE_KEY}
      - MD_FILE_COUNT_LIMIT=${MD_FILE_COUNT_LIMIT}
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:80 || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - seafile-net

networks:
  seafile-net:
    name: seafile-net

.env

COMPOSE_PATH_SEPARATOR=,
COMPOSE_FILE=seafile-server.yml,seadoc.yml,onlyoffice.yml,notification-server.yml
SEAFILE_IMAGE=seafileltd/seafile-mc:13.0.18
SEAFILE_DB_IMAGE=mariadb:10.11.16
SEAFILE_REDIS_IMAGE=redis:8.6.1
SEADOC_IMAGE=seafileltd/sdoc-server:2.0.9
NOTIFICATION_SERVER_IMAGE=seafileltd/notification-server:13.0.10
ONLYOFFICE_IMAGE=onlyoffice/documentserver:8.1.0.1

BASIC_STORAGE_PATH=/opt
SEAFILE_VOLUME=$BASIC_STORAGE_PATH/seafile-data
SEAFILE_MYSQL_VOLUME=$BASIC_STORAGE_PATH/seafile-mysql/db
SEADOC_VOLUME=$BASIC_STORAGE_PATH/seadoc-data
ONLYOFFICE_VOLUME=$BASIC_STORAGE_PATH/onlyoffice

SEAFILE_SERVER_HOSTNAME=your.domain.ru
SEAFILE_SERVER_PROTOCOL=https
TIME_ZONE=Europe/Moscow
JWT_PRIVATE_KEY=password must be at least 32 characters long
SEAFILE_MYSQL_DB_HOST=db
SEAFILE_MYSQL_DB_PORT=3306
SEAFILE_MYSQL_DB_USER=seafile
SEAFILE_MYSQL_DB_PASSWORD=password
SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet_db
SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile_db
SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=seahub_db
CACHE_PROVIDER=redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=password
INIT_SEAFILE_MYSQL_ROOT_PASSWORD=password
INIT_SEAFILE_ADMIN_EMAIL=your e-mail
INIT_SEAFILE_ADMIN_PASSWORD=e-mail application password
ENABLE_SEADOC=true
ENABLE_NOTIFICATION_SERVER=true
NOTIFICATION_SERVER_LOG_LEVEL=info
NOTIFICATION_SERVER_URL=https://your.domain.ru/notification
INNER_NOTIFICATION_SERVER_URL=https://notification-server:8083
ENABLE_SEAFILE_AI=false
MD_FILE_COUNT_LIMIT=100000
ONLYOFFICE_PORT=6233
ONLYOFFICE_JWT_SECRET=password must be at least 32 characters long

SEADOC_MYSQL_DB_NAME=seahub_db
SEAFILE_SERVICE_URL=https://seafile
SEAFILE_LOG_TO_STDOUT=false
ENABLE_GO_FILESERVER=true
SITE_ROOT=/
NON_ROOT=false
SEAFILE_AI_SERVER_URL=https://seafile-ai:8888
MEMCACHED_HOST=
MEMCACHED_PORT=

Команды для генерации случайного набора символов:

openssl rand -base64 32 | tr -dc 'a-zA-Z' | head -c 32; echo

Если нужно больше/меньше символов:
Для 16 букв:
openssl rand -base64 16 | tr -dc 'a-zA-Z' | head -c 16; echo

Для 24 букв:
openssl rand -base64 18 | tr -dc 'a-zA-Z' | head -c 24; echo

Для 64 букв:
openssl rand -base64 48 | tr -dc 'a-zA-Z' | head -c 64; echo

После развертывания стека, нужно отредактировать системный файл конфигурации: seahub_settings.py

Пример моего рабочего seahub_settings.py имеет следующий вид.

# -*- coding: utf-8 -*-
SECRET_KEY = будет сгененрирован в процессе установки
TIME_ZONE = 'Europe/Moscow'

EMAIL_USE_SSL = True
EMAIL_HOST = ''        # smpt server
EMAIL_HOST_USER = ''    # username and domain
EMAIL_HOST_PASSWORD = ''    # password
EMAIL_PORT = 
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER

ENABLE_ONLYOFFICE = True
ONLYOFFICE_APIJS_URL = 'https://office.yourdomain.ru/web-apps/apps/api/documents/api.js'
ONLYOFFICE_JWT_SECRET = '' # должен совпадать с .env
ONLYOFFICE_FILE_EXTENSION = ('doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'odt', 'fodt', 'odp', 'fodp', 'ods', 'fods', 'ppsx', 'pps', 'csv')
ONLYOFFICE_EDIT_FILE_EXTENSION = ('docx', 'pptx', 'xls', 'csv')
OFFICE_PREVIEW_MAX_SIZE = 30 * 1024 * 1024
ONLYOFFICE_FORCE_SAVE = True

SERVICE_URL = 'https://drive.yourdomain.ru'
FILE_SERVER_ROOT = 'https://drive.yourdomain.ru/seafhttp'

:pushpin: Как использовать

  • Отредактируйте содержимое файла, который находится в папке conf вашего Seafile (обычно /opt/seafile-data/seafile/conf/) с помощью команды:
sudo /opt/seafile-data/seafile/conf/seahub_settings.py
  • Замените все yourdomain.ru на свой реальный домен.

  • Перезапустите контейнер Seafile, чтобы изменения вступили в силу:

docker restart seafile

Источник: Setup community edition - Seafile Admin Manual

4 лайка

Не в первый раз сталкиваюсь с тем, что в compose файлах фиксируются версии образов.

У меня это работает так - версия указана latest, деплою сервис, настраиваю проверяю, гоняю в тестовой среде, потом делаю бэкап и переношу в продуктив.

Далее этот сервис живёт месяцами-годами без изменения ровно до того момента, пока я сам не решу передеплоить docker-compose, а если что-то сломается, то из бэкапа откачусь обратно.

Поделись опытом, какой у тебя подход к обновлению. Вот задумался, может я что-то упускаю и мой вариант не самый оптимальный.

За статью спасибо, ещё не добрался до своего облака, буду иметь ввиду на будущее.

P.s. с дебютом :saluting_face::partying_face:

У меня в PVE настроено ежедневное бекапирование всех LXC и VM, храню X дней.

Если есть обновление просто меняю версию, разворачиваю, проверяю решаю проблему при необходимости, если не вышло - в топку на какое-то время. Откатываю бекап.

Я не особо продвинутый user в самохостинге, Linux. Поэтому все приходит с течением времени и я не претендую на истину в последней инстанции.

P.S. Спасибо!

1 лайк

Многие приложения используют фикс по версиям. Тот же Authentik. Это нормальный поход, если не лень отслеживать изменения и баги. К слову, мне лень. Ставлю latest и делаю бекап VM раз в день

Мне нравится подход с закреплением мажорной версии или минорной
Например, указываем 2.5 вместо 2.5.1

1 лайк

на что бэкапы делаете и сколько это место занимает. например если нет pbs можно ли сделать бекапы на нас?

Можно, но возникают вопросы с дедубликацицией, по сути, каждый бэкап занимает столько места, сколько показывает сам PVE размер диска, поэтому учитывайте этот факт.

PBS я пока не разобрался, но такое ощущение, что бэкап весит в 2 раза больше места, чем сам исходник, с дедубликацией во первых бэкапирование быстрее происходит за счет снижения пересылок (по сути, создаются инкрементаьные бэкапы), а во вторых, значительно сокращается занимаемое место
У меня сейчас так

Количество резервных копий на сейчас 957 (но я добавил пропущенные хосты и через пару недель должно перевалить за 1000)
Фактор дедупикации почти 18, т.е. виртуально резервные копии занимают 4 817 ГБ, а по факту 268 ГБ
Поскольку, дедупликация не на уровне бэкапа, а по всему хранилищу, то после унификации инструментов и операционок (у меня alipine в основном), то LXC контейнер сразу же после подготовки к работе практически не занимает место в бэкапе.

87 групп, это отдельные виртуалки и сервисы, на 957 снимков это значит, что в среднем у меня хранится по 11 резервных копий одного сервиса и все равно это меньше 18 (т.е. дедубликация еще выше за счет использованя одного блока несколькими независимыми сервисами)

Но надо учесть, что создание снимка контейнера или виртуалки не очень надежное т.к. могут потеряться данные которые находятся в оперативке, поэтому, там, где это возможно, у меня создаются резервные копии средствами самого сервиса и выгрузка из в PBS

Лично у меня подключен третий HDD 2.5 диск объемом 500Гб, на который по расписанию делаются бекапы средствами PVE.