Свой 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 понял, как запускать это чудо, нужны два файла в корне проекта:
- `requirements.txt` (зависимости):
Flask==2.3.2
gunicorn==20.1.0
Werkzeug==2.3.3- `Procfile` (команда запуска):
web: gunicorn app:app --bind 0.0.0.0:5000- `.python-version` (опционально, явная версия Python):
3.11.14Процесс деплоя
Все делается через Git, как на “взрослых” платформах:
- Создаем приложение на сервере:
ssh dokku@dokku.datahub.mother apps:create my-first-app- Отправляем код:
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 23. 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/
Пора чего-нибудь накатить новогоднего еще :)