Привет, друзья!
Предположим, что у нас есть приложение 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_data
sudo 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-канале ↩