Перейти к основному содержимому
  1. Posts/
  2. Self-Hosting/

Postiz: пытаюсь автоматизировать YouTube, VK и RuTube - и почему из трех работает только один

·3022 слов·15 минут· loading · loading · ·
Stilicho2011
Автор
Stilicho2011
Пишу о homelab, self-hosting, автоматизации и open-source решениях
Оглавление
Self-Hosting - This article is part of a series.
Part : This Article

Postiz: пытаюсь автоматизировать YouTube, VK и RuTube - и почему из трех работает только один
#

У меня по понятным причинам в какой-то момент появилось три площадки, куда я заливаю видео - YouTube как основная, VK и RuTube для той части аудитории, у которой то, чье имя нельзя называть, не работает. Заливать одно и то же видео руками три раза - занятие, которое быстро надоедает, поверьте, я знаю о чем говорю, поэтому я решил посмотреть в сторону self-hosted решения для автоматической публикации. Выбор пал на Postiz - планировщик постов с открытым исходным кодом (лицензия AGPL-3.0), self-hosted альтернатива Buffer/Hootsuite/Later, с заявленной поддержкой трех десятков площадок сразу.

На бумаге все звучало отлично: пишешь пост один раз, выбираешь площадки и время - и Postiz сам раскладывает публикацию по расписанию. На практике все оказалось чуть менее гладко, чем в маркетинговых материалах самого проекта. YouTube подключился без сюрпризов, не считая проблем с отсутствием выбора России как страны регистрации канала. Но это не проблема приложения, а сегодняшняя реальность с Google. С VK пришлось повозиться, но все закончилось фиаско. А RuTube … но обо всем по порядку.

Что такое Postiz
#

Postiz - open-source инструмент для планирования и автоматического постинга в соцсетях. По сути, self-hosted альтернатива Buffer/Hootsuite/Later - только без ежемесячной подписки и с полным контролем над своими данными, что для моего блога о homelab-е звучит как единственно верный путь. Официально в проекте заявлено больше 30 поддерживаемых площадок - X, Instagram, LinkedIn, TikTok, Facebook, Threads, Discord, Slack, YouTube, VK и так далее.

Но вот с чем ты почти сразу столкнешься - не все эти тридцать с лишним площадок поддерживаются одинаково хорошо. Небольшая ремарка: конечно, я не тестировал все площадки, которые указаны в приложении. У части сервисов есть полноценная кнопка “Подключить” прямо в интерфейсе. Часть работает только через Public API, без возможности накликать все в UI. А какие-то площадки, как выяснилось, в Postiz просто отсутствуют - что бы ни говорили лендинги. С RuTube именно такой случай.

Ставим Postiz
#

Первый важный момент - сами разработчики прямо просят не тащить docker-compose.yml из чьих-то статей (получается, что и из моей в том числе), а брать его из канонического репозитория:

git clone https://github.com/gitroomhq/postiz-docker-compose

Логика в принципе понятная - сервисы, образы, набор env-переменных и функционал контейнеров меняются от релиза к релизу, и данные, указанные в статье, могут быстро устареть. Дальше docker compose up -d из канонического репозитория поднимает Postiz, Postgres, Redis и Temporal (он тут отвечает за очереди и расписание публикаций) одним стэком.

Я взял этот compose как основу, но перед тем как запустить его у себя, привел его в порядок под свои обычные требования и привычки - как и с остальными сервисами в моем homelab-е: докер volumes заменил на bind mount, все секреты (JWT_SECRET, пароли БД, ключи соцсетей) вынес в отдельный .env, а Traefik-лейблы добавил сразу, просто закомментированными, потому что в настоящий момент Traefik у меня крутится в отдельном LXC контейнере.

Дальше начинается самое муторное - переменные окружения. Их там много, очень много, но для домашней лаборатории с обратным прокси Traefik по-настоящему критичны лишь несколько:

ПеременнаяЗа что отвечает
DATABASE_URLстрока подключения к Postgres
REDIS_URLподключение к Redis (очереди, кэш)
JWT_SECRETдлинная случайная строка для подписи сессий
FRONTEND_URLадрес, по которому браузер видит фронтенд Postiz
NEXT_PUBLIC_BACKEND_URLадрес, по которому браузер видит бэкенд Postiz
BACKEND_INTERNAL_URLадрес, по которому фронтенд (SSR) достает бэкенд внутри сети/контейнера
MAIN_URLопционально, для абсолютных ссылок в email-уведомлениях. Если не задать - берется FRONTEND_URL

Внешний Postgres вместо контейнера из коробки
#

Один момент, в котором я сразу отступил от канонического compose - Postgres. В официальном репозитории он поднимается локальным контейнером рядом с самим Postiz, но у меня уже есть отдельный сервер Postgres 16 по соответствующему адресу - тот же, что обслуживает Forgejo, Authentik и еще кучу сервисов, поднятый как раз для того, чтобы не плодить по контейнеру СУБД под каждый self-hosted сервис.

Официальная документация по системным требованиям требует PostgreSQL версии 14 или новее, так что шестнадцатая версия проходит с запасом - апгрейдить или городить отдельный инстанс не пришлось. Внешний Postgres для Postiz - штатно поддерживаемый сценарий, разработчики отдельно упоминают его в системных требованиях как вариант, который просто требует больше памяти под сам процесс Postiz на хосте.

Технически это значит, что сервиса postiz-postgres в моем compose просто нет, а DATABASE_URL смотрит напрямую на внешний хост:

DATABASE_URL="postgresql://postiz:пароль@х.х.х.х:5432/postiz"

Единственный нюанс - саму базу и пользователя на внешнем сервере нужно создать заранее вручную (я делаю это через pgAdmin): Postiz сам прогонит миграции при первом старте, но базу с нуля он не создаст, ему нужно, чтобы она уже существовала. Temporal при этом остался при своем собственном Postgres - у него отдельная база со своими требованиями (расширения, схема), поэтому его локальный контейнер я не трогал и никуда не выносил.

Тут есть один важный момент: FRONTEND_URL используется как база для OAuth redirect - именно на этот адрес будут обращаться YouTube, VK и все остальные площадки после авторизации. Если Postiz висит за Traefik (как в моем случае) на https://postiz.домен.ru, то FRONTEND_URL должен быть ровно этим адресом, один в один, без лишних слэшей и опечаток - иначе OAuth callback просто потеряется где-то по пути к твоей хоумлабе. Плюс Postiz сам устанавливает secure cookies, а значит без нормального HTTPS он вообще толком не заработает - самоподписанный сертификат тут не поможет, нужен настоящий, у меня все традиционно через Traefik и Let’s Encrypt.

Traefik-лейблы в compose добавляются точно так же, как и во всех остальных моих статьях про Docker-сервисы за обратным прокси - роутер на websecure, TLS, редирект с HTTP на HTTPS, вот это все.

Чтобы не размазывать финальный файл по кусочкам по всей статье - вот он целиком, со всеми правками, о которых я рассказал выше (bind mount, вынесенные в .env секреты, внешний Postgres, закомментированные Traefik-лейблы):

services:
  postiz:
    image: ghcr.io/gitroomhq/postiz-app:latest
    container_name: postiz
    restart: always
    environment:
      # === Required Settings
      MAIN_URL: 'https://${POSTIZ_DOMAIN}'
      FRONTEND_URL: 'https://${POSTIZ_DOMAIN}'
      NEXT_PUBLIC_BACKEND_URL: 'https://${POSTIZ_DOMAIN}/api'
      JWT_SECRET: '${POSTIZ_JWT_SECRET}'
      DATABASE_URL: 'postgresql://${POSTIZ_DB_USER}:${POSTIZ_DB_PASSWORD}@${POSTIZ_DB_HOST}:${POSTIZ_DB_PORT}/${POSTIZ_DB_NAME}'
      REDIS_URL: 'redis://postiz-redis:6379'
      BACKEND_INTERNAL_URL: 'http://localhost:3000'
      TEMPORAL_ADDRESS: "temporal:7233"
      IS_GENERAL: 'true'
      DISABLE_REGISTRATION: 'false'
      RUN_CRON: 'true'

      # === Storage Settings
      STORAGE_PROVIDER: 'local'
      UPLOAD_DIRECTORY: '/uploads'
      NEXT_PUBLIC_UPLOAD_DIRECTORY: '/uploads'

      # === Cloudflare (R2) Settings
      # STORAGE_PROVIDER: 'cloudflare'
      # CLOUDFLARE_ACCOUNT_ID: '${CLOUDFLARE_ACCOUNT_ID}'
      # CLOUDFLARE_ACCESS_KEY: '${CLOUDFLARE_ACCESS_KEY}'
      # CLOUDFLARE_SECRET_ACCESS_KEY: '${CLOUDFLARE_SECRET_ACCESS_KEY}'
      # CLOUDFLARE_BUCKETNAME: '${CLOUDFLARE_BUCKETNAME}'
      # CLOUDFLARE_BUCKET_URL: 'https://your-bucket-url.r2.cloudflarestorage.com/'
      # CLOUDFLARE_REGION: 'auto'

      # === Social Media API Settings (заполняй по мере подключения платформ)
      X_API_KEY: '${X_API_KEY:-}'
      X_API_SECRET: '${X_API_SECRET:-}'
      LINKEDIN_CLIENT_ID: '${LINKEDIN_CLIENT_ID:-}'
      LINKEDIN_CLIENT_SECRET: '${LINKEDIN_CLIENT_SECRET:-}'
      REDDIT_CLIENT_ID: '${REDDIT_CLIENT_ID:-}'
      REDDIT_CLIENT_SECRET: '${REDDIT_CLIENT_SECRET:-}'
      GITHUB_CLIENT_ID: '${GITHUB_CLIENT_ID:-}'
      GITHUB_CLIENT_SECRET: '${GITHUB_CLIENT_SECRET:-}'
      BEEHIIVE_API_KEY: '${BEEHIIVE_API_KEY:-}'
      BEEHIIVE_PUBLICATION_ID: '${BEEHIIVE_PUBLICATION_ID:-}'
      THREADS_APP_ID: '${THREADS_APP_ID:-}'
      THREADS_APP_SECRET: '${THREADS_APP_SECRET:-}'
      FACEBOOK_APP_ID: '${FACEBOOK_APP_ID:-}'
      FACEBOOK_APP_SECRET: '${FACEBOOK_APP_SECRET:-}'
      YOUTUBE_CLIENT_ID: '${YOUTUBE_CLIENT_ID:-}'
      YOUTUBE_CLIENT_SECRET: '${YOUTUBE_CLIENT_SECRET:-}'
      TIKTOK_CLIENT_ID: '${TIKTOK_CLIENT_ID:-}'
      TIKTOK_CLIENT_SECRET: '${TIKTOK_CLIENT_SECRET:-}'
      PINTEREST_CLIENT_ID: '${PINTEREST_CLIENT_ID:-}'
      PINTEREST_CLIENT_SECRET: '${PINTEREST_CLIENT_SECRET:-}'
      DRIBBBLE_CLIENT_ID: '${DRIBBBLE_CLIENT_ID:-}'
      DRIBBBLE_CLIENT_SECRET: '${DRIBBBLE_CLIENT_SECRET:-}'
      DISCORD_CLIENT_ID: '${DISCORD_CLIENT_ID:-}'
      DISCORD_CLIENT_SECRET: '${DISCORD_CLIENT_SECRET:-}'
      DISCORD_BOT_TOKEN_ID: '${DISCORD_BOT_TOKEN_ID:-}'
      SLACK_ID: '${SLACK_ID:-}'
      SLACK_SECRET: '${SLACK_SECRET:-}'
      SLACK_SIGNING_SECRET: '${SLACK_SIGNING_SECRET:-}'
      MASTODON_URL: 'https://mastodon.social'
      MASTODON_CLIENT_ID: '${MASTODON_CLIENT_ID:-}'
      MASTODON_CLIENT_SECRET: '${MASTODON_CLIENT_SECRET:-}'

      # === OAuth & Authentik Settings (раскомментируй, если решишь добавить SSO как у Vaultwarden)
      # NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME: 'Authentik'
      # NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL: 'https://raw.githubusercontent.com/walkxcode/dashboard-icons/master/png/authentik.png'
      # POSTIZ_GENERIC_OAUTH: 'false'
      # POSTIZ_OAUTH_URL: 'https://auth.domain.ru'
      # POSTIZ_OAUTH_AUTH_URL: 'https://auth.domain.ru/application/o/authorize/'
      # POSTIZ_OAUTH_TOKEN_URL: 'https://auth.domain.ru/application/o/token/'
      # POSTIZ_OAUTH_USERINFO_URL: 'https://auth.domain.ru/application/o/userinfo/'
      # POSTIZ_OAUTH_CLIENT_ID: '${POSTIZ_OAUTH_CLIENT_ID}'
      # POSTIZ_OAUTH_CLIENT_SECRET: '${POSTIZ_OAUTH_CLIENT_SECRET}'
      # POSTIZ_OAUTH_SCOPE: "openid profile email"

      # === Sentry
      # NEXT_PUBLIC_SENTRY_DSN: 'http://spotlight:8969/stream'
      # SENTRY_SPOTLIGHT: '1'

      # === Misc Settings
      OPENAI_API_KEY: '${OPENAI_API_KEY:-}'
      NEXT_PUBLIC_DISCORD_SUPPORT: ''
      NEXT_PUBLIC_POLOTNO: ''
      API_LIMIT: 30

      # === Payment / Stripe Settings
      FEE_AMOUNT: 0.05
      STRIPE_PUBLISHABLE_KEY: '${STRIPE_PUBLISHABLE_KEY:-}'
      STRIPE_SECRET_KEY: '${STRIPE_SECRET_KEY:-}'
      STRIPE_SIGNING_KEY: '${STRIPE_SIGNING_KEY:-}'
      STRIPE_SIGNING_KEY_CONNECT: '${STRIPE_SIGNING_KEY_CONNECT:-}'

      # === Developer Settings
      NX_ADD_PLUGINS: false

      # === Short Link Service Settings (Optional)
      # DUB_TOKEN: "${DUB_TOKEN}"
      # DUB_API_ENDPOINT: "https://api.dub.co"
      # DUB_SHORT_LINK_DOMAIN: "dub.sh"
      # SHORT_IO_SECRET_KEY: "${SHORT_IO_SECRET_KEY}"
      # KUTT_API_KEY: "${KUTT_API_KEY}"
      # KUTT_API_ENDPOINT: "https://kutt.it/api/v2"
      # KUTT_SHORT_LINK_DOMAIN: "kutt.it"
      # LINK_DRIP_API_KEY: "${LINK_DRIP_API_KEY}"
      # LINK_DRIP_API_ENDPOINT: "https://api.linkdrip.com/v1/"
      # LINK_DRIP_SHORT_LINK_DOMAIN: "dripl.ink"

    volumes:
      - ./data/postiz-config:/config/
      - ./data/postiz-uploads:/uploads/
    ports:
      - "4007:5000"
    # --- Traefik: включить, если решишь публиковать Postiz через Docker-провайдер ---
    # labels:
    #   - "traefik.enable=true"
    #   - "traefik.http.routers.postiz.rule=Host(`${POSTIZ_DOMAIN}`)"
    #   - "traefik.http.routers.postiz.entrypoints=websecure"
    #   - "traefik.http.routers.postiz.tls=true"
    #   - "traefik.http.services.postiz.loadbalancer.server.port=5000"
    networks:
      - postiz-network
      - temporal-network
    healthcheck:
      test: ["CMD", "node", "-e", "const r=require('http').get('http://localhost:5000/',res=>process.exit(res.statusCode<500?0:1));r.on('error',()=>process.exit(1));r.setTimeout(4000,()=>{r.destroy();process.exit(1)})"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 120s
    depends_on:
      postiz-redis:
        condition: service_healthy
      temporal:
        condition: service_healthy

  # Собственного Postgres-контейнера для Postiz больше нет - используется
  # внешний Postgres 16 на 192.х.х.х.
  # База и пользователь POSTIZ_DB_* должны быть заранее созданы на нем вручную
  # (через pgAdmin), Postiz со своей стороны миграции применит сам при старте.

  postiz-redis:
    image: redis:7.2
    container_name: postiz-redis
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 5s
    volumes:
      - ./data/redis:/data
    networks:
      - postiz-network

  # Мониторинг / отладка приложения - включается через docker compose --profile debug up
  spotlight:
    profiles: [debug]
    pull_policy: always
    container_name: spotlight
    restart: unless-stopped
    ports:
      - "127.0.0.1:8969:8969"
    image: ghcr.io/getsentry/spotlight:latest
    networks:
      - postiz-network

  # -----------------------
  # Temporal Stack
  # -----------------------
  temporal-elasticsearch:
    container_name: temporal-elasticsearch
    image: elasticsearch:7.17.27
    restart: always
    environment:
      - cluster.routing.allocation.disk.threshold_enabled=true
      - cluster.routing.allocation.disk.watermark.low=512mb
      - cluster.routing.allocation.disk.watermark.high=256mb
      - cluster.routing.allocation.disk.watermark.flood_stage=128mb
      - discovery.type=single-node
      - ES_JAVA_OPTS=-Xms256m -Xmx256m
      - xpack.security.enabled=false
    networks:
      - temporal-network
    expose:
      - 9200
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS \"http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=5s\" || exit 1"]
      interval: 10s
      timeout: 10s
      retries: 10
      start_period: 60s
    volumes:
      - ./data/temporal-elasticsearch:/usr/share/elasticsearch/data

  temporal-postgresql:
    container_name: temporal-postgresql
    image: postgres:16
    restart: always
    environment:
      POSTGRES_USER: '${TEMPORAL_DB_USER}'
      POSTGRES_PASSWORD: '${TEMPORAL_DB_PASSWORD}'
    networks:
      - temporal-network
    expose:
      - 5432
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${TEMPORAL_DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    volumes:
      - ./data/temporal-postgres:/var/lib/postgresql/data

  temporal:
    container_name: temporal
    restart: always
    ports:
      - "127.0.0.1:7233:7233"
    image: temporalio/auto-setup:1.28.1
    depends_on:
      temporal-postgresql:
        condition: service_healthy
      temporal-elasticsearch:
        condition: service_healthy
    environment:
      - DB=postgres12
      - DB_PORT=5432
      - POSTGRES_USER=${TEMPORAL_DB_USER}
      - POSTGRES_PWD=${TEMPORAL_DB_PASSWORD}
      - POSTGRES_SEEDS=temporal-postgresql
      - DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml
      - ENABLE_ES=true
      - ES_SEEDS=temporal-elasticsearch
      - ES_VERSION=v7
      - TEMPORAL_NAMESPACE=default
    networks:
      - temporal-network
    healthcheck:
      test: ["CMD", "temporal", "operator", "cluster", "health", "--address", "temporal:7233"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    volumes:
      - ./dynamicconfig:/etc/temporal/config/dynamicconfig
    labels:
      kompose.volume.type: configMap

  temporal-admin-tools:
    container_name: temporal-admin-tools
    image: temporalio/admin-tools:1.28.1-tctl-1.18.4-cli-1.4.1
    restart: on-failure
    environment:
      - TEMPORAL_ADDRESS=temporal:7233
      - TEMPORAL_CLI_ADDRESS=temporal:7233
    networks:
      - temporal-network
    stdin_open: true
    depends_on:
      temporal:
        condition: service_healthy
    tty: true

  temporal-ui:
    container_name: temporal-ui
    image: temporalio/ui:2.34.0
    restart: always
    environment:
      - TEMPORAL_ADDRESS=temporal:7233
      - TEMPORAL_CORS_ORIGINS=http://127.0.0.1:3000
    networks:
      - temporal-network
    ports:
      - "127.0.0.1:8080:8080"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s
    depends_on:
      temporal:
        condition: service_healthy

networks:
  postiz-network:
    external: false
  temporal-network:
    driver: bridge
    name: temporal-network

.env файл

# Домен, на котором будет отвечать Postiz (без https://)
POSTIZ_DOMAIN=postiz.domain.ru

# Сгенерируй случайную строку, например: openssl rand -hex 32
POSTIZ_JWT_SECRET=secret

# База данных Postiz — внешний Postgres 16 (тот же сервер, что у Forgejo)
# База и пользователь должны быть заранее созданы на сервере вручную (pgAdmin)
POSTIZ_DB_HOST=x.x.x.x
POSTIZ_DB_PORT=5432
POSTIZ_DB_USER=postiz
POSTIZ_DB_PASSWORD=postiz
POSTIZ_DB_NAME=postiz

# База данных Temporal (отдельная от базы Postiz)
TEMPORAL_DB_USER=temporal
TEMPORAL_DB_PASSWORD=CHANGE_ME

# Authentik SSO (authentik.secret.ru) — client_id/secret из провайдера, заведенного в Authentik
# POSTIZ_OAUTH_CLIENT_ID=secret
# POSTIZ_OAUTH_CLIENT_SECRET=secret

# Ниже — по мере подключения соцсетей/интеграций, оставь пустыми, если не используешь
X_API_KEY=
X_API_SECRET=
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
REDDIT_CLIENT_ID=
REDDIT_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
BEEHIIVE_API_KEY=
BEEHIIVE_PUBLICATION_ID=
THREADS_APP_ID=
THREADS_APP_SECRET=
FACEBOOK_APP_ID=
FACEBOOK_APP_SECRET=
YOUTUBE_CLIENT_ID=secret
YOUTUBE_CLIENT_SECRET=secret
TIKTOK_CLIENT_ID=
TIKTOK_CLIENT_SECRET=
PINTEREST_CLIENT_ID=
PINTEREST_CLIENT_SECRET=
DRIBBBLE_CLIENT_ID=
DRIBBBLE_CLIENT_SECRET=
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_BOT_TOKEN_ID=
SLACK_ID=
SLACK_SECRET=
SLACK_SIGNING_SECRET=
MASTODON_CLIENT_ID=
MASTODON_CLIENT_SECRET=

OPENAI_API_KEY=
STRIPE_PUBLISHABLE_KEY=
STRIPE_SECRET_KEY=
STRIPE_SIGNING_KEY=
STRIPE_SIGNING_KEY_CONNECT=

DISABLE_REGISTRATION=true

SSO через Authentik - и засада с DISABLE_REGISTRATION
#

У меня уже есть Authentik (authentik.stilicho.ru), через который заведен SSO почти для всего homelab-а, так что первым делом я, разумеется, полез настраивать его и для Postiz. Сама интеграция описана в официальной документации Postiz по OIDC - создаешь в Authentik OAuth2/OpenID-провайдер, прописываешь в Postiz шесть переменных (POSTIZ_GENERIC_OAUTH, POSTIZ_OAUTH_URL, POSTIZ_OAUTH_AUTH_URL, POSTIZ_OAUTH_TOKEN_URL, POSTIZ_OAUTH_USERINFO_URL, POSTIZ_OAUTH_CLIENT_ID/SECRET), и на странице входа вместо кнопок GitHub/Google появляется кнопка твоего провайдера.

Тонкость первая - redirect URI у Postiz не отдельный /callback- путь, как обычно бывает, а просто /settings. Тонкость вторая, из разряда старых знакомых граблей, но с подвохом наоборот, чем можно подумать: POSTIZ_OAUTH_AUTH_URL и соседние переменные у Authentik обязаны заканчиваться слэшем. Начиная с версии 2023.10 Authentik убрал автоматический редирект для путей без завершающего слэша - раньше /application/o/authorize без слэша сам перенаправлялся на нужный адрес, а теперь просто отдает 404. То есть указываем .../authorize/, .../token/, .../userinfo/ - именно со слэшем на конце, иначе получите ошибку 404.

Но была и третья, куда менее очевидная засада, зато самая обидная. Поднимаешь приложение, создаешь админ-аккаунт, потом включаешь DISABLE_REGISTRATION=true (чтобы никто посторонний не мог зарегистрироваться на публично торчащем инстансе, если он у тебя публично выставлен, конечно) … и после настройки Authentik для SSO вход через него просто не срабатывает. Оказалось, дело вообще не в конфигурации OIDC - это подтвержденный баг самого Postiz: пока DISABLE_REGISTRATION включен, OIDC-вход не работает вообще ни для кого, так еще и контейнер не работает. Обсуждение и статус можно посмотреть в issue #807 в репозитории Postiz.

Держать регистрацию постоянно открытой ради того, чтобы работал вход через Authentik, не вариант - это как раз то, от чего я и пытался закрыться. Рабочее решение - обойти встроенный OIDC Postiz целиком и повесить авторизацию через Authentik на уровне самого Traefik (forward auth), точно так же, как у меня уже сделано для сервисов без собственного логина вроде Homepage или arr-стэка. В этом случае до самого Postiz никто не достучится без авторизации в Authentik, а DISABLE_REGISTRATION=true внутри самого Postiz можно спокойно оставить как есть - он тебе там будет уже не нужен как единственная защита.

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

Подключаем YouTube
#

С YouTube все оказалось на удивление предсказуемо. Провайдер в Postiz полноценный, без каких-либо ограничений. Только для self-hosted инстанса придется завести собственное OAuth-приложение в Google Cloud - готовых ключей для self-host версии никто не дает, они есть только у облачной версии.

Заходим на страницу Credentials в Google Cloud, создаем проект (или берем существующий, если он уже есть под другие цели). В разделе “Enabled APIs and Services” включаем сразу три API - YouTube Data API v3, YouTube Analytics API и YouTube Reporting API. Без второй и третьей публикация в целом заработает, но часть функций вроде аналитики и отчетов просто не заведется - лучше включить все сразу, чтобы потом не гадать, почему что-то не отображается.

Если у вас нет ни одного Google Cloud проекта
#

Отдельно скажу тем, кто, как и я, никогда раньше не заводил Google Cloud проектов. С 2022 года Google не регистрирует новых пользователей Cloud из России - на шаге выбора страны ее просто нет в списке. Обходится это через “эту самую технологию” с выходом в страну, которая в списке есть (важно, чтобы IP и указанная при регистрации страна совпадали, иначе шанс словить блокировку по подозрению в фроде выше, чем обычно). Хорошая новость - платежный аккаунт для получения OAuth-ключей не нужен вообще, YouTube Data API v3 работает в рамках бесплатной квоты, так что вопрос упирается только в разовую регистрацию, не в оплату.

Чтобы не рисковать основным каналом, я развел вещи по разным Google-аккаунтам: Cloud-проект завел на отдельном “техническом” адресе (через …), а сам канал авторизовал в Postiz уже под обычным аккаунтом, к которому он привязан. OAuth-приложение может авторизовать доступ к данным любого пользователя, который согласится его подключить - оно не обязано принадлежать владельцу канала. Единственный нюанс - раз приложение остается в статусе Testing (публиковать его незачем), в Google Auth Platform на вкладке Audience нужно явно добавить адрес канала в Test users, иначе при попытке авторизоваться Google покажет “приложение не прошло проверку” именно для этого аккаунта.

Кстати (если вы видели ролики на ютубе про это приложение), сам интерфейс создания OAuth-приложения в Google Cloud в 2026 году успел поменяться - раньше это был “OAuth consent screen”, сейчас это “Google Auth Platform” с мастером из шагов App Information → Audience → Contact Information, и отдельными вкладками Branding/Audience/Data Access/Clients после создания. Суть та же, просто не пугайтесь, если ваш скриншот из чьих-то старых гайдов не совпадает с тем, что видите вы.

Дальше уже в Google Auth Platform, на вкладке Clients - “Create Credentials” → “OAuth client ID”. Тип приложения выбираем Web application, а в “Authorized redirect URIs” прописываем адрес вида:

https://postiz.домен.ru/integrations/social/youtube

(для локальной разработки без домена в документации фигурирует http://localhost:5000/integrations/social/youtube, но нам, с реальным доменом за Traefik, нужен именно первый вариант).

Полученные ключи прописываем в конфиг Postiz:

YOUTUBE_CLIENT_ID="ваш-client-id"
YOUTUBE_CLIENT_SECRET="ваш-client-secret"

Перезапускаем Postiz - и в разделе интеграций уже спокойно жмем “Подключить YouTube”, кнопка есть, все стандартно через привычный OAuth-попап (не забудьте в открывшемся окне выбрать именно аккаунт канала, а не технический, под которым заводили Cloud-проект).

Единственное, что стоит понимать заранее, а не после неудачной попытки опубликовать пост - YouTube в Postiz принимает только видео, и ровно одно вложение на пост. Никаких каруселей из картинок и постов без видео - просто не сработает. Зато из настроек доступны заголовок, приватность (public/unlisted/private), флаг “сделано для детей”, теги и кастомная обложка.

И еще один нюанс - Postiz ничего не знает о том, что уже происходит на самой площадке. Календарь у него строится строго из собственной базы данных: он показывает только то, что было запланировано через сам Postiz, и никак не подтягивает и не синхронизирует то, что вы уже загрузили напрямую через YouTube Studio. Это именно push-инструмент, а не sync. Если начинаете вести расписание через Postiz, все старое, что публиковалось в обход него или до него, там просто не появится.

Подключаем VK и RuTube
#

А вот ничего мы и не подключаем. Объясняю на пальцах. Поддержка VK официально заявлена, и она есть. Все, что тебе надо, - это, как и везде, получить Client Secret и Client ID. Просто же, да? Ко всему прочему, тебе еще и не надо придумывать регистрацию в другой стране. Ага, как же. Получить вожделенные данные можно, только зарегистрировавшись в VK Бизнес, потому что такие данные выдают лишь тем, кто подтвердил себя как разработчика. По мнению администрации VK, конечно.

Дальше - больше: тебе нужно указать свой ИНН, потом указать, кто ты - ИП или самозанятый. Ну и как вишенка на торте, ты должен подтвердить свои данные через СберID, Т-Банк ID или Госуслуги. Я сломался на последнем этапе. Забыл сказать, что постить ты сможешь только в сообщество. С RuTube, который официальной поддержки в Postiz вообще не имеет, я даже не начинал.

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

Итог
#

В сухом остатке - YouTube подключается стандартно и предсказуемо, единственная реальная сложность это Brand Account и терпение на распространение настроек Google. С VK может и получилось бы что-то в итоге, но чтобы до этого дойти, тебе придется пройти “собеседование” с предоставлением анализов, своей родословной и изложением всей своей трудовой деятельности. Как будто я на госслужбу поступаю?! А RuTube в Postiz на данный момент просто нет - и если для вас принципиально видео именно там, придется либо публиковать его руками, либо писать свой провайдер, либо смотреть в сторону стороннего сервиса.

Я в итоге оставил идею использовать Postiz, потому что никакой автоматизации не получилось. Я-то хотел автоматизировать публикацию на трех сервисах, а в итоге получилось только на одном, что назвать автоматизацией ну никак нельзя.

Мне кажется, что честно рассказать, что где-то что-то не работает “из коробки” несмотря на обещания, как по мне, полезнее, чем нарисовать красивую схему, которая на практике развалится на первом же шаге, а в данном случае у меня даже первый шаг сделать не получилось.

Self-Hosting - This article is part of a series.
Part : This Article

Related

BookLore - self-hosted библиотека и читалка для вашей коллекции книг

·1405 слов·7 минут· loading · loading
Подробная инструкция по установке и настройке BookLore: развёртывание в Docker, добавление книг, организация библиотеки и использование встроенной веб-читалки.

Автоматические снапшоты ZFS на Fedora Server 44 с помощью Sanoid

·1690 слов·8 минут· loading · loading
Продолжение цикла статей про домашний NAS на Fedora Server 44. На этот раз настраиваем автоматическое создание и очистку снапшотов ZFS с помощью Sanoid, разбираем политику хранения снимков и systemd-таймеры, а также знакомимся с Syncoid для будущей репликации данных на резервный сервер.

Настройка домашнего NAS на Fedora Server 44 с OpenZFS, Cockpit и Samba

·1076 слов·6 минут· loading · loading
Собираем современный домашний NAS на Fedora Server 44 с файловой системой OpenZFS, веб-интерфейсом Cockpit и файловым сервером Samba. Полная пошаговая инструкция по установке, настройке ZFS, SELinux и публикации SMB-шары.