Snapshot of uncommitted work: landing, bots, prod infra, partner cabinet, backoffice stage 1
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
d452824c62
314 files changed
+44394
-121
No files matched your search
@@ -0,0 +1,37 @@
|
||||
# Barev Club — образы для продакшена.
|
||||
# app — Node: API + воркер + боты (один процесс) и лендинг Next.js (тот же образ)
|
||||
# web — nginx: статика Mini App и админки + прокси /trpc и /files на API
|
||||
FROM node:22-alpine AS deps
|
||||
RUN corepack enable
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM deps AS build
|
||||
# адреса, которые зашиваются в клиентский код при сборке
|
||||
ARG LANDING_URL
|
||||
ARG API_PUBLIC_URL
|
||||
ARG CLIENT_BOT_USERNAME=barevclub_bot
|
||||
ENV VITE_LANDING_URL=$LANDING_URL NEXT_PUBLIC_API_URL=$API_PUBLIC_URL CLIENT_BOT_USERNAME=$CLIENT_BOT_USERNAME NEXT_TELEMETRY_DISABLED=1
|
||||
RUN pnpm --filter @barev/miniapp-client build \
|
||||
&& pnpm --filter @barev/miniapp-pro build \
|
||||
&& pnpm --filter @barev/admin build \
|
||||
&& pnpm --filter @barev/landing build
|
||||
|
||||
FROM nginx:alpine AS web
|
||||
COPY --from=build /repo/apps/miniapp-client/dist /srv/client
|
||||
COPY --from=build /repo/apps/miniapp-pro/dist /srv/pro
|
||||
COPY --from=build /repo/apps/admin/dist /srv/admin
|
||||
COPY infra/prod/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
FROM node:22-alpine AS app
|
||||
RUN corepack enable
|
||||
WORKDIR /repo
|
||||
ENV NODE_ENV=production TZ=Asia/Yerevan
|
||||
COPY --from=deps /repo /repo
|
||||
# лендинг: standalone-сборка со своими node_modules
|
||||
COPY --from=build /repo/apps/landing/.next/standalone /landing
|
||||
COPY --from=build /repo/apps/landing/.next/static /landing/apps/landing/.next/static
|
||||
EXPOSE 4000 3000
|
||||
# при старте: миграции и синхронизация каталога, затем API + воркер + боты
|
||||
CMD ["sh", "-c", "cd packages/db && ./node_modules/.bin/prisma migrate deploy && ./node_modules/.bin/tsx src/scripts/sync-catalog.ts && cd /repo && exec apps/runtime/node_modules/.bin/tsx apps/runtime/index.ts"]
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Деплой на VPS. Образы собираются на самом сервере (нативный amd64; под эмуляцией на Mac pnpm падает),
|
||||
# после сборки кеш чистится — сервер общий и диск небольшой.
|
||||
# infra/prod/deploy.sh — синхронизировать код, собрать, перезапустить
|
||||
# ONLY=app infra/prod/deploy.sh — собрать только образ app (API/воркер/боты/лендинг)
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
HOST=${DEPLOY_HOST:-root@69.62.120.231}
|
||||
KEY=${DEPLOY_KEY:-$HOME/.ssh/vps_claude}
|
||||
DIR=/opt/barev
|
||||
DOMAIN=${BAREV_DOMAIN:-barevclub.com}
|
||||
BOT=${CLIENT_BOT_USERNAME:-barevclub_bot}
|
||||
r() { ssh -i "$KEY" "$HOST" "$@"; }
|
||||
|
||||
# один деплой за раз: два параллельных запуска ломают пересоздание контейнеров (конфликт имён → 502)
|
||||
LOCK=$DIR/.deploy-lock
|
||||
if ! r "mkdir -p $DIR && mkdir $LOCK 2>/dev/null"; then
|
||||
echo "✗ на сервере уже идёт деплой (есть $LOCK). Дождитесь его окончания."
|
||||
echo " Если прошлый деплой оборвался, снимите блокировку: ssh -i $KEY $HOST rmdir $LOCK"
|
||||
exit 1
|
||||
fi
|
||||
trap 'r "rmdir $LOCK" 2>/dev/null || true' EXIT
|
||||
|
||||
echo "→ синхронизация кода"
|
||||
r "mkdir -p $DIR/src"
|
||||
rsync -az --delete -e "ssh -i $KEY" \
|
||||
--exclude node_modules --exclude .next --exclude dist --exclude .turbo --exclude '*.tsbuildinfo' \
|
||||
--exclude .git --exclude .env --exclude docs/design --exclude i18n-review --exclude '/*.pdf' \
|
||||
./ "$HOST:$DIR/src/"
|
||||
scp -q -i "$KEY" infra/prod/docker-compose.yml "$HOST:$DIR/docker-compose.yml"
|
||||
|
||||
# секреты генерируются один раз и дальше не перезаписываются
|
||||
if ! r "test -f $DIR/.env"; then
|
||||
echo "→ создаю $DIR/.env с новыми секретами"
|
||||
rnd() { openssl rand -hex 24; }
|
||||
DBP=$(rnd); S3K=$(rnd); ADMINP=$(openssl rand -base64 12 | tr -d '/+=')
|
||||
sed -e "s|^DB_PASSWORD=.*|DB_PASSWORD=$DBP|" \
|
||||
-e "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://barev:$DBP@barev-db:5432/barev|" \
|
||||
-e "s|^ADMIN_SESSION_SECRET=.*|ADMIN_SESSION_SECRET=$(rnd)|" \
|
||||
-e "s|^SEED_OWNER_PASSWORD=.*|SEED_OWNER_PASSWORD=$ADMINP|" \
|
||||
-e "s|^S3_SECRET_KEY=.*|S3_SECRET_KEY=$S3K|" \
|
||||
-e "s|^CLIENT_BOT_TOKEN=.*|CLIENT_BOT_TOKEN=$(grep ^CLIENT_BOT_TOKEN= .env | cut -d= -f2-)|" \
|
||||
-e "s|^PRO_BOT_TOKEN=.*|PRO_BOT_TOKEN=$(grep ^PRO_BOT_TOKEN= .env | cut -d= -f2-)|" \
|
||||
infra/prod/env.example | r "umask 077 && cat > $DIR/.env"
|
||||
sed "s|barev-secret|$S3K|" infra/prod/seaweed-s3.json.template | r "cat > $DIR/seaweed-s3.json"
|
||||
# ключ шифрования файлов в S3 (SSE-S3)
|
||||
printf '[s3.sse]\nkek = "%s"\n' "$(openssl rand -hex 32)" | r "cat > $DIR/security.toml && chmod 644 $DIR/security.toml"
|
||||
echo " пароль владельца админки: $ADMINP"
|
||||
fi
|
||||
|
||||
for target in ${ONLY:-web app}; do
|
||||
echo "→ сборка barev-$target"
|
||||
r "cd $DIR/src && nice -n 10 docker build --network host -f infra/prod/Dockerfile --target $target \
|
||||
--build-arg LANDING_URL=https://$DOMAIN --build-arg API_PUBLIC_URL=https://api.$DOMAIN \
|
||||
--build-arg CLIENT_BOT_USERNAME=$BOT -t barev-$target:latest . > /tmp/barev-build-$target.log 2>&1 \
|
||||
|| (tail -40 /tmp/barev-build-$target.log; exit 1)"
|
||||
done
|
||||
|
||||
echo "→ запуск"
|
||||
r "cd $DIR && docker compose up -d --remove-orphans && docker builder prune -af >/dev/null && docker image prune -f >/dev/null"
|
||||
r "cd $DIR && docker compose ps --format '{{.Name}}\t{{.Status}}' && df -h / | tail -1"
|
||||
@@ -0,0 +1,105 @@
|
||||
# Barev Club — продакшен на общем VPS.
|
||||
# Наружу смотрит только barev-web (в сети proxy Nginx Proxy Manager), остальное — во внутренней сети.
|
||||
name: barev
|
||||
|
||||
x-env: &env
|
||||
env_file: .env
|
||||
|
||||
services:
|
||||
barev-db:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: barev
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: barev
|
||||
TZ: Asia/Yerevan
|
||||
volumes: [db:/var/lib/postgresql/data]
|
||||
networks: [internal]
|
||||
mem_limit: 256m
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U barev']
|
||||
interval: 10s
|
||||
retries: 10
|
||||
|
||||
barev-redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server --save 60 1 --maxmemory 64mb --maxmemory-policy noeviction
|
||||
volumes: [redis:/data]
|
||||
networks: [internal]
|
||||
mem_limit: 96m
|
||||
|
||||
# S3-совместимое хранилище для фотоотчётов и сканов (приватный бакет)
|
||||
barev-s3:
|
||||
image: chrislusf/seaweedfs:latest
|
||||
restart: unless-stopped
|
||||
command: server -dir=/data -s3 -s3.port=9000 -s3.config=/etc/seaweed/s3.json -master.volumeSizeLimitMB=64 -volume.max=10
|
||||
volumes:
|
||||
- s3:/data
|
||||
- ./seaweed-s3.json:/etc/seaweed/s3.json:ro
|
||||
# ключ шифрования SSE-S3 (s3.sse.kek): файлы шифруются при записи
|
||||
- ./security.toml:/etc/seaweedfs/security.toml:ro
|
||||
networks: [internal]
|
||||
mem_limit: 192m
|
||||
|
||||
# API + воркер + оба бота
|
||||
barev-app:
|
||||
image: barev-app:latest
|
||||
<<: *env
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
barev-db: { condition: service_healthy }
|
||||
barev-redis: { condition: service_started }
|
||||
barev-s3: { condition: service_started }
|
||||
networks: [internal]
|
||||
mem_limit: 512m
|
||||
|
||||
barev-landing:
|
||||
image: barev-app:latest
|
||||
<<: *env
|
||||
restart: unless-stopped
|
||||
working_dir: /landing
|
||||
command: ['node', 'apps/landing/server.js']
|
||||
environment:
|
||||
PORT: '3000'
|
||||
HOSTNAME: 0.0.0.0
|
||||
networks: [internal]
|
||||
mem_limit: 192m
|
||||
|
||||
barev-web:
|
||||
image: barev-web:latest
|
||||
restart: unless-stopped
|
||||
depends_on: [barev-app, barev-landing]
|
||||
networks: [internal, proxy]
|
||||
mem_limit: 64m
|
||||
|
||||
# ежедневный бэкап БД, хранится 14 дней
|
||||
barev-backup:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PGPASSWORD: ${DB_PASSWORD}
|
||||
volumes: [backups:/backups]
|
||||
networks: [internal]
|
||||
mem_limit: 64m
|
||||
entrypoint:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
while true; do
|
||||
pg_dump -h barev-db -U barev -Fc barev > /backups/barev-$$(date +%F).dump && echo "backup ok"
|
||||
find /backups -name '*.dump' -mtime +14 -delete
|
||||
sleep 86400
|
||||
done
|
||||
|
||||
networks:
|
||||
internal: {}
|
||||
proxy:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
db: {}
|
||||
redis: {}
|
||||
s3: {}
|
||||
backups: {}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Продакшен-окружение (на сервере: /opt/barev/.env). Секреты генерирует infra/prod/deploy.sh при первом деплое.
|
||||
NODE_ENV=production
|
||||
TZ=Asia/Yerevan
|
||||
DB_PASSWORD=__generated__
|
||||
DATABASE_URL=postgresql://barev:__generated__@barev-db:5432/barev
|
||||
REDIS_URL=redis://barev-redis:6379
|
||||
|
||||
CLIENT_BOT_TOKEN=
|
||||
PRO_BOT_TOKEN=
|
||||
CLIENT_BOT_USERNAME=barevclub_bot
|
||||
PRO_BOT_USERNAME=barevworker_bot
|
||||
TG_INIT_DATA_MAX_AGE=86400
|
||||
|
||||
LANDING_DOMAIN=barevclub.com
|
||||
LANDING_URL=https://barevclub.com
|
||||
CLIENT_MINIAPP_URL=https://app.barevclub.com
|
||||
PRO_MINIAPP_URL=https://pro.barevclub.com
|
||||
ADMIN_URL=https://admin.barevclub.com
|
||||
# серверный адрес API для лендинга (внутри docker-сети)
|
||||
API_URL=http://barev-app:4000
|
||||
API_PORT=4000
|
||||
|
||||
ADMIN_SESSION_SECRET=__generated__
|
||||
SEED_OWNER_EMAIL=owner@barev.local
|
||||
SEED_OWNER_PASSWORD=__generated__
|
||||
|
||||
S3_ENDPOINT=http://barev-s3:9000
|
||||
S3_REGION=us-east-1
|
||||
S3_BUCKET=barev-private
|
||||
S3_ACCESS_KEY=barev
|
||||
S3_SECRET_KEY=__generated__
|
||||
|
||||
SENTRY_DSN=
|
||||
|
||||
# Лендинг: контакты в подвале и в JSON-LD (пусто — не показываются)
|
||||
CONTACT_EMAIL=
|
||||
# Подтверждение сайта в Search Console / Яндекс Вебмастере / Bing (только код из meta-тега; можно вместо этого подтвердить через DNS)
|
||||
GOOGLE_SITE_VERIFICATION=
|
||||
YANDEX_VERIFICATION=
|
||||
BING_SITE_VERIFICATION=
|
||||
|
||||
# Страница /ru/apartments: квартиры AREV из CRM (crm.yandp.tech) и заявки на просмотр.
|
||||
# Ключ — companies.settings.barevClub.key организации AREV в CRM. Пусто — страница показывает «список недоступен».
|
||||
CRM_URL=https://crm.yandp.tech
|
||||
CRM_BAREV_KEY=
|
||||
@@ -0,0 +1,85 @@
|
||||
# Шлюз Barev: Nginx Proxy Manager (TLS) → этот контейнер → статика / API / лендинг.
|
||||
# Хост определяется по первому сегменту домена: app.*, pro.*, admin.*, api.*, иначе — лендинг.
|
||||
|
||||
map $host $barev_site {
|
||||
~^app\. client;
|
||||
~^pro\. pro;
|
||||
~^admin\. admin;
|
||||
~^api\. api;
|
||||
default landing;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
client_max_body_size 12m;
|
||||
server_tokens off;
|
||||
|
||||
# www.barevclub.com → barevclub.com: один адрес для SEO и CORS (API разрешает только основной домен)
|
||||
if ($host ~* ^www\.(.+)$) {
|
||||
return 301 https://$1$request_uri;
|
||||
}
|
||||
|
||||
# NPM терминирует TLS и передаёт схему в X-Forwarded-Proto: http → https для всех хостов Barev.
|
||||
# ACME-проверки Let's Encrypt NPM обрабатывает сам, сюда они не доходят.
|
||||
if ($http_x_forwarded_proto = "http") {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||
|
||||
# API доступен с любого хоста Barev — Mini App и админка ходят в него с того же origin
|
||||
location /trpc/ {
|
||||
proxy_pass http://barev-app:4000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
location /files/ {
|
||||
proxy_pass http://barev-app:4000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
location = /client-log {
|
||||
proxy_pass http://barev-app:4000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
location = /health {
|
||||
proxy_pass http://barev-app:4000;
|
||||
}
|
||||
|
||||
location / {
|
||||
if ($barev_site = landing) {
|
||||
proxy_pass http://barev-landing:3000;
|
||||
}
|
||||
if ($barev_site = api) {
|
||||
return 404;
|
||||
}
|
||||
root /srv/$barev_site;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# index.html Mini App и админки не кешируем: иначе Telegram держит старую страницу со ссылкой на старый бандл
|
||||
location = /index.html {
|
||||
if ($barev_site = landing) {
|
||||
proxy_pass http://barev-landing:3000;
|
||||
}
|
||||
root /srv/$barev_site;
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
|
||||
# статика с хешами в имени — кешируем надолго
|
||||
location /assets/ {
|
||||
root /srv/$barev_site;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"identities": [
|
||||
{
|
||||
"name": "barev",
|
||||
"credentials": [{ "accessKey": "barev", "secretKey": "barev-secret" }],
|
||||
"actions": ["Admin", "Read", "Write", "List", "Tagging"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user