Предисловие


Начну с того, что однажды мне захотелось создать приложение. Желание такое возникло из-за того, что я люблю читать, а нормальных книжных агрегаторов на просторах русского интернета просто нет. Собственно из боли поиска чего бы почитать и попыток вспомнить как называлась та книжка, которую я недавно читал и на какой же главе я остановился, родилось желание сделать веб-приложение, в котором всё это было бы возможно и удобно. Стоит отметить, что никакого опыта разработки, программирования и т.п. у меня не было, моя работа вообще с этим не связана. Тем не менее желание перебороло лень и переросло в конкретные действия, своеобразное хобби.

Не буду рассказывать как я изучал 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) Создаём каталог под свой сайт:

image

2) Создаём Virtual Private Cloud:

Главное что он даёт для меня на текущем этапе — это IP для доступа к созданному ресурсу из вне. С подсетями, зонами, изоляцией и отказоустойчивостью ознакомился поверхностно, при необходимости наверстаю.

3) Создаём подсеть и присваиваем ей внутренний IP (как я понял, это примерно как локальная сеть)

image

4) Переходим на вкладку IP и резервируем себе статический IP.

По нему мы будем подключаться из дома и других мест. Вероятно можно и с динамическим работать, но я не разбирался в каких случаях он меняется.

image

Compute Cloud


Тут у нас будут происходить вычисления :) То есть мы создадим виртуальную машину с Linux (я выбрал ubuntu 18.04), установим node.js приложения и postgreSQL.

image

Жмём создать ВМ, выкручиваем все настройки на минимум, так как при разработке нагрузки не будет (когда наше приложение выйдет в свет, тогда и подкрутим побольше, ну и будем мониторить по графикам).

SSH


Проблемный момент, с которым я столкнулся на этом этапе, это SSH:

image

Что это и зачем понятия не имел, поэтому пошел изучать. Оказалось — это просто метод доступа, но не по паролю, а по сгенерированному SSH ключу. Чтобы собственно его сгенерировать, скачиваем и устанавливаем Putty как нам советуют.

Запускаем C:\Program Files\PuTTY\puttygen.exe

image

Нажимаем кнопку Generate и водим мышкой, чтобы придать случайности сгенерированному ключа (как я понял). Далее копируем появившуюся строку начинающуюся с ssh-rsa куда-нибудь в текстовый файл и жмём Save private key, Save public key. Скопированный в текстовый файл ключ вставляем в поле SSH ключ открытой страницы Яндекса. Логин указываем root, иначе у вас не будет доступа при работе с графической файловой системой приложения по которому будете подключаться к облаку из дома/работы (возможно и есть способ, но я не разбирался).
Как заметил andreymal, лучше не использовать root, чтобы китайские боты не подобрали пароль к вашему облаку, но так как в яндекс.облаке доступ только по SSH, то жить вроде можно.

Приложение на хостинге же стоит запускать исключительно не root пользователем, чтобы не позволить злоумышленникам выполнять вредный код через возможные уязвимости в вашем приложении.


Подключаемся к облаку с ПК и выбираем бесплатный SSH клиент


Стандартный Putty позволяет работать только командной строкой, а так как мне пользователю windows это непривычно, то я начал искать клиент с псевдо-проводником. Сначала я попробовал Mobaxterm, но он после какого-то времени простоя отключается, проводник вообще зависает, поэтому сейчас работаю с bitvise ssh и пока проблем как у Mobaxterm не наблюдаю.

Настройка bitvise ssh


image

Тут в поле Server>Host указываем наш внешний IP облака. Порт 22. Нажимаем client key manager>import указываем в нём сгенерированный Putty ранее private ключ. Может ещё потребоваться ключевая фраза, выберите что-нибудь что не забудете. Закрываем это окошко и в поле authentication указываем username: root, method publick key, client key — выбираем импортированный ранее. Жмём login и если мы всё сделали правильно, то подключимся к облаку:

image

Устанавливаем Node.js


Тут я рекомендую пользоваться инструкциями digitalocean.com, они очень подробны и многие есть на русском. Обычно я так и гуглю «digitalocean ubuntu 18.04 node.js» или что вы там захотите установить или настроить.

Как установить Node.js можно почитать тут.

Если коротко, то заходим на nodesource (тут последние версии node.js можно установить), листаем сюда:

image

Копируем и по-очереди запускаем команды:

curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash -
sudo apt-get install -y nodejs

Проверяем как установилось командой

nodejs -v

Нам покажет версию node.js

npm -v

Нам покажет версию менеджера пакетов для node.js.

Далее идём в папку /opt/mysuperapp (my_super_app_name — эту папку вы должны создать). Каталог opt был выбран в качестве места расположения приложения после долгих гуглений «где уместно положить файлы node.js приложения в ubuntu».

Наконец-таки создаём файл server.js, который будет входной точкой приложения и вставляем туда код простого сервера на node.js:

const http = require('http');

const hostname = 'localhost';
const port = 80;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Порт 80 — это для http запросов, 443 — для https. Пока у нас сервер на http.

Сохраняем всё и запускаем командной:

node server.js

В консоли должно появиться строка 'Server running at localhost:80/'

Теперь можно открыть браузер, ввести внешний IP (тот что в облаке яндекса у вашей ВМ ubuntu) и мы увидим «Hello World!»

Делаем всё удобно или цикл разработки с помощью git


Всё вроде работает, но мы же не будем работать всё время подключаясь к облаку. К тому же вдруг мы будем в будущем работать не одни.

Github


Github — это место, где будет лежать код нашего приложения. Если коротко, принцип работы для одного человека следующий:

  • На домашнем ПК разрабатываем наше приложение.
  • Сохраняем и в один клик выгружаем код на Github.
  • На хостинге или на другом ПК скачиваем наше приложение с github, перезагружаем сервер (если это хостинг) и новая версия нашего веб-приложения доступна во всемирной паутине.

Всё быстро, просто и удобно.

Собственно регистрируемся на Github и создаём private репозиторий для нашего приложения (он будет доступен только нам):

image

Копируем строку github.com/ReTWi/mysuperapp.git для скачивания приложения.

image

  1. Возвращаемся в командную строку bitvise, останавливаем приложение нажав ctrl+c (если оно ещё работает).
  2. Переходим в каталог /opt и удаляем созданную нами папку с приложением

Git- это то, с помощью чего мы будем загружать наше приложение на github, а оттуда на хостинг или другой ПК. Git — это отдельная тема для обсуждения, поэтому остановимся пока на этом.
Устанавливаем git на хостинге командами:

sudo apt update
sudo apt install git

Проверяем всё ли хорошо установилось:

git --version

Должна показаться версия git.

Заполняем данные git (так и не понял зачем, но видимо могут быть какие-то занудные предупреждения).

git config --global user.name "Your Name"
git config --global user.email "youremail@domain.com"

Наконец-таки загружаем наше приложение на хостинг командой:
(тут должна быть ссылка на ваше приложение )

git clone https://github.com/ReTWi/mysuperapp.git 

В каталоге /opt появится новый mysuperapp, где будут находится файлы нашего приложения загруженные из github.

Теперь пора повторить тоже самое для ПК и замкнуть цепочку ПК(разные) --> Github --> Хостинг

Устанавливаем node.js на ПК.

Visual studio code


Для начала выберем редактор исходного кода, где будем работать. Я выбрал Visual studio code, так он прост, удобен, в нём много плагинов и можно настроить синхронизацию настроек если вы работаете с нескольких устройств. Собственно скачиваем, устанавливаем, запускаем, выбираем общую папку приложений, так как git clone создаст нам свою.

Плагины я использую следующие:

image

Устанавливаем git для ПК.
Открываем консоль в VScode с помощью ctrl+shift+` или terminal>new terminal

Отступление:

В консоли windows плохо с русскими символами и чтобы не было крякозяблов нужно открыть file>preferences>settings, ввести в поле terminal.integrated.shellArgs.windows, нажать

image

И добавить строку «terminal.integrated.shellArgs.windows»: ["-NoExit", "/c", «chcp 65001»],

image


Повторяем команду для загрузки файлов с github:

git clone https://github.com/ReTWi/mysuperapp.git 

В VScode нажимаем File>Open Folder и открываем папку нашего приложения.

Создаём файл server.js с тем же кодом простого сервера:

const http = require('http');

const hostname = 'localhost';
const port = 80;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Устанавливаем 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):

image

Жмём «commit to master»>«push origin», таким образом загружая файлы с ПК на Github.

image

Заходим в браузере на наш github аккаунт и видим загруженный файл server.js:

image

Потренируемся ещё немного, в VScode заменим строку «res.end('Hello World !\n');» на «res.end('OmNomNom');». Увидим, что сервер сам перезагрузился:

image

Проверим в браузере и увидим там сделанные нами изменения «OmNomNom».

Desktop github тоже покажет нам что мы поменяли строку:

image

Опять жмём «commit to master»>«push origin», чтобы отправить файлы на github.

Переключаемся на командную строку хостинга.

Останавливаем наше приложение, если оно ещё запущено (ctrl+c).

Скачиваем наше обновлённое приложение командами:

git config credential.helper store
git pull

Первая сохранит наши данные, чтобы постоянно не вводить логин и пароль. В дальнейшем нам достаточно будет git pull.

Установим pm2 — нечто похожее на nodemon, только для хостинга:

npm i pm2 -g

Запустим приложение с помощью pm2, которые при следующем git pull на хостинге сам перезагрузит наш сервер:

pm2 start server.js --watch

Откроем браузер по нашему внешнему IP облака и увидим наше «OmNomNom».

Таким образом мы замкнули цепочку работы с приложением и быстрым его развёртыванием на хостинге.

Создаём временные SSL сертификаты для HTTPS на localhost и хостинга


Заходим на сайт zerossl.com

image

В поле domains, ip… вписываем сначала localhost, нажимаем generate и скачиваем 2 файла по кнопке:

image

Сохраняем их у нас в проекте в папке ssl/localhost.

Повторяем процедуру для внешнего IP Облака и сохраняем в ssl/myapp.

Запускаем более сложный https сервер node.js


Структура приложения:

image

  • client — тут будет лежать наш front-end. У меня react.
  • logs — сюда будут падать логи на хостинге
  • node_modules — модули node.js
  • 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

.babelrc


{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "browsers": [">0.25%", "not ie 11", "not op_mini all"]
        }
      }
    ],
    "@babel/preset-react"
  ],
  "plugins": [
    "babel-plugin-styled-components",
    "@babel/plugin-transform-runtime"
  ]
}

Настройки для создания нашего react_bundle с поддержкой браузеров используемых более >0.25% пользователей.

client.js


import React from 'react'
import { render } from 'react-dom'
render(<div>Реакт!!</div>, document.getElementById('cookies'))


Рендерит наш фронтэнд в div с тегом cookies.

pm2-watch.json — позволяет на хостинге командой «npm run server» запустить сервер с отслеживанием изменений в коде и автоматической перезагрузкой.

webpack.config.js


Сборщик реакт приложения:

const webpack = require('webpack'),
  path = require('path'),
  BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = (env, argv) => {
  let prod = argv.mode == 'production'

  let config = {
    entry: './client.js',
    output: {
      path: path.resolve('./public'),
      filename: 'react_bundle.js'
    },
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          loader: 'babel-loader'
        },
        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader']
        }
      ]
    },
    resolve: {
      alias: {
        client: path.resolve('./client/shared'),
        public: path.resolve('./public')
      }
    },
    plugins: [
      argv.analyze ? new BundleAnalyzerPlugin() : false,
      prod ? new webpack.optimize.AggressiveMergingPlugin() : false,
      new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /ru/)
    ].filter(Boolean),
    optimization: {
      minimize: prod ? true : false
    },
    performance: {
      hints: false
    }
  }

  return config
}

Если коротко, то он открывает файл 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

'use strict'

const pino = require('pino'),
  isProd = require('../public/isProd')

let logOptions = isProd
  ? {
      level: 'warn', // уровень логирования
      timestamp: () => {
        return ',"time":"' + new Date() + '"'
      }
    }
  : {
      level: 'warn',
      prettifier: require('pino-pretty'),
      prettyPrint: {
        levelFirst: true,
        translateTime: true
      }
    }
let dest = isProd ? pino.destination('./logs/errors.log') : pino.destination(1)
let log = pino(logOptions, dest)

module.exports = log

server/api/
open.js — сюда добавляем наши пути.

'use strict'
module.exports = function(fastify, options, next) {

  fastify.route({
    method: 'GET',
    url: '/',
    handler: async (req, res) => {
      res.send('api / route')
    }
  })

  fastify.route({
    method: 'GET',
    url: '/hi',
    handler: async (req, res) => {
      res.send('api / route hi')
    }
  })
  next()
}

После настройки и проверки всего на 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 переходим по ссылке. По очереди выполняем команды в консоли хостинга:

sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository universe
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot 
sudo certbot certonly

Выбираем второй способ проверки, который разместит в папке /opt/myapp/ssl/.well-known/acme-challenge определённый файл, а после подтверждения владельца сервера его удалит.

Указываем по запросу наш домен, к примеру: «example.com» и путь к ssl папке нашего приложения (сервер настроен так, что отдаст созданный ботом файл) "/opt/myapp/ssl".

Бот сам настроит cron-задачу для обновления сертификата до срока его истечения в течение 90-дней.

Не думал, что займёт столько времени всё написать, к 4 часам ночи уже мог что-то упустить :/

Интересно мнение хабравчан и специалистов, кто осилил это полотно или прочитал какие-то отдельные моменты. Как у вас устроен цикл разработки? Есть ли какие-то моменты в которых я ошибаюсь или поступаю не лучшим образом?

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


  1. FoterIS
    12.02.2019 11:40

    Подскажите пожалуйст какие данные пользоватей вы храните, по причине которых вы выбрали хостинг в РФ?


    1. ReTWi Автор
      12.02.2019 14:05

      По закону, насколько я знаю, вы обязаны хранить все персональные данные пользователей на серверах в РФ. К ним относятся псевдонимы, почта и т.д. В общем любая информация с помощью которой можно идентифицировать конкретного человека.


  1. andreymal
    12.02.2019 11:43

    Логин указываем root

    Ни в коем случае, возможность логиниться в root нужно вообще полностью отключать в настройках SSH-сервера в целях безопасности. Только su/sudo


    установить модули node.js командой «npm i»

    Правильно я понимаю, что вебпак-бандлы собираются каждый раз прямо на сервере? По-моему это лишняя нагрузка


    Выбираем второй способ проверки

    Можно было бы сразу написать команду со всеми опциями, чтобы никаких вопросов пользователю не задавалось


    Бот сам настроит cron-задачу

    В современных убунтах вместо cron используется systemd-таймер




    Запускать потенциально баганутое nodejs-приложение от имени root — чудовищно плохая идея, потому что малейший баг в коде может привести к появлению у злоумышленника полного доступа к серверу


    А ещё я что-то не нашёл ничего про автозапуск после перезагрузки сервера


    1. ReTWi Автор
      12.02.2019 14:25

      Ни в коем случае, возможность логиниться в root нужно вообще полностью отключать в настройках SSH-сервера в целях безопасности. Только su/sudo

      Подскажите, а в чём разница если я залогинюсь не рутом и буду просто использовать sudo для тех же целей?

      Правильно я понимаю, что вебпак-бандлы собираются каждый раз прямо на сервере? По-моему это лишняя нагрузка

      Просто каждый раз при перекомпиляции (добавили вы новый div в код) git будет выкатывать целое полотно изменений в бандле, которые нам не нужны. Изменения самого же компонента реакт будут также видны. Тут бы сделать так, чтобы гит не отслеживал изменения файла, но переносил его всё равно каждый раз, но вот не знаю, возможно ли это и стоит ли усилий, если можно просто скопилировать на хостинге, всё же это не часто будет происходить.
      Выбираем второй способ проверки

      Пока не постиг подобные сокращения, но буду иметь ввиду, спасибо :)
      В современных убунтах вместо cron используется systemd-таймер

      У certbota написано что будет крон-задача:
      The Certbot packages on your system come with a cron job that will renew your certificates automatically before they expire.

      Запускать потенциально баганутое nodejs-приложение от имени root — чудовищно плохая идея, потому что малейший баг в коде может привести к появлению у злоумышленника полного доступа к серверу

      Можете привести пример? Очень интересно.

      А ещё я что-то не нашёл ничего про автозапуск после перезагрузки сервера

      После перезагрузки ubuntu запускается pm2, который запускает приложение. Если же само приложение крашится, то pm2 его также перезапустит. Ну и в коде критические ошибки стоит отлавливать чтобы ничего не падало, как минимум вот этим:
      process.on('unhandledRejection', (reason, promise) => {
      log.error({ reason, promise }, 'серверный процесс unhandledRejection')
      })
      process.on('uncaughtException', err => {
      log.error({ err }, 'серверный процесс uncaughtException')
      })


      1. andreymal
        12.02.2019 14:33

        Подскажите, а в чём разница если я залогинюсь не рутом и буду просто использовать sudo для тех же целей?

        Во-первых, sudo знает и пишет в логи, какой конкретно пользователь его пользует, что будет важно в многопользовательском окружении, в то время как root это просто root и неизвестно кто. Во-вторых, пользователь root известен всем, и китайские боты будут пытаться подбирать к нему пароль (при его наличии), в то время как ваш не-root логим им не будет известен и подобрать пароль к нему они не смогут (впрочем, при аутентификации только по ssh-ключу эта проблема не столь остра). В-третьих, это хотя бы просто ещё один слой «защиты от дурака»


        git будет выкатывать целое полотно изменений в бандле

        Вы потихоньку начинаете понимать, почему использовать git для развёртывания — не самая хорошая идея ;) Про это уже где-то были статьи на Хабре вроде


        У certbota написано что будет крон-задача:

        Или документация устарела, или неполная, или просто врут. Достаточно посмотреть те файлы конфигурации, которые certbot создаёт после своей работы, чтобы понять, что это не соответствует действительности. Впрочем, это так, мелкая придирка


        Можете привести пример?

        Гуглите абсолютно любую RCE-уязвимость, они постоянно пачками находятся и эксплуатируются


        После перезагрузки ubuntu запускается pm2

        Кем запускается? Этого я и не обнаружил в посте


        1. ReTWi Автор
          12.02.2019 14:49

          Спасибо, а что по вашему лучше использовать для развёртывания тогда?

          Возможно упустил, командой pm2 startup systemd настраивается его автозагрузка. Вот тут подробнее.


          1. andreymal
            12.02.2019 15:01
            +1

            а что по вашему лучше использовать для развёртывания тогда?

            Ключевые слова для гуглинга — Continuous Delivery / Continuous Deployment, в контексте GitHub — ещё Travis CI. (Сам я заливаю всё своё на серверы Ansible-плейбуком прямо с домашнего компа вообще без git, так с меня тут так себе советчик)