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


Предположим, что у нас есть приложение 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-канале

Комментарии (0)