Разворачиваем приложение Next.js с базой данных PostgreSQL и задачей Cron на облачном сервере Ubuntu Linux

от автора

Привет, друзья!

Предположим, что у нас есть приложение Next.js, данные которого хранятся в Postgres, и мы хотим запустить его в продакшн, но не хотим использовать готовую инфраструктуру Vercel. Что делать? Создать собственную инфраструктуру. К счастью, сделать это не так уж и сложно.

Основные элементы нашей системы:

  • приложение, демонстрирующее несколько мощных возможностей Next.js 15
  • база данных Postgres для хранения списка задач, создаваемых/удаляемых в приложении
  • задача Cron для удаления из БД всех задач каждые 10 мин
  • приложение, БД и задача Cron функционируют в контейнерах Docker
  • контейнеры запускаются с помощью Docker Compose на облачном сервере Ubuntu
  • сервер Nginx для перенаправления запросов HTTP (обратного проксирования)
  • домен, привязанный к серверу
  • Certbot для получения сертификата SSL из Let’s Encrypt и его установки для домена

Демо приложения.

Интересно? Тогда прошу под кат.

Источником вдохновения для написания статьи послужил этот туториал от leerob.

Полезные ссылки:

❯ Подготовка

Для начала работы, кроме 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. Обратите внимание на следующее:

  1. Файл .env должен содержать переменную DATABASE_URL со значением вида postgresql://<POSTGRES_USER>:<POSTGRES_PASSWORD>@<localhost или db>:5432/<POSTGRES_DB>?schema=public (db — это название сервиса docker compose).
  2. В файле prisma/schema.prisma блок generator client должен содержать поле binaryTargets со значением в виде массива поддерживаемых платформ — ["native", "debian-openssl-3.0.x"].
  3. В файле 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 — получение списка запущенных контейнеров Docker
  • docker-compose logs web — отображение логов Next.js
  • docker-compose down — остановка и удаление контейнеров Docker
  • docker-compose up -d — запуск контейнеров в фоновом режиме
  • docker system prune -a — удаление контейнеров, образов и сетей Docker
  • docker volume ls — получение списка томов
  • docker volume rm postgres_data — удаление тома postgres_data
  • sudo systemctl restart nginx — перезапуск Nginx
  • docker exec -it myapp-web-1 sh — подключение к контейнеру Next.js
  • docker exec -it myapp-db-1 psql -U myuser -d mydb — подключение к Postgres

Пожалуй, это все, о чем я хотел рассказать вам в этой статье.

Happy coding!


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале


ссылка на оригинал статьи https://habr.com/ru/articles/858094/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *