05 октября 2022

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Технический директор компании vverh.digital. JavaScript программист, любитель Kotlin и Swift.
Как бы часто мы ни начинали новые проекты, каждый раз заложить фундамент трудней всего. Поэтому сегодня мы продемонстрируем универсальную сборку для бэкенда на Node.js c Postgres в Docker. И, конечно, обязательно будем отдавать статику через Nginx.
🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres


Файловая архитектура и инструменты

Используем Visual Code Studio в качестве редактора кода. Операционная система любая. Обязательно установите Docker.

Для начала создадим вот такую файловую архитектуру:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

По мере чтения статьи файлов у нас прибавится. Что мы создали: .env будет содержать переменные среды окружения, в папке app будет контейнер с Node.js, в папке static будет статика, а в Nginx — Nginx-конфигурация. Для начала этого хватит.

Docker и docker-compose.yml

У нас будет два docker-compose.yml: dev и production. Начнем с dev-версии:

docker-compose.dev.yml
        version: '3'

services:
  # Контейнер с Node.js
  app:
    build:
      context: ./app
      target: dev
    tty: true
    working_dir: /opt/server
    volumes:
      - ./app:/opt/server
      - ./static:/opt/static
    env_file:
      - .env
    expose:
      - '3000'
    depends_on:
      - db

  # Контейнер с базой данных
  db:
    image: postgres:12-alpine
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql/data
    expose:
      - '5432'
    restart: always

  # Контейнер с nginx
  nginx:
    container_name: proxy_nginx
    depends_on:
      - app
      - db
      - pgadmin
    image: nginx:latest
    ports:
      - '80:80'
    volumes:
      - ./nginx:/etc/nginx/conf.d
      - ./static:/var/www/static
    restart: always

  # Контейнер с pgadmin
  pgadmin:
    container_name: pgadmin
    depends_on:
      - db
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: info@proglib.io
      PGADMIN_DEFAULT_PASSWORD: qwertyuiop
    expose:
      - '80'
    restart: always
    

Здесь стоит обратить внимание на четыре вещи:

  1. Контейнеры app и nginx связаны со статикой. Для Node.js папка со статикой будет ниже уровнем от app.js (главным файлом, чуть позже создадим), на одном уровне с папкой app.
  2. Контейнер с базой данных содержит ${DB_USER} и ${DB_PASSWORD}. Это переменные из .env-файла, мы его начнем заполнять спустя пару мгновений.
  3. Контейнер с Pgadmin содержит такие строки: PGADMIN_DEFAULT_EMAIL и PGADMIN_DEFAULT_PASSWORD. Вы можете там указать свою почту и свой пароль, он будет использоваться для входа в Pgadmin.
  4. В контейнере с Node.js есть раздел build. Там есть target dev. Просто пока обратите внимание.

Теперь перейдем к production-версии:

docker-compose.production.yml
        version: '3'

services:
  # Контейнер с Node.js
  app:
    build:
      context: ./app
      target: production
    tty: true
    working_dir: /opt/server
    volumes:
      - ./app:/opt/server
      - ./static:/opt/static
      - /opt/server/node_modules/
    env_file:
      - .env
    expose:
      - '3000'
    depends_on:
      - db
    command: npm run start

  # Контейнер с базой данных
  db:
    image: postgres:12-alpine
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql/data
    expose:
      - '5432'
    restart: always

  # Контейнер с nginx
  nginx:
    container_name: proxy_nginx
    depends_on:
      - app
      - db
      - pgadmin
    image: nginx:latest
    ports:
      - '80:80'
    volumes:
      - ./nginx:/etc/nginx/conf.d
      - ./static:/var/www/static
    restart: always

  # Контейнер с pgadmin
  pgadmin:
    container_name: pgadmin
    depends_on:
      - db
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: info@proglib.io
      PGADMIN_DEFAULT_PASSWORD: qwertyuiop
    expose:
      - '80'
    restart: always
    

Здесь стоит отметить, что файлы отличаются между собой только настройками для app контейнера.

Во-первых, мы монтируем папку node_modules внутри контейнера специально, чтобы не было проблем между dev и production версией.

Во-вторых, мы исполняем команду npm run start, которая будет запускать наше приложение (позже ее напишем).

В-третьих, в разделе build у нас другой targetproduction. И вот сейчас мы плавно переходим дальше…

Как Docker поймет, какой файл запускать

Идем в .env файл и вставляем следующее содержимое:

.env
        # dev or production
NODE_ENV=dev

DB_NAME=api
DB_USER=postgres
DB_PASSWORD=secret007
DB_HOST=db

COMPOSE_FILE=docker-compose.${NODE_ENV}.yml
    

В самом начале в переменную NODE_ENV мы записываем в каком режиме мы будем сейчас работать: dev или production. В самом низу мы объединяем COMPOSE_FILE и NODE_ENV. Особо внимательные догадались, что будет происходить в зависимости от содержимого переменной NODE_ENV при команде:

        docker-compose up
    

Будет использоваться тот или иной файл. То есть, если в NODE_ENV указана строка dev, мы активируем файл docker-compose.dev.yml. Если указана строка production, то мы активируем файл docker-compose.production.yml.

Также в файле есть другие переменные для базы данных. Можете поменять их содержимое, если хотите. Главное — помните: DB_HOST должен содержать в себе название контейнера с базой данных из docker-compose.yml.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

Контейнер с Node.js

Пора создавать наше приложение на Node.js. Если подумать, мы провели много подготовительной работы, но это только лишь ⅓ из всего того, что нам еще нужно сделать.

Для начала перейдем в папку app и создадим там файл Dockerfile с таким содержимым:

Dockerfile
        # dev
FROM node:16.10.0-alpine AS dev
RUN apk add --no-cache tzdata
ENV TZ Europe/Moscow
ENV NODE_PATH /opt/server/node_modules

WORKDIR /opt/server/

CMD [ "node" ]

# production
FROM node:16.10.0-alpine AS production
RUN apk add --no-cache tzdata
ENV TZ Europe/Moscow
ENV NODE_PATH /opt/server/node_modules

WORKDIR /opt/server/

COPY /*.json ./
RUN npm i

CMD ["sh", "-c", "npm run start"]
    

Чтобы класс new Date() в Node.js возвращал корректное для вас время, можно поменять таймзону контейнера. Например, если хотите установить уральское время, вместо Europe/Moscow напишите Asia/Yekaterinburg.

Контейнер с Nginx

Прежде чем продолжить, надо еще добавить в папку nginx конфиг. Для этого внутри папки nginx создайте файл nginx.conf со следующим содержимым:

nginx.conf
        server {
  root /var/www;
  listen 80;
  gzip on;
  gzip_types text/plain application/xml text/css application/javascript;
  gzip_min_length 1000;
  # Проверку можно будет добавить в Express
  client_max_body_size 0;

  # C любовью
  add_header X-Created-By "Proglib";

  location / {
    # Ищем файл в папке static (ее Docker собрал слизав у Node)
    # Если ничего не нашли выбрасываем прокси
    try_files /static/$uri $uri @nodeproxy;
  }

  location @nodeproxy {
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 1m;
    proxy_connect_timeout 1m;
    # app это алиас для контейнера с Node.js
    proxy_pass http://app:3000;
  }

  # А по этому маршруту проксируем все в Pgadmin
  location /pgadmin {
    proxy_set_header X-Script-Name /pgadmin;

    proxy_pass http://pgadmin;
    proxy_intercept_errors on;
    error_page 503 = @nodeproxy;
    error_page 502 = @nodeproxy;
  }
}
    

Запускаем сборку

Вот теперь можно все запустить. Из основной папки стартуем нашу сборку командой:

        docker-compose up --build -d
    
🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

После того как все будет готово, и в терминале появятся четыре заветных зеленых done можно продолжать. Теперь-то уже можно начать непосредственно работу с Node.js.

Работаем с Node.js в контейнере

Давайте перейдем в контейнер с Node.js командой:

        docker-compose exec app sh
    

Если вы все сделали правильно, в терминале появится у строки ввода приписка opt/server.

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

В этом режиме вы можете выполнять команды внутри контейнера. Чтобы выйти из контейнера, напишите команду:

        exit
    

Теперь, давайте напишем немного Node.js кода (войдите в контейнер, если вышли). Сначала надо инициализировать проект командой:

        npm init
    

Далее, давайте создадим файл app.js внутри папки app. Создаем его самым обычным способом (можете через терминал, как хотите). Когда файл появится в VS Code, вы можете проверить, появился ли он в контейнере, для этого достаточно написать команду:

        ls
    

У нас должно быть сейчас 3 файла в контейнере: Dockerfile, app.js, package.json.

После установим нужные нам пакеты командой:

        npm install express nodemon

    

Поместим следующий код в app/app.js:

app.js
        // Express
const express = require('express')
const app = express()

// Router
const router = express.Router()

// Главная
router.get('/', (_req, res) => {
  res.status(200).json({
    message: 'Hello World',
  })
})

// Обработка всего остального
router.get('/*', (_req, res) => {
  res.status(400).json({
    error: 'Запрос не может быть обработан, маршрут не найден'
  })
})

// Routes
app.use('/', router)

app.listen(3000, () => {
  console.log('Сервер запущен')
})
    

И создадим команду start в package.json:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Команда start:

        nodemon ./app.js
    

Теперь можно запустить внутри контейнера команду:

        npm run start
    

В консоль вы получите сообщение «Сервер запущен». Если перейдете в своем браузере по адресу http://localhost/, то увидите что-то подобное:

У автора стоит плагин для Google Chrome JSON Viewer
У автора стоит плагин для Google Chrome JSON Viewer

Если создадите файл внутри папки app с окончание js или jsonnodemon подхватит изменения и перезапустит проект. Аналогично, если вы просто поменяете содержимое какого-нибудь файла с данными расширениями.

Если отключите nodemon командой ctrl + c, то по прошлому адресу можно увидеть сообщение от Nginx: 502 Bad Gateway. Если поместите в папку static любой файл, он будет доступен по указанному маршруту. Например, static/file.txt => http://localhost/file.txt. Даже если nodemon не работает. Помним, за работу со статикой у нас отвечает Nginx.

Устанавливаем Linter для JavaScript

Ну а как без этого? Для начала, перейдите в VS Code в раздел с плагинами, скачайте и активируйте плагин Eslint:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Вот теперь можно установить сам Eslint в проект командой:

        npm init @eslint/config
    

Сначала выбираем To check syntax and find problems, после Common JS, на вопрос о фреймворке выбираем None of these, на вопрос про TypeScript отвечаем No. Платформу выбираем Node, галочку снимаем с Browser (все через пробел). Настройки сохранять будем в JSON, соглашаемся с установкой последней версии eslint и выбираем в конце npm.

Если у вас все получилось сделать правильно, будет работать подсветка:

Возможно, придется перезапустить VS Code.
Возможно, придется перезапустить VS Code.

Это все благодаря тому, что мы монтируем всю папку app (вместе с node_modules) в dev-режиме. Осталось добавить команду для тестового (dev) запуска nodemon, который сначала будет парсить проект с помощью Eslit. Для этого добавьте еще две команды в package.json:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

dev:

        nodemon ./app.js --exec \"npm run lint && node\"

    

lint:

        eslint .
    

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

        npm run dev
    

Допустите какую-нибудь ошибку в коде и сохраните файл:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Если получили ошибку, то вы все настроили верно. Поздравляю, теперь ваш код будет чище.

Подключаемся к базе данных

Выходим на финишную прямую: осталось лишь подключить Node.js к базе данных.

В контейнере устанавливаем пакет для соединения с базой данных:

        npm install pg
    

Добавляем код в app/app.js:

        // DB
const { Pool } = require('pg')
const pool = new Pool({
  user: process.env.DB_USER,
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  password: process.env.DB_PASSWORD,
  port: 5432,
})

pool.query('SELECT NOW()', (err, res) => {
  console.log(err, res)
  pool.end()
})
    

Должно получиться как-то так:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Если вы сейчас сохраните и запустите проект, nodemon выбросит ошибку. А все потому, что у нас нет базы данных под названием api (если вы не меняли название). Для этого переходите по адресу http://localhost/pgadmin, и вводите данные из docker-compose.yml для входа. Если вдруг получили ошибку от Nginx после авторизации, то просто нажмите F5.

Нам нужно создать новый сервер:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Во вкладке General введите любое название. Во вкладке Connection введите наши данные для подключения:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Базы api еще нет, поэтому вместо нее введите postgres. Создайте новую базу под названием api:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Теперь запускайте nodemon. Поздравляем, вы подключились к базе данных:

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Финальная сборка

Так как мы написали все нужные команды, теперь можем поменять NODE_ENV в .env на production и запустить наш проект в другом режиме командой:

        docker-compose up --build -d
    

В данный момент production сборка от dev отличается тем, что node_modules контейнер создает свои, и они больше не прокинуты через volumes между контейнером и ПК. Поэтому команда npm i (не забываем про разные таргеты) из Dockerfile установит node_modules только в контейнер. То есть, если вы удалите свои node_modules, то теперь на контейнер это не повлияет.

Поэтому эту команду можно использовать для того, чтобы быстро тянуть файлы с GitHub и запускать проект на сервере. Мы же не храним node_modules в git-репозиториях.

Как вернуться в dev

Для работы в dev-режиме вам просто потребуется вернуть NODE_ENV в dev режим, перезапустить проект через композ:

        docker-compose up --build -d
    

Зайти в контейнер командой:

        docker-compose exec app sh
    

Установить пакеты:

        npm install
    

Запустить внутри команду:

        npm run dev
    

Это обеспечит вам гибкость при разработке и разделит production от dev, позволит работать с линтером и подсветкой в VS Code. А также, разработка внутри контейнера обезопасит ваш компьютер от всяких вредоносных валварей.

***

Напоследок

Для работы с базой данных автор рекомендует Sequelize. Плюс, разделенные docker-compose.yml позволят вам создавать разные Nginx конфиги для production и dev. Например, в боевую сборку можно добавить образ gordonchan/auto-letsencrypt и открыть 443 порт в nginx для https.

И вот ссылка на репозиторий GitHub. Там можно найти весь код проекта.

Материалы по теме

Комментарии

ВАКАНСИИ

Добавить вакансию
Flutter Developer
по итогам собеседования
AppSec BP
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ