feat: Caddy reverse proxy, SSL и инструкция для Ubuntu
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user