From dd5a2ea288f8ac3b6c1479fc3dc1bd1c2028a825 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 8 Sep 2026 10:40:20 +0300 Subject: [PATCH] drop caddy and cloudflared; publish via tailscale funnel; rewrite README --- .env.example | 2 + README.md | 154 ++++++++++++++++++++++++++++++++++++--------- docker-compose.yml | 66 ++++++++++--------- 3 files changed, 157 insertions(+), 65 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..97099ce --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +ADMIN_PASSWORD=сложный-пароль-админки +DB_PASSWORD=случайная-длинная-строка \ No newline at end of file diff --git a/README.md b/README.md index d779da4..095e4bd 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ - **Share-ссылки** — публичные страницы-витрины с выбором группы / воспитанника / диапазона дат - **Дашборд** — статистика, активные группы, активность за 14 дней, последние записи, топ воспитанников - **Резервное копирование** — экспорт/импорт полного дампа (БД + файлы) в `tar.gz` -- **Настройки** — научные тексты футера, анти-спам интервал -- **HTTPS** — самоподписанный TLS-сертификат по умолчанию + готовая заготовка reverse-proxy (Caddy / Let's Encrypt) для публичного запуска +- **Настройки** — тексты футера, анти-спам интервал +- **Публикация через Tailscale** — приложение открывается по постоянному адресу `https://whatido..ts.net` без проброса портов, внешнего IP и reverse-proxy ## Технологии @@ -20,32 +20,39 @@ - PostgreSQL (pg) - Multer (загрузка файлов), Tar (бэкапы) - Docker / Docker Compose +- Tailscale (Serve / Funnel) — публикация по HTTPS ## Быстрый старт ### Требования -- Docker + Docker Compose +- Linux-хост с Docker и плагином `docker compose` +- Free порта `443` на хосте (его займёт tailscale для Funnel) +- Аккаунт Tailscale (для публикации по ссылке) ### Запуск ```bash -# создайте .env с паролем администратора и БД (см. раздел «Конфигурация») +# 1) создайте .env из примера и задайте свои пароли +cp .env.example .env +$EDITOR .env + +# 2) соберите и поднимите стек docker compose up -d --build ``` -После старта: +После старта (без публикации через tailscale): - **HTTP** `http://localhost:3000` — редирект на HTTPS -- **HTTPS** `https://localhost:3443` — приложение (самоподписанный сертификат, принимайте предупреждение браузера) -- **PostgreSQL** — доступен только внутри docker-сети (порт 5432 наружу не публикуется) +- **HTTPS** `https://localhost:3443` — приложение (самоподписанный сертификат, примите предупреждение браузера) +- **PostgreSQL** — доступен только внутри docker-сети (наружу не публикуется) Управление: ```bash -docker compose ps # статус -docker compose logs -f app -docker compose down # остановка (данные сохраняются) +docker compose ps # статус +docker compose logs -f app # логи приложения +docker compose down # остановка (данные сохраняются) ``` > Приложение **не запустится** без `ADMIN_PASSWORD` (защита от пароля по умолчанию). @@ -55,20 +62,114 @@ docker compose down # остановка (данные сохраняютс Переменные окружения (`.env`): -| Переменная | По умолчанию | Назначение | -|------------------|--------------|-------------------------------------| -| `ADMIN_PASSWORD` | — (обязательно) | Пароль администратора (X-Admin-Token). Без него сервер не стартует | -| `DB_PASSWORD` | — (обязательно) | Пароль пользователя `app` в PostgreSQL | +| Переменная | По умолчанию | Назначение | +|------------------|--------------------|-------------------------------------| +| `ADMIN_PASSWORD` | — (обязательно) | Пароль администратора (X-Admin-Token). Без него сервер не стартует | +| `DB_PASSWORD` | — (обязательно) | Пароль пользователя `app` в PostgreSQL | -Пример `.env`: +Пример `.env` (в репозитории — `.env.example`): ``` ADMIN_PASSWORD=сложный-пароль -DB_PASSWORD=случайная-строка +DB_PASSWORD=случайная-длинная-строка ``` `DB_PASSWORD` подставляется в `docker-compose.yml` в `POSTGRES_PASSWORD` и `DATABASE_URL`. Если БД уже была инициализирована ранее, значение `DB_PASSWORD` должно совпадать с фактическим паролем пользователя `app` в БД (иначе приложение не подключится). +Имя узла Tailscale задаётся в `docker-compose.yml` (`tailscale.hostname`, по умолчанию `whatido`). + +## Публичный доступ через Tailscale + +Стек не требует внешнего IP и проброса портов: контейнер `tailscale` запускается с `network_mode: host`, входит в вашу tailnet-сеть и через **Serve** открывает приложение внутри tailnet, а через **Funnel** — в публичном интернете. + +Цепочка: + +``` +Интернет / tailnet → https://whatido..ts.net (TLS от Tailscale) + → localhost:443 контейнера tailscale + → https://127.0.0.1:3443 (приложение, самоподписанный cert) +``` + +Контейнер доверяет самоподписанному сертификату приложения через `SSL_CERT_FILE=/etc/tailscale/app-certs/cert.pem` (файл монтируется из `./certs/cert.pem`). + +### 1. Вход в tailnet при первом запуске + +При первом старте контейнер автоматически выполняет `tailscale up` и печатает ссылку для авторизации — откройте её в браузере и добавьте устройство в аккаунт. Ввести устройство вручную там не нужно: `tailscale status` покажет статус: + +```bash +docker exec -it whatido-tailscale-1 tailscale status +``` + +Если авторизация по какой-то причине не прошла, выполните вход вручную: + +```bash +docker exec -it whatido-tailscale-1 tailscale up --hostname=whatido +# откроется ссылка вида https://login.tailscale.com/a/... — войдите в браузер +``` + +### 2. Включите функции и найдите адрес + +После авторизации узел получит имя вида `whatido` и адрес: + +```bash +docker exec -it whatido-tailscale-1 tailscale status +# что-то вроде: whatido.taile47725.ts.net 100.x.x.x receiver online +``` + +Чтобы публиковать сайт, для tailnet должны быть включены: + +- **HTTPS Certificates** — автоматически выдается при первом Serve/Funnel +- **Serve** (доступ из tailnet) и **Funnel** (доступ из интернета) — включаются в админ-консоли Tailscale, например по прямой ссылке на узел: + `https://login.tailscale.com/f/serve?node=` и `https://login.tailscale.com/funnel?node=` + (id узла берётся из `tailscale status`) + +### 3. Запуск Serve / Funnel + +Контейнер при старте сам выполняет: + +```bash +tailscale funnel --bg --yes https://127.0.0.1:3443 +``` + +(`funnel` включает и serve-часть; флаг `--bg` — работа в фоне, `--yes` — не спрашивать подтверждения.) + +Если сервис уже запущен и команды в compose не отработали (например, функции только что включили в консоли), выполните вручную: + +```bash +docker exec whatido-tailscale-1 tailscale funnel --bg --yes https://127.0.0.1:3443 +``` + +Проверить состояние: + +```bash +docker exec whatido-tailscale-1 tailscale funnel status +# https://whatido..ts.net/ (Funnel on) +``` + +### 4. Готово + +- Из любого устройства вашей tailnet: `https://whatido..ts.net/` +- Из интернета (при включённом Funnel): тот же адрес +- Админка: `https://whatido..ts.net/admin` + +### Важные замечания + +- **Порт 443 на хосте должен быть свободен** — tailscale слушает его напрямую (поэтому у сервиса `network_mode: host`, а приложение опубликовано на `127.0.0.1:3000/3443`). +- **Сертификат приложения**: контейнер приложения генерирует self-signed `cert.pem` при сборке образа (это не секрет — публичный сертификат). Он монтируется в tailscale через `./certs/cert.pem`. Если образ приложения пересобирали впервые на новом хосте — скопируйте сертификат и перезапустите tailscale: + + ```bash + docker compose up -d --build app + docker cp $(docker compose ps -q app):/app/certs/cert.pem certs/cert.pem + docker compose up -d tailscale + ``` + +- **`/lib/modules`** монтируется в контейнер tailscale, чтобы на LC/дистрибутивах без авто-загрузки модулей корректно инициализировался netfilter (иначе `tailscaled` падает с `Table does not exist` и узел мигает online/offline). +- **Ограничения сети**: если у провайдера нет глобального IPv6 и соединения к ПК-ядрам Tailscale нестабильные (CGNAT), отвечать наружу узел может с перебоями — классический признак: `tailscale status` показывает `online`, а из интернета URL не открывается. Внутри tailnet сайт работает всегда. + +## Альтернативная публикация (Caddy) + +Исторически проект публиковался через reverse-proxy Caddy + Let's Encrypt (файлы `Caddyfile.example`, закомментированный сервис в старых версиях compose). Если нужно классическое публичное HTTPS на собственном домене с проброшенными портами 80/443 — этот вариант остаётся возможным: раскомментируйте/восстановите сервис `caddy`, укажите домен в `Caddyfile` и уберите сетевые блокировки. По умолчанию сейчас рекомендуется Tailscale-схема выше. + ## Структура данных - `groups` — группы, расписание (`day_of_week`, `time_start`, `time_end`) @@ -104,21 +205,11 @@ DB_PASSWORD=случайная-строка - **Пароль администратора** обязателен (`ADMIN_PASSWORD`); фолбэка на `admin` нет. - **CORS отключён** — кросс-доменные запросы к API запрещены. - **Rate limiting** по IP на публичные роуты: `POST /api/entries` — 10 запросов / 15 мин, загрузка файлов и share-ссылки — 300 / 15 мин. -- **Загрузки** ограничены: 30 МБ суммарно на запись, 10 МБ на файл; заблокированы опасные расширения (`.html`, `.js`, `.svg`, `.xml`, `.exe` и др.); SVG иным способом не отдаётся inline. +- **Загрузки** ограничены: 30 МБ суммарно на запись, 10 МБ на файл; заблокированы опасные расширения (`.html`, `.js`, `.svg`, `.xml`, `.exe` и др.); SVG не отдаётся inline. - **Restore** проходит полную валидацию данных бэкапа; удаление файлов ограничено каталогом `uploads/`. - **Заголовки**: `helmet` — `X-Frame-Options`, `nosniff`, HSTS, `Referrer-Policy`. - **Порт БД** 5432 наружу не публикуется (доступ только внутри docker-сети). -- **TLS**: по умолчанию самоподписанный сертификат. Для публикации включите reverse-proxy Caddy с Let's Encrypt — заготовка закомментирована в `docker-compose.yml`, конфиг в `Caddyfile.example`. - -### Публичный запуск (TLS) - -```bash -cp Caddyfile.example Caddyfile # подставить реальный домен -# расскомментировать сервис caddy и перевести ports сервиса app в expose (следуйте комментариям в docker-compose.yml) -docker compose up -d --build -``` - -Caddy автоматически получит Let's Encrypt сертификат на 80/443. +- **TLS**: снаружи HTTPS терминируется Tailscale (сертификат Let's Encrypt для `*.ts.net`); между tailscale и приложением используется самоподписанный сертификат приложения. ## Основные API @@ -144,10 +235,11 @@ Caddy автоматически получит Let's Encrypt сертифика ## Структура проекта ``` -├── docker-compose.yml # сервисы app + db (+ закомментированный caddy) +├── docker-compose.yml # сервисы: app + db + tailscale +├── .env.example # шаблон переменных окружения ├── Dockerfile # сборка образа (Node 20, генерация TLS-сертификата) -├── Caddyfile.example # шаблон reverse-proxy с Let's Encrypt (домен заменить) ├── server.js # Express-приложение +├── certs/ # cert.pem приложения (монтируется в tailscale, в git не хранится) ├── db/ │ ├── init.sql # схема при первом запуске │ └── migration.sql # миграции существующей БД @@ -155,4 +247,4 @@ Caddy автоматически получит Let's Encrypt сертифика ├── scripts/ # вспомогательные скрипты ├── uploads/ # загруженные файлы (bind-монт, вне git) └── backups/ # локальные бэкапы -``` +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 431fdfb..f10c5b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,10 +19,12 @@ services: app: build: . - # --- Публикация портов через reverse-proxy (Caddy), см. сервис caddy ниже --- + expose: + - "3000" + - "3443" ports: - - "3000:3000" - - "3443:3443" + - "127.0.0.1:3000:3000" + - "127.0.0.1:3443:3443" environment: DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/whereldo ADMIN_PASSWORD: ${ADMIN_PASSWORD} @@ -33,36 +35,32 @@ services: volumes: - ./uploads:/app/uploads - # --- Настоящий TLS (Let's Encrypt) перед публичным запуском --- - # - # Расскомментируйте сервис caddy, скопируйте Caddyfile.example в Caddyfile, - # замените yourdomain.example на реальный домен и в app замените блок ports: - # - # expose: - # - "3000" - # - "3443" - # ports: - # - "127.0.0.1:3000:3000" - # - "127.0.0.1:3443:3443" (и удалить внешние "3000:3000" / "3443:3443") - # - # Затем: docker compose up -d --build - # - # caddy: - # image: caddy:2-alpine - # restart: unless-stopped - # ports: - # - "80:80" - # - "443:443" - # environment: - # DOMAIN: yourdomain.example # ЗАМЕНИТЕ на реальный домен - # volumes: - # - ./Caddyfile:/etc/caddy/Caddyfile:ro - # - caddy_data:/data - # - caddy_config:/config - # depends_on: - # - app + # Публикация через Tailscale (Serve / Funnel) без проброса портов. + # Приложение доступно по https://whatido..ts.net + tailscale: + image: tailscale/tailscale:latest + hostname: whatido + restart: unless-stopped + network_mode: host + entrypoint: ["/bin/sh", "-c"] + cap_add: + - NET_ADMIN + - SYS_MODULE + environment: + SSL_CERT_FILE: /etc/tailscale/app-certs/cert.pem + volumes: + - /var/lib/tailscale:/var/lib/tailscale + - /dev/net/tun:/dev/net/tun + - /lib/modules:/lib/modules:ro + - ./certs:/etc/tailscale/app-certs:ro + command: > + "tailscaled & + sleep 6 && + tailscale up --hostname=whatido --accept-dns=false && + tailscale funnel --bg --yes https://127.0.0.1:3443 && + sleep infinity" + depends_on: + - app volumes: - pgdata: - # caddy_data: - # caddy_config: + pgdata: \ No newline at end of file