feat: Caddy reverse proxy, SSL и инструкция для Ubuntu

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shop
2026-05-16 21:00:57 +03:00
parent f1d377684b
commit ccebf0d26d
4 changed files with 124 additions and 34 deletions
+3
View File
@@ -1,2 +1,5 @@
PORT=3000 PORT=3000
HOST=127.0.0.1
NODE_ENV=production
TRUST_PROXY=1
SESSION_SECRET=change-me-to-a-long-random-string SESSION_SECRET=change-me-to-a-long-random-string
+94 -26
View File
@@ -40,20 +40,27 @@ cp .env.example .env
# Сгенерируйте секрет сессии: # Сгенерируйте секрет сессии:
sed -i "s/change-me-to-a-long-random-string/$(openssl rand -hex 32)/" .env sed -i "s/change-me-to-a-long-random-string/$(openssl rand -hex 32)/" .env
# 5. Установка и первый запуск # 5. Установка приложения
npm install --omit=dev npm install --omit=dev
# 6. Caddy (HTTPS + прокси) — см. раздел ниже; для проверки без домена:
npm start npm start
``` ```
Сайт будет доступен на **http://IP_СЕРВЕРА:3000**. Без Caddy сайт на **http://IP:3000**. С Caddy и доменом — **https://ваш-домен**.
Порт можно изменить в `.env`: В `.env` для production задайте (уже есть в `.env.example`):
```env ```env
PORT=3000 PORT=3000
HOST=127.0.0.1
NODE_ENV=production
TRUST_PROXY=1
SESSION_SECRET=ваш-длинный-секрет SESSION_SECRET=ваш-длинный-секрет
``` ```
`HOST=127.0.0.1` — Node слушает только localhost; снаружи доступ через Caddy.
При первом запуске создаются `data/shop.db`, `data/sessions.db` и демо-товары. При первом запуске создаются `data/shop.db`, `data/sessions.db` и демо-товары.
--- ---
@@ -72,7 +79,7 @@ After=network.target
Type=simple Type=simple
User=www-data User=www-data
WorkingDirectory=/opt/shop WorkingDirectory=/opt/shop
Environment=NODE_ENV=production EnvironmentFile=/opt/shop/.env
ExecStart=/usr/bin/node src/server.js ExecStart=/usr/bin/node src/server.js
Restart=on-failure Restart=on-failure
RestartSec=5 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 ```bash
apt install -y nginx apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
cat > /etc/nginx/sites-available/shop << 'EOF' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
server { curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
listen 80; | tee /etc/apt/sources.list.d/caddy-stable.list
server_name shop.example.com; apt update
apt install -y caddy
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
``` ```
Замените `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 git pull
npm install --omit=dev npm install --omit=dev
systemctl start shop systemctl start shop
# Caddy перезагружать не нужно, если Caddyfile не менялся
``` ```
--- ---
@@ -150,6 +213,9 @@ npm run dev
| Переменная | Описание | По умолчанию | | Переменная | Описание | По умолчанию |
|------------------|---------------------------|----------------| |------------------|---------------------------|----------------|
| `PORT` | Порт HTTP-сервера | `3000` | | `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-значение | | `SESSION_SECRET` | Секрет для сессий | dev-значение |
## Скрипты npm ## Скрипты npm
@@ -176,6 +242,8 @@ cp -a data/shop.db data/shop.db.bak
## Структура проекта ## Структура проекта
``` ```
caddy/
Caddyfile.example — пример reverse proxy + SSL
src/ src/
server.js — точка входа server.js — точка входа
db.js — схема SQLite db.js — схема SQLite
+12
View File
@@ -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
}
}
+9 -2
View File
@@ -12,6 +12,12 @@ const authRoutes = require('./routes/auth');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; 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('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views')); app.set('views', path.join(__dirname, 'views'));
@@ -29,6 +35,7 @@ app.use(
maxAge: 7 * 24 * 60 * 60 * 1000, maxAge: 7 * 24 * 60 * 60 * 1000,
httpOnly: true, httpOnly: true,
sameSite: 'lax', sameSite: 'lax',
secure: isProduction,
}, },
}) })
); );
@@ -55,6 +62,6 @@ app.use((err, req, res, _next) => {
}); });
}); });
app.listen(PORT, () => { app.listen(PORT, HOST, () => {
console.log(`Магазин: http://localhost:${PORT}`); console.log(`Магазин: http://${HOST}:${PORT}`);
}); });