diff --git a/README.md b/README.md index f32ed81..b5d9dbb 100644 --- a/README.md +++ b/README.md @@ -70,31 +70,24 @@ SESSION_SECRET=ваш-длинный-секрет Чтобы магазин работал после перезагрузки сервера: ```bash -cat > /etc/systemd/system/shop.service << 'EOF' -[Unit] -Description=Shop Node.js -After=network.target +cp /opt/shop/deploy/shop.service /etc/systemd/system/shop.service -[Service] -Type=simple -User=www-data -WorkingDirectory=/opt/shop -EnvironmentFile=/opt/shop/.env -ExecStart=/usr/bin/node src/server.js -Restart=on-failure -RestartSec=5 +# Зависимости и сборка (от root, до смены владельца) +cd /opt/shop +npm install --omit=dev -[Install] -WantedBy=multi-user.target -EOF - -# Права на каталог (если клонировали в /opt/shop) +# Владелец — тот же пользователь, что в unit (www-data) chown -R www-data:www-data /opt/shop +chmod +x /opt/shop/scripts/diagnose-502.sh systemctl daemon-reload systemctl enable shop systemctl start shop systemctl status shop + +# Backend должен ответить: +curl -s http://127.0.0.1:3000/health +# {"ok":true,"service":"shop"} ``` Логи: `journalctl -u shop -f` @@ -175,11 +168,47 @@ ufw enable 1. `shop` (Node.js на `127.0.0.1:3000`) 2. `caddy` (прокси + HTTPS) -### Типичные проблемы +### HTTP 502 при рабочем SSL + +**SSL есть, 502 — значит Caddy жив, а Node на `127.0.0.1:3000` не отвечает.** + +На сервере выполните: + +```bash +bash /opt/shop/scripts/diagnose-502.sh +journalctl -u shop -n 50 --no-pager +curl -v http://127.0.0.1:3000/health +``` + +**Частые причины и исправление:** + +| Причина | Что сделать | +|--------|-------------| +| Служба `shop` не запущена или падает | `systemctl restart shop`, смотрите логи `journalctl -u shop -f` | +| Нет `npm install` / сломан `better-sqlite3` | `cd /opt/shop && npm install --omit=dev` (нужны `build-essential`, `python3`) | +| Нет прав на `data/` у `www-data` | `mkdir -p /opt/shop/data && chown -R www-data:www-data /opt/shop` | +| В `.env` нет `HOST`/`PORT` | `HOST=127.0.0.1`, `PORT=3000`, затем `systemctl restart shop` | +| Неверный путь к `node` в systemd | `which node` → подставьте в `ExecStart` в `/etc/systemd/system/shop.service` | +| Caddy стартовал раньше shop | `cp deploy/caddy-after-shop.conf /etc/systemd/system/caddy.service.d/shop.conf` и `daemon-reload` | + +**Быстрое восстановление:** + +```bash +cd /opt/shop +git pull +npm install --omit=dev +chown -R www-data:www-data /opt/shop +systemctl restart shop +curl -s http://127.0.0.1:3000/health # должен быть {"ok":true,...} +systemctl reload caddy +``` + +Пока `curl http://127.0.0.1:3000/health` не возвращает OK — HTTPS через Caddy будет отдавать 502. + +### Другие проблемы | Симптом | Решение | |--------|---------| -| 502 Bad Gateway | `systemctl status shop`, проверьте `curl http://127.0.0.1:3000` | | Нет сертификата | DNS, порты 80/443, верный домен в `Caddyfile` | | Редирект-цикл / нет cookies | В `.env`: `TRUST_PROXY=1`, `NODE_ENV=production` | @@ -244,6 +273,10 @@ cp -a data/shop.db data/shop.db.bak ``` caddy/ Caddyfile.example — пример reverse proxy + SSL +deploy/ + shop.service — unit для systemd +scripts/ + diagnose-502.sh — проверка при 502 src/ server.js — точка входа db.js — схема SQLite diff --git a/deploy/caddy-after-shop.conf b/deploy/caddy-after-shop.conf new file mode 100644 index 0000000..4cc5b03 --- /dev/null +++ b/deploy/caddy-after-shop.conf @@ -0,0 +1,8 @@ +# Установка: +# mkdir -p /etc/systemd/system/caddy.service.d +# cp /opt/shop/deploy/caddy-after-shop.conf /etc/systemd/system/caddy.service.d/shop.conf +# systemctl daemon-reload + +[Unit] +After=shop.service +Wants=shop.service diff --git a/deploy/shop.service b/deploy/shop.service new file mode 100644 index 0000000..6a85de9 --- /dev/null +++ b/deploy/shop.service @@ -0,0 +1,20 @@ +[Unit] +Description=Shop Node.js +After=network.target + +[Service] +Type=simple +User=www-data +Group=www-data +WorkingDirectory=/opt/shop +EnvironmentFile=/opt/shop/.env +# Путь к node: which node (часто /usr/bin/node) +ExecStart=/usr/bin/node src/server.js +Restart=on-failure +RestartSec=5 + +# Права на запись в data/ и node_modules (если нужно) +UMask=0022 + +[Install] +WantedBy=multi-user.target diff --git a/scripts/diagnose-502.sh b/scripts/diagnose-502.sh new file mode 100644 index 0000000..d17a952 --- /dev/null +++ b/scripts/diagnose-502.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Диагностика HTTP 502 (Caddy не достучался до Node) +set -e + +echo "=== Shop / Caddy 502 diagnostic ===" +echo + +echo "1. Служба shop" +systemctl is-active shop 2>/dev/null || echo " shop: не установлена или не active" +systemctl status shop --no-pager -l 2>/dev/null | head -20 || true +echo + +echo "2. Служба caddy" +systemctl is-active caddy 2>/dev/null || true +echo + +echo "3. Порт 3000" +if command -v ss >/dev/null; then + ss -tlnp | grep ':3000' || echo " Ничего не слушает порт 3000 — запустите shop" +else + netstat -tlnp 2>/dev/null | grep ':3000' || echo " Порт 3000 не слушается" +fi +echo + +echo "4. curl backend" +if curl -sf --max-time 3 http://127.0.0.1:3000/health; then + echo + echo " OK — Node отвечает, проблема скорее в Caddyfile" +else + echo " FAIL — Node не отвечает на 127.0.0.1:3000" + echo " Проверьте: journalctl -u shop -n 50 --no-pager" +fi +echo + +echo "5. .env (HOST, PORT)" +if [ -f /opt/shop/.env ]; then + grep -E '^(HOST|PORT|NODE_ENV)=' /opt/shop/.env || true +else + echo " /opt/shop/.env не найден" +fi +echo + +echo "6. Права data/" +if [ -d /opt/shop/data ]; then + ls -la /opt/shop/data +else + echo " Каталог data/ отсутствует — создайте: mkdir -p /opt/shop/data && chown www-data:www-data /opt/shop/data" +fi +echo + +echo "7. Node" +which node || which nodejs || echo " node не найден в PATH" +node -v 2>/dev/null || nodejs -v 2>/dev/null || true diff --git a/src/routes/health.js b/src/routes/health.js new file mode 100644 index 0000000..18b8094 --- /dev/null +++ b/src/routes/health.js @@ -0,0 +1,15 @@ +const express = require('express'); +const { db } = require('../db'); + +const router = express.Router(); + +router.get('/health', (_req, res) => { + try { + db.prepare('SELECT 1').get(); + res.json({ ok: true, service: 'shop' }); + } catch (err) { + res.status(503).json({ ok: false, error: err.message }); + } +}); + +module.exports = router; diff --git a/src/server.js b/src/server.js index 8ecf144..3cd1b7f 100644 --- a/src/server.js +++ b/src/server.js @@ -7,6 +7,7 @@ require('./db'); require('./seed'); const { loadUser } = require('./middleware/auth'); +const healthRoutes = require('./routes/health'); const shopRoutes = require('./routes/shop'); const authRoutes = require('./routes/auth'); @@ -22,6 +23,8 @@ if (process.env.TRUST_PROXY === '1' || isProduction) { app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); +app.use(healthRoutes); + app.use(express.static(path.join(__dirname, 'public'))); app.use(express.urlencoded({ extended: true })); @@ -62,6 +65,11 @@ app.use((err, req, res, _next) => { }); }); -app.listen(PORT, HOST, () => { +const server = app.listen(PORT, HOST, () => { console.log(`Магазин: http://${HOST}:${PORT}`); }); + +server.on('error', (err) => { + console.error('Не удалось запустить сервер:', err.message); + process.exit(1); +});