diff --git a/.env.example b/.env.example index e3ea6c4..74d827e 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ PORT=3000 +HOST=127.0.0.1 +NODE_ENV=production +TRUST_PROXY=1 SESSION_SECRET=change-me-to-a-long-random-string diff --git a/README.md b/README.md index 299eb8d..f32ed81 100644 --- a/README.md +++ b/README.md @@ -40,20 +40,27 @@ cp .env.example .env # Сгенерируйте секрет сессии: sed -i "s/change-me-to-a-long-random-string/$(openssl rand -hex 32)/" .env -# 5. Установка и первый запуск +# 5. Установка приложения npm install --omit=dev + +# 6. Caddy (HTTPS + прокси) — см. раздел ниже; для проверки без домена: npm start ``` -Сайт будет доступен на **http://IP_СЕРВЕРА:3000**. +Без Caddy сайт на **http://IP:3000**. С Caddy и доменом — **https://ваш-домен**. -Порт можно изменить в `.env`: +В `.env` для production задайте (уже есть в `.env.example`): ```env PORT=3000 +HOST=127.0.0.1 +NODE_ENV=production +TRUST_PROXY=1 SESSION_SECRET=ваш-длинный-секрет ``` +`HOST=127.0.0.1` — Node слушает только localhost; снаружи доступ через Caddy. + При первом запуске создаются `data/shop.db`, `data/sessions.db` и демо-товары. --- @@ -72,7 +79,7 @@ After=network.target Type=simple User=www-data WorkingDirectory=/opt/shop -Environment=NODE_ENV=production +EnvironmentFile=/opt/shop/.env ExecStart=/usr/bin/node src/server.js Restart=on-failure RestartSec=5 @@ -94,32 +101,87 @@ systemctl status shop --- -## Nginx (опционально, порт 80/443) +## Caddy — SSL и reverse proxy (рекомендуется) + +[Caddy](https://caddyserver.com/) автоматически выпускает и продлевает сертификаты Let's Encrypt. + +**Перед установкой:** + +1. Домен указывает на IP сервера (A-запись). +2. Открыты порты **80** и **443** в файрволе. +3. Служба `shop` запущена и слушает `127.0.0.1:3000`. + +### Установка Caddy на Ubuntu ```bash -apt install -y nginx - -cat > /etc/nginx/sites-available/shop << 'EOF' -server { - listen 80; - server_name shop.example.com; - - location / { - proxy_pass http://127.0.0.1:3000; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -EOF - -ln -sf /etc/nginx/sites-available/shop /etc/nginx/sites-enabled/ -nginx -t && systemctl reload nginx +apt install -y debian-keyring debian-archive-keyring apt-transport-https +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ + | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ + | tee /etc/apt/sources.list.d/caddy-stable.list +apt update +apt install -y caddy ``` -Замените `shop.example.com` на ваш домен. HTTPS: `apt install certbot python3-certbot-nginx && certbot --nginx`. +### Конфигурация + +В репозитории лежит пример: `caddy/Caddyfile.example`. + +```bash +# Замените shop.example.com и email на свои +cp /opt/shop/caddy/Caddyfile.example /etc/caddy/Caddyfile +nano /etc/caddy/Caddyfile +``` + +Пример `/etc/caddy/Caddyfile`: + +```caddyfile +{ + email admin@example.com +} + +shop.example.com { + encode gzip zstd + reverse_proxy 127.0.0.1:3000 +} +``` + +Проверка и перезапуск: + +```bash +caddy validate --config /etc/caddy/Caddyfile +systemctl enable caddy +systemctl reload caddy +systemctl status caddy +``` + +Сайт: **https://shop.example.com** + +Логи Caddy: `journalctl -u caddy -f` + +### Файрвол (ufw) + +```bash +ufw allow 22/tcp +ufw allow 80/tcp +ufw allow 443/tcp +ufw enable +``` + +Порт **3000** наружу не открывайте — к приложению ходят только через Caddy. + +### Порядок запуска после перезагрузки + +1. `shop` (Node.js на `127.0.0.1:3000`) +2. `caddy` (прокси + HTTPS) + +### Типичные проблемы + +| Симптом | Решение | +|--------|---------| +| 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` | --- @@ -131,6 +193,7 @@ systemctl stop shop git pull npm install --omit=dev systemctl start shop +# Caddy перезагружать не нужно, если Caddyfile не менялся ``` --- @@ -150,6 +213,9 @@ npm run dev | Переменная | Описание | По умолчанию | |------------------|---------------------------|----------------| | `PORT` | Порт HTTP-сервера | `3000` | +| `HOST` | Адрес привязки | `0.0.0.0` (dev), `127.0.0.1` (prod) | +| `NODE_ENV` | Режим (`production` — secure cookies) | — | +| `TRUST_PROXY` | Доверять заголовкам Caddy (`1`) | — | | `SESSION_SECRET` | Секрет для сессий | dev-значение | ## Скрипты npm @@ -176,13 +242,15 @@ cp -a data/shop.db data/shop.db.bak ## Структура проекта ``` +caddy/ + Caddyfile.example — пример reverse proxy + SSL src/ - server.js — точка входа - db.js — схема SQLite - seed.js — демо-данные - routes/ — маршруты - views/ — шаблоны EJS - public/css/ — стили + server.js — точка входа + db.js — схема SQLite + seed.js — демо-данные + routes/ — маршруты + views/ — шаблоны EJS + public/css/ — стили ``` ## Репозиторий diff --git a/caddy/Caddyfile.example b/caddy/Caddyfile.example new file mode 100644 index 0000000..1001281 --- /dev/null +++ b/caddy/Caddyfile.example @@ -0,0 +1,12 @@ +# Скопируйте в /etc/caddy/Caddyfile и замените домен. +# Caddy сам получит и обновит сертификат Let's Encrypt (нужны DNS A-запись и открытые порты 80/443). + +{ + email admin@example.com +} + +shop.example.com { + encode gzip zstd + + reverse_proxy 127.0.0.1:3000 +} diff --git a/src/server.js b/src/server.js index 6847358..8ecf144 100644 --- a/src/server.js +++ b/src/server.js @@ -12,6 +12,12 @@ const authRoutes = require('./routes/auth'); const app = express(); const PORT = process.env.PORT || 3000; +const HOST = process.env.HOST || '0.0.0.0'; +const isProduction = process.env.NODE_ENV === 'production'; + +if (process.env.TRUST_PROXY === '1' || isProduction) { + app.set('trust proxy', 1); +} app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); @@ -29,6 +35,7 @@ app.use( maxAge: 7 * 24 * 60 * 60 * 1000, httpOnly: true, sameSite: 'lax', + secure: isProduction, }, }) ); @@ -55,6 +62,6 @@ app.use((err, req, res, _next) => { }); }); -app.listen(PORT, () => { - console.log(`Магазин: http://localhost:${PORT}`); +app.listen(PORT, HOST, () => { + console.log(`Магазин: http://${HOST}:${PORT}`); });