Welcome to my personal place for love, peace and happiness 🤖

Свой Heroku: запустил Dokku дома и накатил новогоднего :)

Мы привыкли, что для развертывания веб-приложений нужно платить за VPS или разбираться в дебрях Kubernetes. Но если у вас есть домашний сервер (в моем случае — Asustor), вы можете создать свою собственную PaaS-платформу (Platform as a Service), которая работает по принципу *“git push — и готово”*.

Сегодня я расскажу, как настроить Dokku через Portainer, запустить веселое Python-приложение к Новому 2026 году и поделюсь лайфхаками по оптимизации сборки и масштабированию.


Часть 1. Фундамент: Запуск Dokku в Portainer

Dokku — это Docker-контейнер, который управляет другими Docker-контейнерами. Чтобы он заработал на NAS, его нужно правильно запустить. Я использовал Portainer Stack.

Docker Compose конфигурация

Вот рабочий `docker-compose.yml`, который решает главную проблему — доступность приложений из локальной сети.

# version: '3.2'
services:
  agent:
    image: dokku/dokku:${VERSION}
    pid: host.  # ⚠️ важно для работы на NAS
    network_mode: bridge # ⚠️ важно для работы на NAS
    environment:
      DOKKU_HOSTNAME: ${DOKKU_HOSTNAME}
      DOKKU_HOST_ROOT: ${DOKKU_HOST_ROOT}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${VOLUME_PATH:-/var/lib/dokku}:/mnt/dokku
    ports:
      - "3022:22" # ⚠️ важно для работы через ssh и что бы не конфликтовал с 22 
      - "80:80"    # Внешний порт 80 -> порт 80 внутри контейнера Dokku
      - "443:443"

Нюансы сети:
Чтобы обращаться к приложениям по красивым домегам типа `my-app.dokku.datahub.mother`, я настроил AdGuard Home в качестве локального DNS-сервера (фильтры получил бонусом – где-то 20% это всякие счетчики, ужас:). Добавил правило Rewrite: `*.dokku.datahub.mother` → `192.168.0.20` (IP моего NAS). Теперь все поддомены (приложения) автоматически ведут на Dokku.

Также для удобства я настроил `~/.ssh/config` на ноутбуке, чтобы не вводить порты вручную:

Host dokku.datahub.mother
  HostName 192.168.0.20
  Port 3022
  User dokku

Часть 2. Приложение “my-first-app”: Новогоднее гадание

Для теста я написал простое Flask-приложение, которое рассчитывает ваш возраст в наступающем 2026 году.

Код приложения (`app.py`)

from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def home():
    return """
    <h1>Приветствую в игре Нового 2026 года! 🎉</h1>
    <p>Это веселая интерактивная игра в честь Нового года. Угадай свой возраст на 1 января 2026!</p>
    <p>Введи свой год рождения:</p>
    <form action="/result" method="get">
        <input type="number" name="birth_year" min="1900" max="2025" required>
        <button type="submit">Угадать возраст на Новогодний 2026!</button>
    </form>
    """

@app.route('/result')
def result():
    birth_year = request.args.get('birth_year')
    if not birth_year or not birth_year.isdigit():
        return "Ошибка: введи корректный год рождения!"
    
    birth_year = int(birth_year)
    age_in_2026 = 2026 - birth_year
    return f"""
    <h2>Результат: 🎇</h2>
    <p>В 2026 году тебе будет {age_in_2026} лет!</p>
    <p>Счастливого Нового 2026 года! Пусть все твои желания сбудутся!</p>
    <a href="/">Играть снова</a>
    """

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Подготовка к деплою

Чтобы Dokku понял, как запускать это чудо, нужны два файла в корне проекта:

  1. `requirements.txt` (зависимости):
Flask==2.3.2
gunicorn==20.1.0
Werkzeug==2.3.3
  1. `Procfile` (команда запуска):
web: gunicorn app:app --bind 0.0.0.0:5000
  1. `.python-version` (опционально, явная версия Python):
3.11.14

Процесс деплоя

Все делается через Git, как на “взрослых” платформах:

  1. Создаем приложение на сервере:
ssh dokku@dokku.datahub.mother apps:create my-first-app
  1. Отправляем код:
git init
    git add .
    git commit -m "Happy New Year 2026 version"
    git remote add dokku dokku@dokku.datahub.mother:my-first-app
    git push dokku master

После пуша Dokku сам скачает Python, установит Flask и запустит Gunicorn. Через минуту-две приложение доступно по адресу `http://my-first-app.dokku.datahub.mother`.


Часть 3. Уровень PRO: Скорость (uv) и Marimo

Аппетит приходит во время еды. После простого Flask-приложения я решил развернуть что-то посерьезнее — Data Science ноутбук на Marimo, и столкнулся с реальными сложностями и особенностями. Для примера брал их дело ноутбук https://marimo.app

1. Ускорение сборки с `uv`

Стандартный `pip` устанавливает пакеты медленно. Если проект большой, деплой может висеть минутами.
Я перешел на uv — новый менеджер пакетов на Rust.

Вместо `requirements.txt` я использовал `pyproject.toml` и `uv.lock`. Dokku (благодаря современным buildpacks) увидел `uv.lock` и переключился на быстрый режим. Время сборки сократилось в разы.

2. Ловушка масштабирования (Scaling)

Marimo — это stateful приложение (хранит состояние в памяти). Flask, который мы делали выше — stateless.

Когда я задеплоил Marimo, Dokku по умолчанию все было хорошо, но потом я решил масштабировать его и сделал так

ssh dokku@dokku.datahub.mother ps:scale my-marimo-app web=3

далее Dokku запустил 3 копии контейнера (`web=3`).
Начался хаос:

  • Интерфейс открывался.
  • При нажатии кнопок вылетала ошибка `Invalid server token`.

Почему? Браузер загружал страницу с *Контейнера 1*, а WebSocket-запрос улетал в *Контейнер 2*, который ничего не знал про мою сессию.

Решение:
Для интерактивных приложений (Streamlit, Marimo, Jupyter) всегда принудительно ставьте одну реплику:
Ну ли придется делать липкие сессии на nginx или еще что-то.

ssh dokku.datahub.mother ps:scale my-marimo-app web=1 # все вернуло в рабочее состояние.

А если не хватает мощности — лучше дайте этому единственному контейнеру больше ресурсов, чем пытаться плодить клонов или дайте каждому запускать свой:

Вот так можно установить лимиты или повысить их:

ssh dokku.datahub.mother resource:limit my-marimo-app --memory 2G --cpu 2

3. SSL в локальной сети

Браузеры блокируют микрофон и иногда WebSockets на HTTP-сайтах. Для локальной сети Let’s Encrypt не сработает (нет публичного IP), ну и его чуть сложнее запускать.
Я решил вопрос генерацией самоподписанного сертификата одной командой Dokku:

ssh dokku.datahub.mother certs:generate my-first-app my-first-app.dokku.datahub.mother

Браузер ругается, но приложение работает полноценно.


Еще я прогнал стресс тесты

ab -n 10000 -k -c 2000 ...

Много они не показали, решением было подкрутить nginx, настроить кеш ssl, горизонтальное масштабирование не приносило больших результатов. я упирался в ограничения клиента при тестах нагрузки.

Итог

Dokku на домашнем сервере — это отличный инструмент.

  • Для простых API (Flask/FastAPI): Работает “из коробки” идеально.
  • Для сложных задач: Использование `uv` делает работу комфортной, а понимание разницы между *Stateless* и *Stateful* приложениями спасает от занудных ошибок и отладки.

Теперь my-first-app готово предсказывать возраст всем гостям на Новый год, а сервер готов к новым экспериментам! 🎄 Пожалуй оставлю его для будущих экспериментов. Прижился как-то быстро. Кстати у Dokku есть коммерческая PRO версия, а точнее не версия, а полноценный UI с кнопочками и стоит он 900$. https://dokku.com/docs/enterprise/pro/

Пора чего-нибудь накатить новогоднего еще :)

Follow this blog
Send
Share
Tweet
Pin