drop caddy and cloudflared; publish via tailscale funnel; rewrite README

This commit is contained in:
dev
2026-09-08 10:40:20 +03:00
parent 6844d659fc
commit dd5a2ea288
3 changed files with 157 additions and 65 deletions
+2
View File
@@ -0,0 +1,2 @@
ADMIN_PASSWORD=сложный-пароль-админки
DB_PASSWORD=случайная-длинная-строка
+122 -30
View File
@@ -11,8 +11,8 @@
- **Share-ссылки** — публичные страницы-витрины с выбором группы / воспитанника / диапазона дат - **Share-ссылки** — публичные страницы-витрины с выбором группы / воспитанника / диапазона дат
- **Дашборд** — статистика, активные группы, активность за 14 дней, последние записи, топ воспитанников - **Дашборд** — статистика, активные группы, активность за 14 дней, последние записи, топ воспитанников
- **Резервное копирование** — экспорт/импорт полного дампа (БД + файлы) в `tar.gz` - **Резервное копирование** — экспорт/импорт полного дампа (БД + файлы) в `tar.gz`
- **Настройки** — научные тексты футера, анти-спам интервал - **Настройки** — тексты футера, анти-спам интервал
- **HTTPS** — самоподписанный TLS-сертификат по умолчанию + готовая заготовка reverse-proxy (Caddy / Let's Encrypt) для публичного запуска - **Публикация через Tailscale** — приложение открывается по постоянному адресу `https://whatido.<tailnet>.ts.net` без проброса портов, внешнего IP и reverse-proxy
## Технологии ## Технологии
@@ -20,32 +20,39 @@
- PostgreSQL (pg) - PostgreSQL (pg)
- Multer (загрузка файлов), Tar (бэкапы) - Multer (загрузка файлов), Tar (бэкапы)
- Docker / Docker Compose - Docker / Docker Compose
- Tailscale (Serve / Funnel) — публикация по HTTPS
## Быстрый старт ## Быстрый старт
### Требования ### Требования
- Docker + Docker Compose - Linux-хост с Docker и плагином `docker compose`
- Free порта `443` на хосте (его займёт tailscale для Funnel)
- Аккаунт Tailscale (для публикации по ссылке)
### Запуск ### Запуск
```bash ```bash
# создайте .env с паролем администратора и БД (см. раздел «Конфигурация») # 1) создайте .env из примера и задайте свои пароли
cp .env.example .env
$EDITOR .env
# 2) соберите и поднимите стек
docker compose up -d --build docker compose up -d --build
``` ```
После старта: После старта (без публикации через tailscale):
- **HTTP** `http://localhost:3000` — редирект на HTTPS - **HTTP** `http://localhost:3000` — редирект на HTTPS
- **HTTPS** `https://localhost:3443` — приложение (самоподписанный сертификат, принимайте предупреждение браузера) - **HTTPS** `https://localhost:3443` — приложение (самоподписанный сертификат, примите предупреждение браузера)
- **PostgreSQL** — доступен только внутри docker-сети (порт 5432 наружу не публикуется) - **PostgreSQL** — доступен только внутри docker-сети (наружу не публикуется)
Управление: Управление:
```bash ```bash
docker compose ps # статус docker compose ps # статус
docker compose logs -f app docker compose logs -f app # логи приложения
docker compose down # остановка (данные сохраняются) docker compose down # остановка (данные сохраняются)
``` ```
> Приложение **не запустится** без `ADMIN_PASSWORD` (защита от пароля по умолчанию). > Приложение **не запустится** без `ADMIN_PASSWORD` (защита от пароля по умолчанию).
@@ -55,20 +62,114 @@ docker compose down # остановка (данные сохраняютс
Переменные окружения (`.env`): Переменные окружения (`.env`):
| Переменная | По умолчанию | Назначение | | Переменная | По умолчанию | Назначение |
|------------------|--------------|-------------------------------------| |------------------|--------------------|-------------------------------------|
| `ADMIN_PASSWORD` | — (обязательно) | Пароль администратора (X-Admin-Token). Без него сервер не стартует | | `ADMIN_PASSWORD` | — (обязательно) | Пароль администратора (X-Admin-Token). Без него сервер не стартует |
| `DB_PASSWORD` | — (обязательно) | Пароль пользователя `app` в PostgreSQL | | `DB_PASSWORD` | — (обязательно) | Пароль пользователя `app` в PostgreSQL |
Пример `.env`: Пример `.env` (в репозитории — `.env.example`):
``` ```
ADMIN_PASSWORD=сложный-пароль ADMIN_PASSWORD=сложный-пароль
DB_PASSWORD=случайная-строка DB_PASSWORD=случайная-длинная-строка
``` ```
`DB_PASSWORD` подставляется в `docker-compose.yml` в `POSTGRES_PASSWORD` и `DATABASE_URL`. Если БД уже была инициализирована ранее, значение `DB_PASSWORD` должно совпадать с фактическим паролем пользователя `app` в БД (иначе приложение не подключится). `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.<tailnet>.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=<node-id>` и `https://login.tailscale.com/funnel?node=<node-id>`
(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.<tailnet>.ts.net/ (Funnel on)
```
### 4. Готово
- Из любого устройства вашей tailnet: `https://whatido.<tailnet>.ts.net/`
- Из интернета (при включённом Funnel): тот же адрес
- Админка: `https://whatido.<tailnet>.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`) - `groups` — группы, расписание (`day_of_week`, `time_start`, `time_end`)
@@ -104,21 +205,11 @@ DB_PASSWORD=случайная-строка
- **Пароль администратора** обязателен (`ADMIN_PASSWORD`); фолбэка на `admin` нет. - **Пароль администратора** обязателен (`ADMIN_PASSWORD`); фолбэка на `admin` нет.
- **CORS отключён** — кросс-доменные запросы к API запрещены. - **CORS отключён** — кросс-доменные запросы к API запрещены.
- **Rate limiting** по IP на публичные роуты: `POST /api/entries` — 10 запросов / 15 мин, загрузка файлов и share-ссылки — 300 / 15 мин. - **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/`. - **Restore** проходит полную валидацию данных бэкапа; удаление файлов ограничено каталогом `uploads/`.
- **Заголовки**: `helmet` — `X-Frame-Options`, `nosniff`, HSTS, `Referrer-Policy`. - **Заголовки**: `helmet` — `X-Frame-Options`, `nosniff`, HSTS, `Referrer-Policy`.
- **Порт БД** 5432 наружу не публикуется (доступ только внутри docker-сети). - **Порт БД** 5432 наружу не публикуется (доступ только внутри docker-сети).
- **TLS**: по умолчанию самоподписанный сертификат. Для публикации включите reverse-proxy Caddy с Let's Encrypt — заготовка закомментирована в `docker-compose.yml`, конфиг в `Caddyfile.example`. - **TLS**: снаружи HTTPS терминируется Tailscale (сертификат Let's Encrypt для `*.ts.net`); между tailscale и приложением используется самоподписанный сертификат приложения.
### Публичный запуск (TLS)
```bash
cp Caddyfile.example Caddyfile # подставить реальный домен
# расскомментировать сервис caddy и перевести ports сервиса app в expose (следуйте комментариям в docker-compose.yml)
docker compose up -d --build
```
Caddy автоматически получит Let's Encrypt сертификат на 80/443.
## Основные API ## Основные 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-сертификата) ├── Dockerfile # сборка образа (Node 20, генерация TLS-сертификата)
├── Caddyfile.example # шаблон reverse-proxy с Let's Encrypt (домен заменить)
├── server.js # Express-приложение ├── server.js # Express-приложение
├── certs/ # cert.pem приложения (монтируется в tailscale, в git не хранится)
├── db/ ├── db/
│ ├── init.sql # схема при первом запуске │ ├── init.sql # схема при первом запуске
│ └── migration.sql # миграции существующей БД │ └── migration.sql # миграции существующей БД
+31 -33
View File
@@ -19,10 +19,12 @@ services:
app: app:
build: . build: .
# --- Публикация портов через reverse-proxy (Caddy), см. сервис caddy ниже --- expose:
- "3000"
- "3443"
ports: ports:
- "3000:3000" - "127.0.0.1:3000:3000"
- "3443:3443" - "127.0.0.1:3443:3443"
environment: environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/whereldo DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/whereldo
ADMIN_PASSWORD: ${ADMIN_PASSWORD} ADMIN_PASSWORD: ${ADMIN_PASSWORD}
@@ -33,36 +35,32 @@ services:
volumes: volumes:
- ./uploads:/app/uploads - ./uploads:/app/uploads
# --- Настоящий TLS (Let's Encrypt) перед публичным запуском --- # Публикация через Tailscale (Serve / Funnel) без проброса портов.
# # Приложение доступно по https://whatido.<tailnet>.ts.net
# Расскомментируйте сервис caddy, скопируйте Caddyfile.example в Caddyfile, tailscale:
# замените yourdomain.example на реальный домен и в app замените блок ports: image: tailscale/tailscale:latest
# hostname: whatido
# expose: restart: unless-stopped
# - "3000" network_mode: host
# - "3443" entrypoint: ["/bin/sh", "-c"]
# ports: cap_add:
# - "127.0.0.1:3000:3000" - NET_ADMIN
# - "127.0.0.1:3443:3443" (и удалить внешние "3000:3000" / "3443:3443") - SYS_MODULE
# environment:
# Затем: docker compose up -d --build SSL_CERT_FILE: /etc/tailscale/app-certs/cert.pem
# volumes:
# caddy: - /var/lib/tailscale:/var/lib/tailscale
# image: caddy:2-alpine - /dev/net/tun:/dev/net/tun
# restart: unless-stopped - /lib/modules:/lib/modules:ro
# ports: - ./certs:/etc/tailscale/app-certs:ro
# - "80:80" command: >
# - "443:443" "tailscaled &
# environment: sleep 6 &&
# DOMAIN: yourdomain.example # ЗАМЕНИТЕ на реальный домен tailscale up --hostname=whatido --accept-dns=false &&
# volumes: tailscale funnel --bg --yes https://127.0.0.1:3443 &&
# - ./Caddyfile:/etc/caddy/Caddyfile:ro sleep infinity"
# - caddy_data:/data depends_on:
# - caddy_config:/config - app
# depends_on:
# - app
volumes: volumes:
pgdata: pgdata:
# caddy_data:
# caddy_config: