Привет, друзья!
Предположим, что у нас есть приложение Next.js, данные которого хранятся в Postgres, и мы хотим запустить его в продакшн, но не хотим использовать готовую инфраструктуру Vercel. Что делать? Создать собственную инфраструктуру. К счастью, сделать это не так уж и сложно.
Основные элементы нашей системы:
- приложение, демонстрирующее несколько мощных возможностей Next.js 15
- база данных Postgres для хранения списка задач, создаваемых/удаляемых в приложении
- задача Cron для удаления из БД всех задач каждые 10 мин
- приложение, БД и задача Cron функционируют в контейнерах Docker
- контейнеры запускаются с помощью Docker Compose на облачном сервере Ubuntu
- сервер Nginx для перенаправления запросов HTTP (обратного проксирования)
- домен, привязанный к серверу
- Certbot для получения сертификата SSL из Let’s Encrypt и его установки для домена
Интересно? Тогда прошу под кат.
Источником вдохновения для написания статьи послужил этот туториал от leerob.
Полезные ссылки:
- Репозиторий с кодом проекта на GitHub
- Облачный сервер и домен
- Next.js
- Руководство по Next.js 14 на русском
- PostgreSQL
- Table Plus
- Docker
- Руководство по Docker на русском
- Prisma
- Руководство по Prisma на русском
- Nginx
- Certbot
- Bash
❯ Подготовка
Для начала работы, кроме Node.js, нам потребуются 3 вещи:
- репозиторий с кодом проекта
- Docker (Docker Desktop)
- сервер и домен
Для локальной разработки я буду использовать VSCode и Windows.
С первыми двумя пунктами все понятно, на последнем остановлюсь подробнее.
Для покупки и настройки сервера и домена я использовал сервис Timeweb Cloud.
Начнем с сервера.
Переходим в раздел «Облачные серверы» и нажимаем «Создать»:
Выбираем «Ubuntu 22.04» и такой вариант в разделе «3. Конфигурация»:
Важно, чтобы оперативной памяти (RAM) было 2 ГБ, минимум.
Нажимаем «Заказать»:
На странице сервера в правой нижней части находятся все необходимые данные для привязки домена и подключения к серверу по SSH:
Переходим в раздел «Домены» и нажимаем «Купить домен»:
Выбираем название домена и заполняем данные администратора (включая email, он потребуется certbot):
Нажимаем «Заказать»:
Переходим в настройки DNS и добавляем 2 записи:
- типа «А» со значением IPv4 сервера
- типа «АААА» со значением IPv6 сервера
После покупки потребуется некоторое время для регистрации и настройки домена, не пугайтесь, если certbot не сможет с первого раза обнаружить его на сервере.
Панель управления проектом:
❯ Локальная разработка и тестирование
Главная страница приложения и файл README.md содержат подробное описание функционала приложения, а код приложения снабжен подробными комментариями, поэтому, с вашего позволения, я перейду сразу к тому, ради чего мы здесь собрались.
Для взаимодействия с БД в приложении используется ORM prisma. Обратите внимание на следующее:
- Файл
.envдолжен содержать переменнуюDATABASE_URLсо значением видаpostgresql://<POSTGRES_USER>:<POSTGRES_PASSWORD>@<localhost или db>:5432/<POSTGRES_DB>?schema=public(db— это название сервиса docker compose). - В файле
prisma/schema.prismaблокgenerator clientдолжен содержать полеbinaryTargetsсо значением в виде массива поддерживаемых платформ —["native", "debian-openssl-3.0.x"]. - В файле
package.json:
- команда
buildперед сборкой приложения выполняет генерацию типов prisma с помощьюnpx prisma generate - команда
start— применяет миграции к БД с помощьюnpx prisma migrate deploy - команда
studioзапускает prisma studio — GUI в браузере для работы с БД
- команда
Для локальной разработки приложения требуется тестовая БД. Запустить ее в контейнере можно с помощью такой команды:
docker run --name db -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=mydb -v postgres_data:/var/lib/postgresql/data -d postgres
-eили--environment— переменная-pили--port— связывание портов-vили--volume— том для постоянного хранения данных (данные в контейнере уничтожаются вместе с контейнером)-dили--detach— автономный режим создания контейнераpostgresилиpostgres:latest— название образа для контейнера
Выполняем команду docker ps для получения списка запущенных контейнеров:
Docker desktop:
Выполняем сборку приложения с помощью команды npm run build:
Запускаем приложение с помощью npm start:
Переходим по адресу http://localhost:3000 и убеждаемся в работоспособности приложения.
БД доступна в prisma studio (
npm run studio):
Останавливаем и удаляем контейнер с БД:
docker stop db docker rm db
Удаляем том:
docker volume rm postgres_data
Не забудьте остановить приложения для освобождения порта 3000.
❯ Контейнеризация приложения
Для создания контейнеризованной системы, включающей в себя приложение, БД и задачу cron, используются файлы Dockerfile и docker-compose.yml.
Dockerfile определяет этапы сборки и запуска приложения:
# образ FROM node:20.16.0 # рабочая директория WORKDIR /app # копируем указанные файлы в корень контейнера COPY package.json package-lock.json ./ # устанавливаем зависимости RUN npm install # копируем остальные файлы в корень контейнера COPY . . # устанавливаем переменную ENV NODE_ENV=production # выполняем сборку приложения RUN npm run build # выставляем порт EXPOSE 3000 # запускаем приложение CMD ["npm", "start"]
Обратите внимание на следующее:
- устанавливаются как производственные зависимости, так и зависимости для разработки для корректного выполнения линтинга (
eslint) и проверки типов (typescript) - результат каждого этапа кешируется докером, повторный запуск выполняется только при изменении скопированных файлов. Файлы
package.jsonиpackage-lock.jsonкопируются отдельно, поскольку мы не хотим переустанавливать зависимости при каждом изменении любого файла приложения
docker-compose.yml определяет контейнеризованные сервисы:
services: # приложение Next.js web: # сборка на основе Dockerfile build: . ports: - '3000:3000' environment: - NODE_ENV=production - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?schema=public # приложение зависит от БД depends_on: - db # внутренняя сеть для коммуникации сервисов networks: - my_network # БД db: image: postgres:latest env_file: .env ports: - '5432:5432' volumes: - postgres_data:/var/lib/postgresql/data networks: - my_network # задача cron cron: image: alpine/curl # https://crontab.guru/ command: > sh -c " echo '*/10 * * * * curl -X POST http://web:3000/db/clear' > /etc/crontabs/root && \ crond -f -l 2 " # cron зависит от приложения depends_on: - web networks: - my_network # тома volumes: postgres_data: # сети networks: my_network: driver: bridge
Обратите внимание на следующее:
- в значении переменной
DATABASE_URLсервисаwebдолжно быть указано название сервиса БД (db) webзависит (depends_on) отdb, аcron— отweb- для того, чтобы сервисы могли взаимодействовать между собой, они должны находиться в одной сети (
network)
Запускаем сервисы с помощью команды docker-compose up -d:
Docker desktop:
Переходим по адресу http://localhost:3000 и убеждаемся в работоспособности приложения.
БД доступна в prisma studio (
npm run studio).
❯ Деплой сервисов на облачном сервере
Вся логика по настройке сервера, установки docker и docker compose, получения и установки сертификата SSL и деплоя контейнеризованных сервисов содержится в файле deploy.sh:
#!/bin/bash # Переменные окружения POSTGRES_USER="myuser" # можно заменить POSTGRES_PASSWORD="postgres" # необходимо заменить POSTGRES_DB="mydb" SECRET_KEY="my-secret" # для демо приложения NEXT_PUBLIC_SAFE_KEY="safe-key" # для демо приложения DOMAIN_NAME="nextselfhost.ru" # необходимо заменить EMAIL="aio350@mail.ru" # необходимо заменить # Переменные для скриптов REPO_URL="https://github.com/harryheman/self-host-nextjs.git" # необходимо заменить APP_DIR=~/myapp SWAP_SIZE="1G" # область подкачки в 1 Гб # Обновляем список пакетов и существующие пакеты sudo apt update && sudo apt upgrade -y # Добавляем область подкачки # https://wiki.astralinux.ru/pages/viewpage.action?pageId=48759505 echo "Добавление области подкачки..." sudo fallocate -l $SWAP_SIZE /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile # Делаем область подкачки постоянной echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # Устанавливаем Docker sudo apt install apt-transport-https ca-certificates curl software-properties-common -y curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" -y sudo apt update sudo apt install docker-ce -y # Устанавливаем Docker Compose sudo rm -f /usr/local/bin/docker-compose sudo curl -L "https://github.com/docker/compose/releases/download/v2.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose # Ждем полной загрузки файла if [ ! -f /usr/local/bin/docker-compose ]; then echo "Провал загрузки Docker Compose. Завершение работы." exit 1 fi sudo chmod +x /usr/local/bin/docker-compose # Проверяем, что Docker Compose является исполняемым и существует в path sudo ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose # Проверяем установку Docker Compose docker-compose --version if [ $? -ne 0 ]; then echo "Провал установки Docker Compose. Завершение работы." exit 1 fi # Запускаем Docker при старте системы и запускаем сервис Docker sudo systemctl enable docker sudo systemctl start docker # Клонируем репозиторий Git if [ -d "$APP_DIR" ]; then echo "Директория $APP_DIR уже существует. Извлечение последних изменений..." cd $APP_DIR && git pull else echo "Клонирование репозитория из $REPO_URL..." git clone $REPO_URL $APP_DIR cd $APP_DIR fi # Для внутренней коммуникации Docker ("db" - название контейнера Postgres) DATABASE_URL="postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@db:5432/$POSTGRES_DB?schema=public" # Создаем файл .env в директории приложения (~/myapp/.env) echo "POSTGRES_USER=$POSTGRES_USER" > "$APP_DIR/.env" echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" >> "$APP_DIR/.env" echo "POSTGRES_DB=$POSTGRES_DB" >> "$APP_DIR/.env" echo "DATABASE_URL=$DATABASE_URL" >> "$APP_DIR/.env" # Переменные для демонстрации echo "SECRET_KEY=$SECRET_KEY" >> "$APP_DIR/.env" echo "NEXT_PUBLIC_SAFE_KEY=$NEXT_PUBLIC_SAFE_KEY" >> "$APP_DIR/.env" # Устанавливаем Nginx sudo apt install nginx -y # Удаляем старые настройки Nginx (при наличии) sudo rm -f /etc/nginx/sites-available/myapp sudo rm -f /etc/nginx/sites-enabled/myapp # Временно останавливаем Nginx для запуска Certbot в автономном режиме sudo systemctl stop nginx # Получаем сертификат SSL с помощью Certbot sudo apt install certbot -y sudo certbot certonly --standalone -d $DOMAIN_NAME --non-interactive --agree-tos -m $EMAIL # Проверяем наличие файлов SSL или генерируем их if [ ! -f /etc/letsencrypt/options-ssl-nginx.conf ]; then sudo wget https://raw.githubusercontent.com/certbot/certbot/main/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf -P /etc/letsencrypt/ fi if [ ! -f /etc/letsencrypt/ssl-dhparams.pem ]; then sudo openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 fi # Создаем настройки Nginx с обратным прокси, поддержкой SSL, # ограничением количества запросов и поддержкой потоковой передачи данных sudo cat > /etc/nginx/sites-available/myapp <<EOL limit_req_zone \$binary_remote_addr zone=mylimit:10m rate=10r/s; server { listen 80; server_name $DOMAIN_NAME; # Перенаправляем все запросы HTTP на HTTPS return 301 https://\$host\$request_uri; } server { listen 443 ssl; server_name $DOMAIN_NAME; ssl_certificate /etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/$DOMAIN_NAME/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # Включаем ограничение количества запросов limit_req zone=mylimit burst=20 nodelay; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host \$host; proxy_cache_bypass \$http_upgrade; # Отключаем буферизацию для поддержки потоков proxy_buffering off; proxy_set_header X-Accel-Buffering no; } } EOL # Создаем символическую ссылку при отсутствии sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp # Перезапускаем Nginx для применения новых настроек sudo systemctl restart nginx # Собираем и запускаем контейнеры Docker из директории приложения (~/myapp) cd $APP_DIR sudo docker-compose up -d # Проверяем запуск Docker Compose if ! sudo docker-compose ps | grep "Up"; then echo "Провал запуска контейнеров Docker. Проверьте логи с помощью 'docker-compose logs'" exit 1 fi # Выводим финальное сообщение echo "Деплой завершен. Приложение Next.js и база данных PostgreSQL запущены. Приложение доступно по адресу: https://$DOMAIN_NAME, база данных - из веб-сервиса. Файл .env был создан и содержит следующие значения: - POSTGRES_USER - POSTGRES_PASSWORD (произвольно сгенерированный) - POSTGRES_DB - DATABASE_URL - SECRET_KEY - NEXT_PUBLIC_SAFE_KEY"
Обратите внимание на следующее:
- значения переменных
DOMAIN_NAME,EMAILиREPO_URLнужно заменить на свои - пароль от БД (
POSTGRES_PASSWORD) должен быть сильным, поскольку БД будет доступна извне (мы рассмотрим один из вариантов того, как это можно сделать, позже). Также опционально можно заменить значениеPOSTGRES_USER
Подключаемся к облачному серверу:
ssh root@193.164.149.235 пароль
Копируем файл deploy.sh из репозитория на сервер:
curl -o ~/deploy.sh https://raw.githubusercontent.com/harryheman/self-host-nextjs/main/deploy.sh
Путь к файлу в репозитории нужно заменить на свой.
Генерируем пароль из 12 произвольных символов:
openssl rand -base64 12
Убедитесь, что пароль не содержит спецсимволов, особенно слэшей, иначе prisma не сможет подключиться к БД.
Копируем пароль и вставляем его в значение переменной POSTGRES_PASSWORD в файле deploy.sh:
nano deploy.sh
Разрешаем выполнение файла deploy.sh и запускаем скрипт:
chmod +x deploy.sh ./deploy.sh
Переходим по адресу https://nextselfhost.ru/ и убеждаемся в работоспособности приложения.
Пример взаимодействия с БД:
Для просмотра и редактирования данных в БД на сервере через GUI локально можно использовать table plus или аналог:
На случай, если вы забыли пароль от БД:
cat deploy.sh # или cd myapp cat .env
Для работы с файлами приложения на сервере из локального VSCode можно использовать расширение Remote — SSH:
Список полезных команд:
docker-compose ps— получение списка запущенных контейнеров Dockerdocker-compose logs web— отображение логов Next.jsdocker-compose down— остановка и удаление контейнеров Dockerdocker-compose up -d— запуск контейнеров в фоновом режимеdocker system prune -a— удаление контейнеров, образов и сетей Dockerdocker volume ls— получение списка томовdocker volume rm postgres_data— удаление томаpostgres_datasudo systemctl restart nginx— перезапуск Nginxdocker exec -it myapp-web-1 sh— подключение к контейнеру Next.jsdocker exec -it myapp-db-1 psql -U myuser -d mydb— подключение к Postgres
Пожалуй, это все, о чем я хотел рассказать вам в этой статье.
Happy coding!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
ссылка на оригинал статьи https://habr.com/ru/articles/858094/

Добавить комментарий