├── .gitignore
├── public
├── favicon.ico
├── img
│ ├── avatar.jpg
│ ├── deploy.png
│ ├── index.jpg
│ ├── server.jpg
│ └── icons
│ │ ├── icon_tg.png
│ │ ├── icon_vk.png
│ │ ├── icon_github.png
│ │ └── icon_discord.png
└── css
│ ├── config-page.css
│ └── index-page.css
├── routes
├── index.js
└── generateConfigNgnix.js
├── package.json
├── index.js
├── views
├── generateConfig.ejs
└── index.ejs
├── utils
└── configNgnixGenerate.js
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | .env
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AtomBaytovich/example_deploy_proj/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/img/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AtomBaytovich/example_deploy_proj/HEAD/public/img/avatar.jpg
--------------------------------------------------------------------------------
/public/img/deploy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AtomBaytovich/example_deploy_proj/HEAD/public/img/deploy.png
--------------------------------------------------------------------------------
/public/img/index.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AtomBaytovich/example_deploy_proj/HEAD/public/img/index.jpg
--------------------------------------------------------------------------------
/public/img/server.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AtomBaytovich/example_deploy_proj/HEAD/public/img/server.jpg
--------------------------------------------------------------------------------
/public/img/icons/icon_tg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AtomBaytovich/example_deploy_proj/HEAD/public/img/icons/icon_tg.png
--------------------------------------------------------------------------------
/public/img/icons/icon_vk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AtomBaytovich/example_deploy_proj/HEAD/public/img/icons/icon_vk.png
--------------------------------------------------------------------------------
/public/img/icons/icon_github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AtomBaytovich/example_deploy_proj/HEAD/public/img/icons/icon_github.png
--------------------------------------------------------------------------------
/public/img/icons/icon_discord.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AtomBaytovich/example_deploy_proj/HEAD/public/img/icons/icon_discord.png
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 |
4 | router.get('/', (req, res) => {
5 | return res.render('index', {
6 | title: 'Мини сайт Atom Baytovich | CODE'
7 | });
8 | });
9 |
10 |
11 | module.exports = router;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-deploy-proj",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "nodemon index.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/AtomBaytovich/example_deploy_proj.git"
12 | },
13 | "author": "Atom Baytovich",
14 | "license": "ISC",
15 | "bugs": {
16 | "url": "https://github.com/AtomBaytovich/example_deploy_proj/issues"
17 | },
18 | "homepage": "https://github.com/AtomBaytovich/example_deploy_proj#readme",
19 | "dependencies": {
20 | "body-parser": "^1.20.0",
21 | "content-disposition": "^0.5.4",
22 | "cors": "^2.8.5",
23 | "ejs": "^3.1.8",
24 | "express": "^4.18.1",
25 | "punycode": "^2.1.1"
26 | },
27 | "devDependencies": {
28 | "nodemon": "^2.0.19"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const cors = require('cors');
4 | const bodyParser = require('body-parser');
5 |
6 |
7 | const PORT = process.env.PORT || 3000;
8 |
9 | const app = express();
10 |
11 | // cors
12 | app.use(cors());
13 | app.options('*', cors());
14 |
15 | // parse application/x-www-form-urlencoded
16 | app.use(bodyParser.urlencoded({ extended: false }))
17 |
18 | // parse application/json
19 | app.use(bodyParser.json())
20 |
21 | // шаблонизатор ejs
22 | app.set('views', './views');
23 | app.set('view engine', 'ejs');
24 |
25 | // статическая папка
26 | app.use(express.static(path.join(__dirname, 'public')));
27 |
28 | // подключаем наши роуты
29 | app.use('/', require('./routes/index'));
30 | app.use('/config-ngnix', require('./routes/generateConfigNgnix'));
31 | // Not Found
32 | app.use("*", (req, res) => res.send('404 | Not Found'));
33 |
34 | // прослушка
35 | app.listen(PORT, () => {
36 | console.log(`Проект запущен на http://localhost:${PORT}`);
37 | });
--------------------------------------------------------------------------------
/routes/generateConfigNgnix.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const punycode = require('punycode');
4 | const { generateConfig } = require("../utils/configNgnixGenerate");
5 | const stream = require('stream');
6 | const contentDisposition = require('content-disposition')
7 |
8 | router.get('/', (req, res) => {
9 | return res.render('generateConfig', {
10 | title: 'Генерация конфига Ngnix'
11 | });
12 | });
13 |
14 | router.post('/', (req, res) => {
15 | const { domain } = req.body;
16 | const fileName = `${domain}.txt`;
17 | let domainEncode = punycode.toASCII(domain);
18 |
19 | const configText = generateConfig({
20 | domain: domainEncode
21 | })
22 |
23 | const fileContents = Buffer.from(configText, "utf-8");
24 | const readStream = new stream.PassThrough();
25 |
26 | readStream.end(fileContents);
27 | res.setHeader('Content-Disposition', contentDisposition(fileName))
28 | res.set('Content-Type', 'text/plain');
29 | readStream.pipe(res);
30 | })
31 |
32 |
33 | module.exports = router;
--------------------------------------------------------------------------------
/views/generateConfig.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | <%= title %>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
31 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/utils/configNgnixGenerate.js:
--------------------------------------------------------------------------------
1 | /// xn--l1abdi.com на свой домен
2 | /// конфигурация с редиректом на https и с www
3 | /// TG канал: @atom_baytovich
4 |
5 | const generateConfig = ({ domain = 'xn--l1abdi.com' }) => {
6 | return `
7 | server {
8 | listen 443 ssl http2;
9 | listen [::]:443 ssl http2;
10 | server_name ${domain};
11 | # SSL
12 | ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
13 | ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
14 | ssl_trusted_certificate /etc/letsencrypt/live/${domain}/chain.pem;
15 | include /etc/letsencrypt/options-ssl-nginx.conf;
16 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
17 |
18 | # reverse proxy
19 | location / {
20 | proxy_pass http://localhost:3000;
21 | proxy_http_version 1.1;
22 | proxy_set_header Upgrade $http_upgrade;
23 | proxy_set_header Connection 'upgrade';
24 | proxy_set_header Host $host;
25 | proxy_cache_bypass $http_upgrade;
26 | }
27 |
28 | }
29 |
30 | # subdomains redirect
31 | server {
32 | listen 443 ssl http2;
33 | listen [::]:443 ssl http2;
34 | server_name *.${domain};
35 | # SSL
36 | ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
37 | ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
38 | ssl_trusted_certificate /etc/letsencrypt/live/${domain}/chain.pem;
39 | return 301 https://${domain}$request_uri;
40 | }
41 |
42 | # HTTP redirect
43 | server {
44 | listen 80;
45 | listen [::]:80;
46 | server_name .${domain};
47 |
48 | location / {
49 | return 301 https://${domain}$request_uri;
50 | }
51 | }
52 | `
53 | }
54 |
55 | module.exports = {
56 | generateConfig
57 | }
--------------------------------------------------------------------------------
/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | <%= title %>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |

19 |
20 |
21 | Atom Baytovich
22 |
23 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/public/css/config-page.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,500;1,300&display=swap');
2 |
3 | html, body, div, span, applet, object, iframe,
4 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
5 | a, abbr, acronym, address, big, cite, code,
6 | del, dfn, em, img, ins, kbd, q, s, samp,
7 | small, strike, strong, sub, sup, tt, var,
8 | b, u, i, center,
9 | dl, dt, dd, ol, ul, li,
10 | fieldset, form, label, legend,
11 | table, caption, tbody, tfoot, thead, tr, th, td,
12 | article, aside, canvas, details, embed,
13 | figure, figcaption, footer, header, hgroup,
14 | menu, nav, output, ruby, section, summary,
15 | time, mark, audio, video {
16 | margin: 0;
17 | padding: 0;
18 | border: 0;
19 | font-size: 100%;
20 | font: inherit;
21 | vertical-align: baseline;
22 | }
23 |
24 | article, aside, details, figcaption, figure,
25 | footer, header, hgroup, menu, nav, section {
26 | display: block;
27 | }
28 |
29 | html {
30 | height: 100%;
31 | }
32 |
33 | body {
34 | line-height: 1;
35 | font-family: 'Montserrat', sans-serif;
36 | background: url(../img/server.jpg) no-repeat 50% fixed;
37 | background-size: cover !important;
38 | overflow: hidden;
39 | }
40 |
41 | ol, ul {
42 | list-style: none;
43 | }
44 |
45 | blockquote, q {
46 | quotes: none;
47 | }
48 |
49 | blockquote:before, blockquote:after,
50 | q:before, q:after {
51 | content: '';
52 | content: none;
53 | }
54 |
55 | table {
56 | border-collapse: collapse;
57 | border-spacing: 0;
58 | }
59 |
60 | input {
61 | all: unset;
62 | }
63 |
64 | button {
65 | padding: 0;
66 | border: none;
67 | font: inherit;
68 | color: inherit;
69 | background-color: transparent;
70 | /* отображаем курсор в виде руки при наведении; некоторые
71 | считают, что необходимо оставлять стрелочный вид для кнопок */
72 | cursor: pointer;
73 | }
74 |
75 | /* закончились обнуляющие стили */
76 |
77 | a {
78 | color: #fff;
79 | /* text-decoration: ; */
80 | }
81 |
82 | .wrapper {
83 | min-height: 100vh;
84 | display: flex;
85 | align-items: center;
86 | color: #fff !important;
87 | }
88 |
89 | .main {
90 | max-width: 360px;
91 | max-height: 500px;
92 | padding: 5px;
93 | display: flex;
94 | align-items: center;
95 | flex-direction: column;
96 | background-color: #1e1e1e66;
97 | backdrop-filter: blur(30px);
98 | box-shadow: #04040496 1px 1px 3px 2px;
99 | margin: 0 auto;
100 | border-radius: 10px;
101 | }
102 |
103 | .main p {
104 | margin-top: 10px;
105 | margin-bottom: 20px;
106 | text-align: center;
107 | font-size: 17px;
108 | }
109 |
110 | .form__main {
111 | display: flex;
112 | flex-direction: column;
113 | }
114 |
115 | .form__main input {
116 | width: 250px;
117 | height: 30px;
118 | border-radius: 5px;
119 | background-color: #04040496;
120 | margin-bottom: 10px;
121 | text-indent: 5px;
122 | }
123 |
124 | .form__main button {
125 | width: 150px;
126 | border-radius: 5px;
127 | font-size: 14px;
128 | background-color: #1e1e1e66;
129 | height: 25px;
130 |
131 | }
132 |
133 | .form__main .button {
134 | margin: 0 auto;
135 | margin-bottom: 20px;
136 | }
137 |
138 | .href {
139 | margin-bottom: 10px;
140 | }
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # 👨💻 Как правильно деплоить сайт? Node.js (Nginx и SSL Let's Encrypt), PM2 + GIT bonuse
2 |
3 | В этом гайде я расскажу как правильно задеплоить сайт на хостинг с доменом, настроим SSL сертификат для HTTPS протокола (certbot). Познакомимся с NGNIX и PM2.
4 |
5 | Для примера проекта, который будет здесь, я создал простейший сайт на express (свою визитку).
6 | Т.к. я не ux/ui designer, то дизайн не супер-пупер (простите).
7 |
8 | [Online demo: норм.com](https://норм.com)
9 |
10 | [Мой авторский TG канал](https://t.me/atom_baytovich)
11 |
12 | ## Начало
13 | (чуть сухой терминологии)
14 | - pm2 - это менеджер процессов, который поможет вам управлять вашим приложением и поддерживать его онлайн 24/7.
15 | - ngnix - это веб-сервер и почтовый прокси-сервер, работающий на Unix-подобных операционных системах. Имеет неблокирующий ввод/вывод. (поэтому такой быстрый) (кстати, его создатель - это разработчик из России)
16 | - https - это расширение протокола HTTP. Оно позволяет существенно снизить риск перехвата персональных данных посетителей (логины, пароли, номера банковских карт и т. д.), а также избежать подмены контента, в том числе рекламы, при загрузке сайта.
17 | - ssl (Secure Sockets Laye) - это цифровой сертификат, удостоверяющий подлинность веб-сайта и позволяющий использовать зашифрованное соединение.
18 |
19 | 📌 Для того, чтобы залить сайт, нам нужны ДОМЕН и VPS-сервер (в нашем случае UBUNTU 20.04).
20 | Я буду использовать [reg.ru (!не реклама!)](https://www.reg.ru/?rlink=reflink-10083843)
21 |
22 | 📌 Программы, которые я буду использовать: [TERMIUS - ssh](https://termius.com/) и [FileZilla - ftp](https://www.filezilla.ru/)
23 |
24 | [Как купить / настроить домен](https://help.reg.ru/hc/ru/articles/4408047000977-%D0%9A%D0%B0%D0%BA-%D0%BF%D1%80%D0%B8%D0%B2%D1%8F%D0%B7%D0%B0%D1%82%D1%8C-%D0%B4%D0%BE%D0%BC%D0%B5%D0%BD-%D0%BA-VPS) на vps рассказано у [reg.ru](https://www.reg.ru/?rlink=reflink-10083843) в статьях.
25 | Примечание* создание записи в dns сервера к домену произойдёт не сразу.
26 |
27 | ## Продолжаем
28 |
29 | 🥷 После покупки сервера с доменом и настройки домена на dns, мы можем приступить к "внутрянке".
30 |
31 | 1. Зайдём на наш vps-сервер по ssh и обнаружим, что он вовсе пустой.
32 | 2. Установим Node.js первым делом
33 |
34 | ```ssh
35 | sudo apt update # обновление состояние пакетов
36 | curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
37 | sudo apt install nodejs
38 | ```
39 | 3. После установки проверим версии Node.js и npm (должны видны версии в случае успеха)
40 | ```ssh
41 | node --version
42 | npm --version
43 | ```
44 | 4. Установим ngnix
45 | ```ssh
46 | sudo apt install nginx # Отвечаем 'y'
47 |
48 | ```
49 | 5. Установим SSL сертфиката Let's Encrypt
50 | ```ssh
51 | sudo apt install certbot python3-certbot-nginx # Отвечаем 'y'
52 | ```
53 | 6. Настроим Certbot и автообновление сертификата
54 | ```ssh
55 | sudo certbot --nginx -d домен.com -d www.домен.com
56 | certbot renew --dry-run # это автообновление
57 | ```
58 | 7. Далее зайдём в настройку виртуальных хостов
59 |
60 | ```ssh
61 | sudo nano /etc/nginx/sites-available/default
62 | ```
63 | 8. Поля конфигурации полностью изменить на: [полная конфигурация редиректов с www и https в тг](https://t.me/atom_baytovich/17)
64 | 9. Ctrl + X чтобы выйти, Ctrl + X чтобы сохранить и после нажать клавишу Enter. И перезагрузить Ngnix
65 | ```ssh
66 | sudo service nginx restart
67 | ```
68 |
69 | 10. ✨ БОНУС установка GIT
70 | ```ssh
71 | sudo apt update
72 | sudo apt install git
73 | ```
74 | * Настройка git
75 | ```
76 | git config --global user.name "Your Name"
77 | git config --global user.email "youremail@domain.com"
78 | ```
79 | * После клонируем проект с GitHub и стандартные команды
80 | ```ssh
81 | git clone 'url'
82 | ```
83 |
84 | #### _Вуаля, готово. После обращаемся к самому проекту_
85 |
86 | ## Перейдём к PM2
87 | Его установка глобально:
88 |
89 | ```ssh
90 | npm install pm2 -g
91 | ```
92 |
93 | Запуск проекта:
94 | ```ssh
95 | pm2 start index.js
96 | ```
97 |
98 | Остановка проекта:
99 | ```ssh
100 | pm2 stop index.js
101 | ```
102 |
103 | Посмотреть логи:
104 | ```ssh
105 | pm2 logs
106 | ```
107 |
108 | Посмотреть статус всех приложений pm2:
109 | ```ssh
110 | pm2 status
111 | ```
112 |
113 |
114 | ## ⭐️ Ну вот и всё. Спасибо за внимание!
115 | Подписывайся на [мой тг](https://t.me/atom_baytovich) и поставь звёздочку репозиторию. Я старался 😘
116 |
--------------------------------------------------------------------------------
/public/css/index-page.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,500;1,300&display=swap');
2 |
3 | html, body, div, span, applet, object, iframe,
4 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
5 | a, abbr, acronym, address, big, cite, code,
6 | del, dfn, em, img, ins, kbd, q, s, samp,
7 | small, strike, strong, sub, sup, tt, var,
8 | b, u, i, center,
9 | dl, dt, dd, ol, ul, li,
10 | fieldset, form, label, legend,
11 | table, caption, tbody, tfoot, thead, tr, th, td,
12 | article, aside, canvas, details, embed,
13 | figure, figcaption, footer, header, hgroup,
14 | menu, nav, output, ruby, section, summary,
15 | time, mark, audio, video {
16 | margin: 0;
17 | padding: 0;
18 | border: 0;
19 | font-size: 100%;
20 | font: inherit;
21 | vertical-align: baseline;
22 | }
23 |
24 | article, aside, details, figcaption, figure,
25 | footer, header, hgroup, menu, nav, section {
26 | display: block;
27 | }
28 |
29 | html {
30 | height: 100%;
31 | }
32 |
33 | body {
34 | line-height: 1;
35 | font-family: 'Montserrat', sans-serif;
36 | background: url(../img/server.jpg) no-repeat 50% fixed;
37 | background-size: cover !important;
38 | overflow: hidden;
39 | }
40 |
41 | ol, ul {
42 | list-style: none;
43 | }
44 |
45 | blockquote, q {
46 | quotes: none;
47 | }
48 |
49 | blockquote:before, blockquote:after,
50 | q:before, q:after {
51 | content: '';
52 | content: none;
53 | }
54 |
55 | table {
56 | border-collapse: collapse;
57 | border-spacing: 0;
58 | }
59 |
60 | /* закончились обнуляющие стили */
61 |
62 | .wrapper {
63 | min-height: 100vh;
64 | display: flex;
65 | align-items: center;
66 | color: #fff;
67 | }
68 |
69 | .main {
70 | max-width: 300px;
71 | padding: 0 20px 30px 20px;
72 | border-radius: 30px;
73 | margin: 0 auto;
74 | background-color: #1e1e1e66;
75 | backdrop-filter: blur(10px);
76 | box-shadow: #04040496 1px 1px 3px 2px;
77 | }
78 |
79 | .menu {
80 | text-align: center;
81 | }
82 |
83 | .years {
84 | font-size: 17px;
85 | font-weight: 500;
86 | margin-bottom: 10px;
87 | line-height: 18px;
88 | }
89 |
90 | .desc p {
91 | font-size: 17px;
92 | margin-bottom: 5px;
93 | line-height: 18px;
94 | }
95 |
96 | .contact-list {
97 | font-size: 17px;
98 | font-weight: 500;
99 | margin-top: 20px;
100 | }
101 |
102 | .socials-buttons {
103 | width: 100%;
104 | display: flex;
105 | margin-top: 20px;
106 | }
107 |
108 | .socials-link {
109 | margin-top: 20px;
110 | }
111 |
112 |
113 | /* АВАТАР */
114 | .avatar {
115 | height: 180px;
116 | }
117 |
118 | .avatar img {
119 | display: block;
120 | margin: 0 auto;
121 | height: 220px;
122 | width: 220px;
123 | border-radius: 60px;
124 | box-shadow: #8a8a8a96 1px 1px 3px 2px;
125 | position: relative;
126 | top: -60px;
127 | }
128 |
129 | /* СОЦИАЛЬНЫЙ БЛОК */
130 |
131 | .social-block {
132 | padding: 14px 4px 4px 4px;
133 | margin: 4px;
134 | display: flex;
135 | background: var(--back-color);
136 | color: #fff;
137 | cursor: pointer;
138 | border-radius: 10px;
139 | width: 256px;
140 | text-align: center;
141 | text-decoration: none;
142 | transition: .2s;
143 | border: var(--border);
144 | flex-direction: column;
145 | align-items: center;
146 | font-size: 14px;
147 | box-shadow: rgba(210, 120, 120, 0.59) 1px 1px 2px 2px;
148 | }
149 |
150 | .social-block img {
151 | color: #fff;
152 | cursor: pointer;
153 | text-align: center;
154 | font-size: 14px;
155 | height: 50px;
156 | width: 50px;
157 | margin-bottom: 8px;
158 | }
159 |
160 | .social-block:hover {
161 | box-shadow: rgba(193, 152, 152, 0.59) 1px 1px 2px 2px;
162 | }
163 |
164 | /* LINKS BLOCK */
165 |
166 | .block-link {
167 | margin-top: 10px;
168 | width: 100%;
169 | display: flex;
170 | padding: 0px;
171 | border-radius: 8px;
172 | transition: .2s;
173 | box-shadow: rgba(210, 120, 120, 0.59) 1px 1px 2px 2px;
174 | }
175 |
176 | .block-link a {
177 | padding: 8px 20px;
178 | color: #fff;
179 | cursor: pointer;
180 | border-radius: 8px;
181 | width: 250px;
182 | text-align: center;
183 | text-decoration: none;
184 | }
185 |
186 | .block-link:hover {
187 | box-shadow: rgba(193, 152, 152, 0.59) 1px 1px 2px 2px;
188 | }
189 |
190 | /* TITLE */
191 |
192 |
193 | .title {
194 | font-size: 30px;
195 | text-align: center;
196 | margin-bottom: 10px;
197 | }
--------------------------------------------------------------------------------