diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8aa3396 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +certbot +node_modules +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ac9ec18 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20-alpine + +WORKDIR /app + +# Устанавливаем зависимости +COPY package*.json ./ +RUN npm ci + +# Копируем исходники +COPY . . + +# Prisma client + +# 🔥 ВАЖНО: билд Next.js +RUN npm run build + +EXPOSE 3000 + +CMD ["npm", "run", "start"] diff --git a/FreshVPSInstGuide.md b/FreshVPSInstGuide.md new file mode 100644 index 0000000..65abf8e --- /dev/null +++ b/FreshVPSInstGuide.md @@ -0,0 +1,203 @@ +**AUTOMATICALLY:** +curl -L -o setup.sh https://raw.githubusercontent.com/DeepDight/tt-tournament/instdockervpsnginx/setup.sh +chmod +x setup.sh +sudo ./setup.sh + +sudo usermod -aG docker $USER +echo "⚠️ Чтобы изменения вступили в силу, выйдите из сессии и зайдите снова, или выполните: newgrp docker" +docker ps + +curl -L -o deploy.sh https://raw.githubusercontent.com/DeepDight/tt-tournament/instdockervpsnginx/deploy.sh +chmod +x deploy.sh +./deploy.sh + +**MANUAL:** +**Только, что созданная VPS:** + +sudo apt update && sudo apt upgrade -y (Обновление пакетов) + +**Установка docker:** + +sudo apt install -y ca-certificates curl gnupg lsb-release + +sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +sudo apt update +sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +sudo apt install -y docker.io + +git clone https://github.com/GlebkaF/tt-tournament + +**Начало контента(создание всего, что только можно):** + +docker network create tt-network(создаем все сами, из-за того, что не используем докеркомпоз) + +docker volume create tt-postgres-data(чтобы после рестарта базы не пропали) + +docker run -d \ + --name tt-postgres \ + --network tt-network \ + -e POSTGRES_DB=tt_tournament \ + -e POSTGRES_USER=tournament_user \ + -e POSTGRES_PASSWORD=strong_password \ + -p 5433:5432 \ + -v tt-postgres-data:/var/lib/postgresql/data \ + --restart unless-stopped \ + --health-cmd='pg_isready -U tournament_user' \ + --health-interval=5s \ + --health-timeout=5s \ + --health-retries=5 \ + postgres:16 + + +**Проверка** + +docker ps +docker logs tt-postgres + +**Запускаем nginx в Docker** + +docker run -d \ + --name tt-nginx \ + --network tt-network \ + -p 80:80 \ + -v $(pwd)/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \ + nginx:alpine + +Проверка(Если все в порядке, сайт должен открываться без :3000) +docker exec -it tt-nginx wget -qO- http://tt-app:3000 + + +**Создаем образ** + +docker build -t tt-app . + +**Запускаем контейнер приложения:** + +docker run -d \ + --name tt-app \ + --network tt-network \ + -e DATABASE_URL="postgresql://tournament_user:strong_password@tt-postgres:5432/tt_tournament" \ + -e BASIC_AUTH_USERNAME=admin \ + -e BASIC_AUTH_PASSWORD=admin1 \ + -p 3000:3000 \ + --restart unless-stopped \ + tt-app + +На этом моменте сайт is working, но мы не перенесли дамп базы данных. + +**Качаем дамп бд ну или создаем новую** + +docker run --rm -e PGPASSWORD=Password postgres:16 \ + pg_dump -h link.com \ + -U UserName \ + -p 5432 \ + -d DBName \ + -F c > neon_tt_tournament.dump + +Проверим скачался ли файл? +ls -lh neon_tt_tournament.dump + +docker cp neon_tt_tournament.dump tt-postgres:/neon_tt_tournament.dump + +docker exec -i tt-postgres pg_restore \ + -U tournament_user \ + -C \ + -d postgres \ + --no-owner \ + --no-privileges \ + /neon_tt_tournament.dump + +**После того, как сделали все выше, делаем так, чтобы контейнеры запускались автоматически при старте VPS:** + +docker update --restart=always tt-app +docker update --restart=always tt-postgres +docker update --restart=always tt-nginx + +**Подарок для друзей, подключение сертификатов к сайту:** + +docker rm -f tt-nginx + +mkdir -p certbot/conf +mkdir -p certbot/www + +Временно меняем nginx.conf: + +nano nginx/nginx.conf + +events {} + +http { + server { + listen 80; + server_name example.com www.example.com; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 404; + } + } +} + +Запускаем: + +docker run -d \ + --name tt-nginx \ + --network tt-network \ + -p 80:80 \ + -v $(pwd)/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \ + -v $(pwd)/certbot/www:/var/www/certbot \ + nginx:alpine + +Получаем сертификат: + +docker run --rm \ + -v $(pwd)/certbot/conf:/etc/letsencrypt \ + -v $(pwd)/certbot/www:/var/www/certbot \ + certbot/certbot certonly \ + --webroot \ + --webroot-path=/var/www/certbot \ + -d example.com \ + -d www.example.com \ + --email your@email.com \ + --agree-tos \ + --no-eff-email + +Возвращаем назад исходный nginx.conf +Перезапускаем nginx: + +docker rm -f tt-nginx + +docker run -d \ + --name tt-nginx \ + --network tt-network \ + -p 80:80 \ + -p 443:443 \ + -v $(pwd)/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \ + -v $(pwd)/certbot/conf:/etc/letsencrypt \ + -v $(pwd)/certbot/www:/var/www/certbot \ + nginx:alpine + + + + +**Создание дампа:** +docker exec -t tt-postgres pg_dump -U tournament_user -F c -v tt_tournament > tt_tournament.dump +Проверка создался ли: +ls -lh tt_tournament.dump + +**Восстановление бд:** +docker exec -it tt-postgres psql -U tournament_user -d tt_tournament +DROP SCHEMA public CASCADE; +CREATE SCHEMA public; + +После этого: +docker cp neon_tt_tournament.dump tt-postgres:/neon_tt_tournament.dump +docker exec -i tt-postgres pg_restore -U tournament_user -d tt_tournament --no-owner --no-privileges /neon_tt_tournament.dump diff --git a/app/matches/page.tsx b/app/matches/page.tsx index 414b86d..8400ef0 100644 --- a/app/matches/page.tsx +++ b/app/matches/page.tsx @@ -1,3 +1,4 @@ +export const dynamic = "force-dynamic"; import { Player } from "@/app/interface"; import MatchPage from "@/component/MatchPage"; import { Metadata } from "next"; diff --git a/app/players/[id]/page.tsx b/app/players/[id]/page.tsx index e5294d5..fdbc9e2 100644 --- a/app/players/[id]/page.tsx +++ b/app/players/[id]/page.tsx @@ -1,3 +1,4 @@ +export const dynamic = "force-dynamic"; import PlayerProfile from "@/component/PlayerProfile"; import { Metadata } from "next"; diff --git a/app/schedule/page.tsx b/app/schedule/page.tsx index f94dd55..94dd47b 100644 --- a/app/schedule/page.tsx +++ b/app/schedule/page.tsx @@ -1,3 +1,4 @@ +export const dynamic = "force-dynamic"; import { Player } from "@/app/interface"; import Schedule from "@/component/Schedule"; diff --git a/app/test/rating/page.tsx b/app/test/rating/page.tsx index 6f01ec6..7fe95a4 100644 --- a/app/test/rating/page.tsx +++ b/app/test/rating/page.tsx @@ -1,3 +1,4 @@ +export const dynamic = "force-dynamic"; import { Match, PrismaClient } from "@prisma/client"; import { Player } from "@/app/interface"; import React from "react"; @@ -141,15 +142,13 @@ function calculateGlicko2Ratings( if (match.result === "PLAYER1_WIN") { score1 = 1.0; score2 = 0.0; - result = `${playerDetails.get(match.player1Id)?.firstName} ${ - playerDetails.get(match.player1Id)?.lastName - }`; + result = `${playerDetails.get(match.player1Id)?.firstName} ${playerDetails.get(match.player1Id)?.lastName + }`; } else if (match.result === "PLAYER2_WIN") { score1 = 0.0; score2 = 1.0; - result = `${playerDetails.get(match.player2Id)?.firstName} ${ - playerDetails.get(match.player2Id)?.lastName - }`; + result = `${playerDetails.get(match.player2Id)?.firstName} ${playerDetails.get(match.player2Id)?.lastName + }`; } else { score1 = 0.5; score2 = 0.5; diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..3a71f07 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env bash +set -e + +echo "=== TT Tournament auto-deploy started ===" + +# ----------------------------- +# Переменные +# ----------------------------- +REPO_URL="https://github.com/DeepDight/tt-tournament.git" +REPO_BRANCH="instdockervpsnginx" +APP_DIR="$HOME/tt-tournament" + +APP_NAME="tt-app" +POSTGRES_CONTAINER="tt-postgres" +NGINX_CONTAINER="tt-nginx" +NETWORK="tt-network" +VOLUME="tt-postgres-data" + +POSTGRES_DB="tt_tournament" +POSTGRES_USER="tournament_user" +POSTGRES_PORT="5433" + +CERTBOT_DIR="$APP_DIR/certbot" + +# ----------------------------- +# Клонирование репозитория +# ----------------------------- +echo ">>> Клонирование репозитория" +if [ -d "$APP_DIR" ]; then + echo "⚠️ $APP_DIR уже существует, используем его" +else + git clone $REPO_URL $APP_DIR +fi + +cd $APP_DIR +git fetch +git checkout $REPO_BRANCH +git pull origin $REPO_BRANCH + +# ----------------------------- +# Docker network & volume +# ----------------------------- +echo ">>> Создание docker network и volume" +docker network inspect $NETWORK >/dev/null 2>&1 || docker network create $NETWORK +docker volume inspect $VOLUME >/dev/null 2>&1 || docker volume create $VOLUME + +# ----------------------------- +# Ввод паролей и домена +# ----------------------------- +echo ">>> Ввод паролей и домена" +read -s -p "Пароль для локального PostgreSQL: " POSTGRES_PASSWORD >> Запуск PostgreSQL" +docker rm -f $POSTGRES_CONTAINER 2>/dev/null || true + +docker run -d \ + --name $POSTGRES_CONTAINER \ + --network $NETWORK \ + -e POSTGRES_DB=$POSTGRES_DB \ + -e POSTGRES_USER=$POSTGRES_USER \ + -e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \ + -p ${POSTGRES_PORT}:5432 \ + -v $VOLUME:/var/lib/postgresql/data \ + --restart unless-stopped \ + --health-cmd="pg_isready -U $POSTGRES_USER" \ + --health-interval=5s \ + --health-timeout=5s \ + --health-retries=5 \ + postgres:16 + +echo ">>> Ожидание PostgreSQL" +sleep 10 + +# ----------------------------- +# Загрузка дампа (опционально) +# ----------------------------- +echo ">>> Загрузить дамп из Neon?" +read -p "Загрузить дамп? (y/n): " LOAD_DUMP >> Скачивание дампа из Neon" + docker run --rm \ + -e PGPASSWORD="$NEON_PASSWORD" \ + postgres:16 \ + pg_dump -h "$NEON_HOST" \ + -U "$NEON_USER" \ + -p 5432 \ + -d "$NEON_DB" \ + -F c > "$APP_DIR/neon_tt_tournament.dump" + + echo ">>> Копирование дампа в локальный контейнер PostgreSQL" + docker cp "$APP_DIR/neon_tt_tournament.dump" $POSTGRES_CONTAINER:/neon_tt_tournament.dump + + echo ">>> Восстановление дампа в локальную базу" + docker exec -i $POSTGRES_CONTAINER pg_restore \ + -U $POSTGRES_USER \ + -d $POSTGRES_DB \ + --no-owner \ + --no-privileges \ + /neon_tt_tournament.dump + + echo "✅ Дамп успешно восстановлен" +fi + +# ----------------------------- +# Подготовка certbot +# ----------------------------- +mkdir -p "$CERTBOT_DIR/conf" "$CERTBOT_DIR/www" + +# ----------------------------- +# Временный nginx для certbot +# ----------------------------- +echo ">>> Запуск временного nginx для certbot" +docker rm -f $NGINX_CONTAINER 2>/dev/null || true + +cat > nginx/nginx-temp.conf <>> Генерация Let's Encrypt сертификата для $DOMAIN" + + docker run --rm \ + -v $CERTBOT_DIR/conf:/etc/letsencrypt \ + -v $CERTBOT_DIR/www:/var/www/certbot \ + certbot/certbot certonly \ + --webroot \ + --webroot-path=/var/www/certbot \ + -d $DOMAIN \ + -d www.$DOMAIN \ + --email your@email.com \ + --agree-tos \ + --no-eff-email \ + --non-interactive \ + --keep-until-expiring +else + echo "✅ Сертификат для $DOMAIN уже существует, пропускаем генерацию" +fi + +docker rm -f $NGINX_CONTAINER + +# ----------------------------- +# Сборка приложения +# ----------------------------- +if docker image inspect "$APP_NAME" >/dev/null 2>&1; then + echo ">>> Docker image '$APP_NAME' уже существует, пропускаем сборку" +else + echo ">>> Сборка Docker image '$APP_NAME'" + docker build -t "$APP_NAME" . +fi + +# ----------------------------- +# nginx с HTTPS +# ----------------------------- +cat > nginx/nginx.conf <>> Запуск nginx с HTTPS" +docker rm -f $NGINX_CONTAINER 2>/dev/null || true + +docker run -d \ + --name $NGINX_CONTAINER \ + --network $NETWORK \ + -p 80:80 -p 443:443 \ + -v $APP_DIR/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \ + -v $CERTBOT_DIR/conf:/etc/letsencrypt \ + -v $CERTBOT_DIR/www:/var/www/certbot \ + nginx:alpine + +# ----------------------------- +# Запуск приложения +# ----------------------------- +echo ">>> Запуск приложения" +docker rm -f $APP_NAME 2>/dev/null || true + +docker run -d \ + --name $APP_NAME \ + --network $NETWORK \ + -e DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_CONTAINER}:5432/${POSTGRES_DB}" \ + -e BASIC_AUTH_USERNAME=admin \ + -e BASIC_AUTH_PASSWORD=$BASIC_AUTH_PASSWORD \ + -p 3000:3000 \ + --restart unless-stopped \ + $APP_NAME + +docker exec -it $APP_NAME npx prisma generate +docker restart $APP_NAME + +# ----------------------------- +# Автозапуск +# ----------------------------- +docker update --restart=always $APP_NAME +docker update --restart=always $POSTGRES_CONTAINER +docker update --restart=always $NGINX_CONTAINER + +echo "✅ Deploy completed successfully" +echo "🌍 Сайт доступен по https://$DOMAIN" +echo "📂 Репозиторий находится в $APP_DIR, можно зайти: cd $APP_DIR" diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..2d080fa --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,34 @@ +events {} + +http { + upstream app_backend { + server tt-app:3000; + } + + server { + listen 80; + server_name new.ebtt.ru www.new.ebtt.ru; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl; + server_name new.ebtt.ru www.new.ebtt.ru; + + ssl_certificate /etc/letsencrypt/live/new.ebtt.ru/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/new.ebtt.ru/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + location / { + proxy_pass http://app_backend; + 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; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +} \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..0dce8c3 --- /dev/null +++ b/setup.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -e + +echo "=== TT Tournament setup (update & Docker install) ===" + +# ----------------------------- +# Обновление системы +# ----------------------------- +echo ">>> Обновление системы" +sudo apt update && sudo apt upgrade -y + +# ----------------------------- +# Проверка и установка Docker +# ----------------------------- +if ! command -v docker &> /dev/null; then + echo ">>> Docker не найден, устанавливаем Docker CE" + + # Добавляем официальный репозиторий Docker + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + sudo chmod a+r /etc/apt/keyrings/docker.gpg + + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + + sudo apt update + sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + echo "✅ Docker установлен" +else + echo "✅ Docker уже установлен" +fi