Начну с того, что однажды мне захотелось создать приложение. Желание такое возникло из-за того, что я люблю читать, а нормальных книжных агрегаторов на просторах русского интернета просто нет. Собственно из боли поиска чего бы почитать и попыток вспомнить как называлась та книжка, которую я недавно читал и на какой же главе я остановился, родилось желание сделать веб-приложение, в котором всё это было бы возможно и удобно. Стоит отметить, что никакого опыта разработки, программирования и т.п. у меня не было, моя работа вообще с этим не связана. Тем не менее желание перебороло лень и переросло в конкретные действия, своеобразное хобби.
Не буду рассказывать как я изучал javascript, node.js, react, html, css и т.п., перейдём к тому, к чему я пришел на данный момент, чем хотел бы с вами поделится и, конечно, послушать конструктивную критику специалистов.
Как и многие я тренировался на собственном ПК на localhost:3000, создавал front/back-end'ы, верстал, работал с api и т.д., но меня всегда тревожила мысль а том, как же всё это потом перенести на хостинг? Будет ли оно работать? Нужно ли будет переписывать из-за этого код? И самое главное, нельзя ли всё настроить так, чтобы я мог работать над приложением с любого ПК и легко переносить всё на хостинг на production? Об этом я и расскажу.
Выбор хостинга
На своё хобби я готов был тратить 10$ в месяц, поэтому выбирал тот хостинг, с которым планировал и остаться в будущем. Как я и говорил, до этого у меня был 0 опыт, в том числе и с хостингом сайтов. Я попробовал и отказался от следующих:
Jelastic: красивый и удобный интерфейс, вроде всё интуитивно, масштабируемо и понятно. Тем не менее столкнулся с трудностями при настройке (nginx почему-то из vps не хотел работать, только их отдельным модулем) и подключении SSL(и автоматическом обновлении) к русскоязычном домену стандартными средствами (обещали баг пофиксить, но я не хочу ждать)
Облачный хостинг REG.RU: тут же у меня и домен, поэтому решение казалось логичным, однако у них не было отдельно настроенного PostgreSQL, а так как с администрированием базы мне связываться не хотелось, начал искать дальше.
AWS и Google облака: попробовал, всё вроде хорошо, но вспомнил про наши «замечательные» законы и требование размешать данные пользователей на сервера в РФ. У этих ребят, к сожалению, серверов в РФ не оказалось. Не юрист, но от греха решил поискать облака с серверами в РФ. Если же ваше приложение маловероятно будет иметь проблемы с законом, то хороший выбор.
Облака с серверами в РФ хоть и были, но хотелось всё же что-то, что избавит меня от необходимости погружаться в администрирование PostgreSQL. Порыв немного наткнулся на не так давно ставшие доступными Яндекс.Облака, попробовал, вроде всё просто и удобно, поэтому остановился пока на них. Стоит отметить, что хостинг PostgreSQL у них сразу идёт с 1core и 4гб RAM, что по стоимости около 2к рублей в месяц, поэтому на время разработки и невысокой нагрузки я планирую запустить PostgreSQL на VPS за ~300р, а с повышением нагрузки перенести базу и пусть Яндекс занимается администрированием и обновлением.
Настройка Яндекс.Облака
Virtual Private Cloud
1) Создаём каталог под свой сайт:
2) Создаём Virtual Private Cloud:
Главное что он даёт для меня на текущем этапе — это IP для доступа к созданному ресурсу из вне. С подсетями, зонами, изоляцией и отказоустойчивостью ознакомился поверхностно, при необходимости наверстаю.
3) Создаём подсеть и присваиваем ей внутренний IP (как я понял, это примерно как локальная сеть)
4) Переходим на вкладку IP и резервируем себе статический IP.
По нему мы будем подключаться из дома и других мест. Вероятно можно и с динамическим работать, но я не разбирался в каких случаях он меняется.
Compute Cloud
Тут у нас будут происходить вычисления :) То есть мы создадим виртуальную машину с Linux (я выбрал ubuntu 18.04), установим node.js приложения и postgreSQL.
Жмём создать ВМ, выкручиваем все настройки на минимум, так как при разработке нагрузки не будет (когда наше приложение выйдет в свет, тогда и подкрутим побольше, ну и будем мониторить по графикам).
SSH
Проблемный момент, с которым я столкнулся на этом этапе, это SSH:
Что это и зачем понятия не имел, поэтому пошел изучать. Оказалось — это просто метод доступа, но не по паролю, а по сгенерированному SSH ключу. Чтобы собственно его сгенерировать, скачиваем и устанавливаем Putty как нам советуют.
Запускаем C:Program FilesPuTTYputtygen.exe
Нажимаем кнопку Generate и водим мышкой, чтобы придать случайности сгенерированному ключа (как я понял). Далее копируем появившуюся строку начинающуюся с ssh-rsa куда-нибудь в текстовый файл и жмём Save private key, Save public key. Скопированный в текстовый файл ключ вставляем в поле SSH ключ открытой страницы Яндекса. Логин указываем root, иначе у вас не будет доступа при работе с графической файловой системой приложения по которому будете подключаться к облаку из дома/работы (возможно и есть способ, но я не разбирался). Как заметил andreymal, лучше не использовать root, чтобы китайские боты не подобрали пароль к вашему облаку, но так как в яндекс.облаке доступ только по SSH, то жить вроде можно.
Приложение на хостинге же стоит запускать исключительно не root пользователем, чтобы не позволить злоумышленникам выполнять вредный код через возможные уязвимости в вашем приложении.
Подключаемся к облаку с ПК и выбираем бесплатный SSH клиент
Стандартный Putty позволяет работать только командной строкой, а так как мне пользователю windows это непривычно, то я начал искать клиент с псевдо-проводником. Сначала я попробовал Mobaxterm, но он после какого-то времени простоя отключается, проводник вообще зависает, поэтому сейчас работаю с bitvise ssh и пока проблем как у Mobaxterm не наблюдаю.
Настройка bitvise ssh
Тут в поле Server>Host указываем наш внешний IP облака. Порт 22. Нажимаем client key manager>import указываем в нём сгенерированный Putty ранее private ключ. Может ещё потребоваться ключевая фраза, выберите что-нибудь что не забудете. Закрываем это окошко и в поле authentication указываем username: root, method publick key, client key — выбираем импортированный ранее. Жмём login и если мы всё сделали правильно, то подключимся к облаку:
Устанавливаем Node.js
Тут я рекомендую пользоваться инструкциями digitalocean.com, они очень подробны и многие есть на русском. Обычно я так и гуглю «digitalocean ubuntu 18.04 node.js» или что вы там захотите установить или настроить.
Далее идём в папку /opt/mysuperapp (my_super_app_name — эту папку вы должны создать). Каталог opt был выбран в качестве места расположения приложения после долгих гуглений «где уместно положить файлы node.js приложения в ubuntu».
Наконец-таки создаём файл server.js, который будет входной точкой приложения и вставляем туда код простого сервера на node.js:
Порт 80 — это для http запросов, 443 — для https. Пока у нас сервер на http.
Сохраняем всё и запускаем командной:
node server.js
В консоли должно появиться строка 'Server running at localhost:80/'
Теперь можно открыть браузер, ввести внешний IP (тот что в облаке яндекса у вашей ВМ ubuntu) и мы увидим «Hello World!»
Делаем всё удобно или цикл разработки с помощью git
Всё вроде работает, но мы же не будем работать всё время подключаясь к облаку. К тому же вдруг мы будем в будущем работать не одни.
Github
Github — это место, где будет лежать код нашего приложения. Если коротко, принцип работы для одного человека следующий:
На домашнем ПК разрабатываем наше приложение.
Сохраняем и в один клик выгружаем код на Github.
На хостинге или на другом ПК скачиваем наше приложение с github, перезагружаем сервер (если это хостинг) и новая версия нашего веб-приложения доступна во всемирной паутине.
Всё быстро, просто и удобно.
Собственно регистрируемся на Github и создаём private репозиторий для нашего приложения (он будет доступен только нам):
Возвращаемся в командную строку bitvise, останавливаем приложение нажав ctrl+c (если оно ещё работает).
Переходим в каталог /opt и удаляем созданную нами папку с приложением
Git- это то, с помощью чего мы будем загружать наше приложение на github, а оттуда на хостинг или другой ПК. Git — это отдельная тема для обсуждения, поэтому остановимся пока на этом.
Устанавливаем git на хостинге командами:
sudo apt update
sudo apt install git
Проверяем всё ли хорошо установилось:
git --version
Должна показаться версия git.
Заполняем данные git (так и не понял зачем, но видимо могут быть какие-то занудные предупреждения).
Для начала выберем редактор исходного кода, где будем работать. Я выбрал Visual studio code, так он прост, удобен, в нём много плагинов и можно настроить синхронизацию настроек если вы работаете с нескольких устройств. Собственно скачиваем, устанавливаем, запускаем, выбираем общую папку приложений, так как git clone создаст нам свою.
Плагины я использую следующие:
Устанавливаем git для ПК.
Открываем консоль в VScode с помощью ctrl+shift+` или terminal>new terminal
Отступление:
В консоли windows плохо с русскими символами и чтобы не было крякозяблов нужно открыть file>preferences>settings, ввести в поле terminal.integrated.shellArgs.windows, нажать
И добавить строку «terminal.integrated.shellArgs.windows»: ["-NoExit", "/c", «chcp 65001»],
Повторяем команду для загрузки файлов с github:
git clone https://github.com/ReTWi/mysuperapp.git
В VScode нажимаем File>Open Folder и открываем папку нашего приложения.
Создаём файл server.js с тем же кодом простого сервера:
Устанавливаем nodemon для автоматической перезагрузки сервера при изменениях в коде:
npm i nodemon -g
i — сокращение от install
g — глобальная установка (чтобы было доступно в консоли), а не только для нашего приложения.
Запускаем командой:
nodemon server.js
Открываем в браузере localhost:80/ или просто localhost:80 и видим Hello World.
Теперь настало время проверить нашу цепочку ПК>Github>Hosting.
Скачиваем Github desktop для большего удобства, подключаем свой github аккаунт, затем нажимаем файл add local repository и указываем каталог нашего приложения.
В приложении мы видим изменения которые мы сделали по сравнению с загруженной с Github версией (мы добавили server.js):
Жмём «commit to master»>«push origin», таким образом загружая файлы с ПК на Github.
Заходим в браузере на наш github аккаунт и видим загруженный файл server.js:
Потренируемся ещё немного, в VScode заменим строку «res.end('Hello World !n');» на «res.end('OmNomNom');». Увидим, что сервер сам перезагрузился:
Проверим в браузере и увидим там сделанные нами изменения «OmNomNom».
Desktop github тоже покажет нам что мы поменяли строку:
Опять жмём «commit to master»>«push origin», чтобы отправить файлы на github.
private — ваши приватные файлы, я там храню SSH доступы к облаку
server — ваш backend
ssl — ssl сертификаты для работы https на localhost и на хостинге
.babelrc — настройки сборки react приложения webpack'om (позволяет использовать более современный JS при разработке frontend)
.gitignore — файлы, которые не будут перемещаться на github (git их как бы не видит)
client.js — точка входа для генерации react сборки
package.json — используемые вами node_modeles и различные снипеты команд.
package-lock.json — изменения в модулях (насколько я понял, по файлу будет проверяться одинаковые ли у вас установлены модули на хостинге и на ПК).
pm2-watch.json — настройки запуска pm2 для хостинга
README.md — обложка для github
server.js — точка запуска нашего backend сервера Node.js
webpack.config.js — конфигурация сборки react
.gitignore
Тут мы указываем те файлы/папки, которые мы не хотим выгружать на github. Они будут только на данном устройстве и git не будет отслеживать/показывать их изменения. Открываем и вставляем:
/node_modules/
/logs/*
# exception to the rule
!logs/.gitkeep
/public/react_bundle.js
/public/isProd.js
Так как github не выгружает пустые папки, то можно внутрь что-нибудь положить, к примеру пустой файл .gitkeep. Сохраняем файл и закрываем.
package.json
Открываем и вставляем следующее (после // добавил комментарии)
{
"name": "myapp", // название вашего приложения
"version": "1.0.0",
"description": "OmNomNom",
"main": "server.js",
"scripts": {
"server": "pm2 start pm2-watch.json", // командой npm run server можно запустить этот скрипт
"client": "webpack -w --mode development", // командой npm client можно запустить этот скрипт. Собирает приложение реакт и отслеживает изменения в коде, автоматически обновляя сборку.
"client-prod": "webpack --mode production", // собирает сжатый вариант для production
"client-analyze": "webpack --mode production --analyze true" // собирает сжатый вариант для production и позволяет посмотреть размеры разных модулей приложения. Полезно для оптимизации
},
"repository": {
"type": "git",
"url": "git+https://github.com/myapp/knigam.git" // ссылка на ваш репозиторий github
},
"author": "rtw",
"license": "UNLICENSED", // запрет на любое использование (личное приложение)
"bugs": {
"url": https://github.com/myapp/knigam.git"
},
"homepage": "https://github.com/myapp/knigam.git#readme",
"dependencies": {
"@babel/core": "^7.2.2", // Современный js для frontend
"@babel/plugin-transform-runtime": "^7.2.0", // Современный js для frontend
"@babel/preset-env": "^7.3.1", // Современный js для frontend
"@babel/preset-react": "^7.0.0", // Современный js для frontend
"ajv": "^6.8.1", // Проверка типов переменных
"babel-loader": "^8.0.5", // Современный js для frontend
"babel-plugin-styled-components": "^1.10.0", // Работа со styled-components
"css-loader": "^2.1.0", // Для сборки webpack'om css
"fastify": "^2.0.0-rc.6", // аналог express, более живой и активно развивающийся
"fastify-cookie": "^2.1.6", // Работа с куки
"fastify-static": "^2.2.0", // Работа со статичными файлами
"moment": "^2.24.0", // Работа со временем
"pg": "^7.8.0", // Работа со временем
"pino": "^5.11.1", // Работа с postgreSQL из node.js
"pino-pretty": "^2.5.0", // Читаемые логи в консоли
"react": "^16.8.1", // Frontend библиотека. Выбор был между ней и Vue.js, но второй больше фрэймворк. В реакте больше нужно делать руками, что полезно для обучения
"react-dom": "^16.8.1", // React для работы с браузером
"style-loader": "^0.23.1", // Для сборки webpack'om стилей, уже не помню
"styled-components": "^4.1.3", // CSS in JS, очень удобно для динамических стилей и локализации стилей в компонентах
"webpack": "^4.29.3", // Сборщик реакт приложения
"webpack-bundle-analyzer": "^3.0.3", // Анализатор размеров модулей собранного реакт приложения
"webpack-cli": "^3.2.3" // Консоль сборщик реакт приложения, не помню уже зачем
}
}
Остановлюсь на двух основных фрэймворках/библиотеках выбранных для приложения:
Fastify был выбран в качестве альтернативы express.js, так как в первом уже есть экспериментальная поддержка htpp2, он активно развивается и мне кажется у него больше будущего, нежели у express.js, который стал очень неповоротлив и кое-как развивается. С другой стороны express.js уже долгое время в работе и по нему вам легче будет найти информацию.
React был выбран так как мне с ним было проще работать, понять и пробовать всё своими руками. Vue — показался уже чем-то со своими правилами, направлением. Хотя во Vue может и меньше придётся писать что-то своими руками, но так как приоритет сделан на обучении и для человека ранее не программировавшего react пошел как-то легче.
Сохраняем файл package.json и устанавливаем все модули указанные в dependencies командой:
npm i
У нас появится папка node_modules, в которой будут все модули для нашего приложения.
client — пока пустая папка
logs — внутри лежит файл .gitkeep, чтобы папка перекочевала на хостинг и логи успешно туда падали. При разработке мы всё будем выводить в консоль.
public
Тут статические файлы нашего сайта будут лежать, изображения там, фавиконки и т.д.
Отдельно остановимся на двух файлах:
index.html:
<!DOCTYPE html>
<html>
<head>
<base href="/" />
<meta charset="UTF-8" />
<title>MyApp</title>
</head>
<body>
<div id="cookies">Этот текст заменится содержимым react_bundle после его загрузки</div>
<noscript
>Система: онлайн подключение возможно только при наличии Javscript</noscript
>
<script src="react_bundle.js"></script>
</body>
</html>
— тут у нас подгружается react-фронтэнд и рендерится в тег по его id.
isProd.js содержит единственную строку «module.exports = false»
Так как он находится в исключениях .gitignore, то не переносится. Соответственно на ПК мы устанавливаем его в false, а на хостинге в true. Затем используем этот файл, чтобы понять в какой мы сейчас среде (разработка/продакшн). Мне показалось наиболее удобным, к тому же можно частично в коде поменять при разработке и проверить работу модулей в продакшне.
ssl — там лежат сохранённые ранее сертификаты в папках localhost и myapp
Если коротко, то он открывает файл client.js и все что у него внутри, собирая react_bundle и помещая его в папку public, откуда потом через открытый index.html он будет загружен.
server.js
const isProd = require('./public/isProd'),
fs = require('fs'),
log = require('./server/logger'),
path = require('path')
// Ошибки среды node.js, чтобы приложение никогда не падало
process.on('unhandledRejection', (reason, promise) => {
log.error({ reason, promise }, 'серверный процесс unhandledRejection')
})
process.on('uncaughtException', err => {
log.error({ err }, 'серверный процесс uncaughtException')
})
// Redirect server from http port 80 to https 443
const fastifyHttp = require('fastify')({
logger: log,
ignoreTrailingSlash: true
})
fastifyHttp.listen(80, '::', (err, address) => {
if (err) {
log.error({ err, address }, 'Ошибка при запуске HTTP сервера')
} else {
log.warn('Http cервер запущен')
}
})
// Let's Encrypt challenge
fastifyHttp.get('/.well-known/acme-challenge/:file', (req, res) => {
let stream = fs.createReadStream(
path.join(__dirname + '/ssl/.well-known/acme-challenge/' + req.params.file)
)
res.type('text/html').send(stream)
})
fastifyHttp.get('/*', (req, res) => {
res.redirect(301, 'https://' + req.headers.host + req.raw.url)
})
fastifyHttp.get('/', (req, res) => {
res.redirect(301, 'https://' + req.headers.host + req.raw.url)
})
// Сервер
let fastifyOptions = {
logger: log,
ignoreTrailingSlash: true,
http2: true
}
fastifyOptions.https = isProd
? {
allowHTTP1: true,
key: fs.readFileSync('./ssl/myapp/key.txt'),
cert: fs.readFileSync('./ssl/myapp/crt.txt')
}
: {
allowHTTP1: true,
key: fs.readFileSync('./ssl/localhost/cert.key'),
cert: fs.readFileSync('./ssl/localhost/cert.pem')
}
const fastify = require('fastify')(fastifyOptions)
fastify.listen(443, '::', (err, address) => {
if (err) {
log.error({ err, address }, 'Ошибка при запуске сервера')
} else {
log.warn(
`Сервер запущен в ${
isProd ? 'продакшен' : 'режиме разработки'
}`
)
}
})
// Валидатор
fastify.setSchemaCompiler(schema => {
return ajv.compile(schema)
})
// Ошибки fastify
fastify.setErrorHandler((err, req, res) => {
log.error({ err, req }, 'fastify errorHandler')
// Ошибка валидации данных запроса
if (err.validation) {
return res.send({
error: 'Ошибка валидации данных запроса'
})
} else {
return res.send({
error:
'Ошибка errorHandler'
})
}
})
// Статические файлы
fastify.register(require('fastify-static'), {
root: path.join(__dirname, './public')
})
// Куки
fastify.register(require('fastify-cookie'), err => {
if (err) log.error({ err }, 'fastify-cookie')
})
// Ответ на любой запрос исключая апи / несуществующая страница
// Тут мы на любой запрос отдаём index.html, по сути наш фронэнд
// Запросы фронтэнда у нас принимаются с префиксом api, то есть GET /api/userdata
fastify.setNotFoundHandler((req, res) => {
res.sendFile('index.html')
})
// Routes
fastify.register(
async openRoutes => {
// Пути доступные всем
openRoutes.register(require('./server/api/open'))
openRoutes.register(async withSession => {
// Пути доступные только после авторизации и проверки сессии
// Проверяем прямо тут хуком, пример:
///withSession.addHook('preHandler', async (req, res) => {
// if (!(await sessionManagerIsOk(req, res))) return
// })
withSession.register(require('./server/api/with_session'))
})
},
{ prefix: '/api' } // префикс всех путей
)
Папка server
Тут лежит на бэкэнд и все пути.
logger.js — в зависимости от среды isProd логирует или в консоль или в errors.log
После настройки и проверки всего на Localhost, просто выгружаем всё на github, а оттуда git pull на хостинг. Всё что на хостинге нужно будет сделать, это установить модули node.js командой «npm i» и создать файл isProd.js
Автоматически обновляемый SSL
Когда вы купите себе домен и привяжете его к IP облака, пример инструкции для REG.RU, можно установить на сервере автоматически обновляемый бесплатный SSL для работы сайта по https.
Наш сервер работает без nginx. Возможно он понадобится нам в будущем как балансировщик нагрузки или более быстрый HTTP-сервер для раздачи статических файлов, но пока необходимости я в нём не вижу. Балансировка нагрузки пока нам не требуется, а касательно скорости раздачи статики сравнений я не нашел.
Перед установкой в папке ssl создадим папку .well-known, а в ней acme-challenge. Получится /opt/myapp/ssl/.well-known/acme-challenge
Для установке на сервере с node.js без nginx автоматически обновляемого SSL переходим по ссылке. По очереди выполняем команды в консоли хостинга:
Выбираем второй способ проверки, который разместит в папке /opt/myapp/ssl/.well-known/acme-challenge определённый файл, а после подтверждения владельца сервера его удалит.
Указываем по запросу наш домен, к примеру: «example.com» и путь к ssl папке нашего приложения (сервер настроен так, что отдаст созданный ботом файл) "/opt/myapp/ssl".
Бот сам настроит cron-задачу для обновления сертификата до срока его истечения в течение 90-дней.
Не думал, что займёт столько времени всё написать, к 4 часам ночи уже мог что-то упустить :/
Интересно мнение хабравчан и специалистов, кто осилил это полотно или прочитал какие-то отдельные моменты. Как у вас устроен цикл разработки? Есть ли какие-то моменты в которых я ошибаюсь или поступаю не лучшим образом?
Спасибо за статью! Сам пользуюсь хостингом https://hostsailor.com/ru/ и очень доволен. Плата за виртуальный хостинг нормальная, никаких накруток цены нет. Также хостинг предоставляет услуги аренды выделенного сервера VDS/VPS Уже сотрудничаю с ними семь месяца, неполадка было всего лишь один раз, но ее решили буквально за два часа, поддержка радует. В целом, рекомендую.
Спасибо за статью! Сам пользуюсь хостингом https://hostsailor.com/ru/ и очень доволен. Плата за виртуальный хостинг нормальная, никаких накруток цены нет. Также хостинг предоставляет услуги аренды выделенного сервера VDS/VPS Уже сотрудничаю с ними семь месяца, неполадка было всего лишь один раз, но ее решили буквально за два часа, поддержка радует. В целом, рекомендую.