Привет! Меня зовут Артём Шумейко, я Python-разработчик и создатель одноименного канала на YouTube. Представьте: у вас есть проект с фронтенд- и бэкенд-частью. Первый работает на одном порту и отображает данные, после — передает бэкенду. Второй работает на другом порту, принимает и обрабатывает эти данные, после чего возвращает ответ. Обычно сайты находятся на едином домене с фронтендом и бэкендом, а здесь — на двух отдельных.  

Будучи новичком я не понимал, как объединить фронтенд и бэкенд. Думал, нужно подключать два домена и неведомым образом их «подружить». Но все оказалось намного проще. В тексте поделюсь подробной инструкцией и покажу, как задеплоить проект на облачный сервер

О проекте

В инструкции не будем писать веб-приложение с нуля — возьмем готовый проект, который состоит из 30-40 строк кода. Фронтенд будет отображать данные с бэкенда: картинки и текст.

from fastapi import FastAPI
import random

@app.get("/items")
def get_items():
    items = [
        {
            "id": 1,
            "name": "Docker",
            "img": "https://static-00.iconduck.com/assets.00/docker-icon-2048x2048-5mc7mvtn.png",
        },
        {
            "id": 2,
            "name": "Nginx",
            "img": "https://www.svgrepo.com/show/373924/nginx.svg",
        },
        {
            "id": 3,
            "name": "GitHub",
            "img": "https://cdn-icons-png.flaticon.com/512/25/25231.png",
        },
    ]
    random.shuffle(items)
    return items
Фронтенд и бэкенд проекта.
Фронтенд и бэкенд проекта.

Весь проект локально запущен в двух терминалах. В первом — бэкенд с портом 8000, во втором — фронтенд с портом 5173. По сути, это два разных сервера, которые нужно соединить.

Вариант объединения фронтенда и бэкенда.
Вариант объединения фронтенда и бэкенда.

Чтобы объединить фронтенд и бэкенд, есть две стратегии. Первый способ — это реверс-прокси(обратный прокси), при котором мы не создаем домен. В нем разворачиваем фронтенд на основной сайт, а бэкенд — на адрес/API. Второй — это использование обратного прокси через создание поддомена. В нем создаем еще один домен к основному, например api.mysite.ru, чтобы разделить сайты. В тексте будем использовать обратный прокси без поддомена.

Два метода объединения фронтенда и бэкенда.
Два метода объединения фронтенда и бэкенда.

Настройка Docker-файла

Бэкенд

Нужно написать два простых Docker-файла, чтобы перенести данные в контейнеры. В образ Python добавляем slim, чтобы не занимать много места:

FROM python:3.11.9-slim

Перемещаем все файлы в локальную директорию и добавляем зависимости в requirements.txt. После копируем файлы в Docker-образ:

COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

COPY . .

Сначала устанавливаем файлы с зависимостями, которые редко меняются, и только потом остальные. Запускаем код: 

CMD [ "python", "main.py" ]

Фронтенд

Копируем весь Docker-файл и добавляем его в папку frontend. В нем будем использовать не Python, а JavaScript и Node.js. 

Указываем последнюю версию ноды типа alpine и называем ее build:

FROM node:alpine as build

Здесь зависимости находятся не в папке requirements.txt, а в package.json. Копируем их и добавляем в файл:

COPY package.json package.json
RUN npm install

После — копируем оставшиеся файлы:

COPY . .
RUN npm run build

Фронтенд поддерживает только статические файлы: JavaScript, HTML и CSS. Чтобы забрать их из контейнера, запускаем nginx прямо в коде:

FROM nginx: stable-alpine

После команды npm build внутри Docker-образа появилась папка /dist. В ней хранятся созданные билды, а именно — статические файлы. Отправляем их в nginx:

COPY --from=build /dist /usr/share/nginx/html

Далее копируем nginx.conf в классическую директору nginx:

COPY --from=build nginx.conf /etc/nginx/conf.d/default.conf

Указываем порт, на котором будет работать nginx, через EXPOSE 3000, но можно выбрать другой:

EXPOSE 3000
CMD [ "nginx", "-g", "daemon off;" ]

Чтобы отдавать файлы index html, создаем nginx.conf: 

server {
  listen 3000;

  location / {
    root /usr/share/nginx/html;
    index index.html index.htm;
    try_files $uri $uri/ /index.html =404;
  }

  include /etc/nginx/extra-conf.d/*.conf;
}

Общая сборка приложения

Создаем файл docker-compose, чтобы управлять сразу несколькими контейнерами. Здесь используем контейнер бэкенда и фронтенда в YAML-формате.

services:
  backend:
    build:
      context: ./backend

  frontend:
    build:
      context: ./frontend

Перед контейнерами запускаем nginx из готового образа DockerHub:

services:
  nginx:
    image: nginx:stable-alpine
    ports:
      - "80:80"

  backend:
    build:
      context: ./backend

  frontend:
    build:
      context: ./frontend

Указываем стандартный порт HTTP — 80. 

Далее нужно выполнить две задачи: 

  • настроить приложение так, чтобы nginx запускался только после бэкенда и фронтенда;

  • добавить volumes, чтобы nginx видел файлы с локального сервера.

services:
  nginx:
    image: nginx:stable-alpine
    ports:
      - "80:80"
    volumes:
      - './nginx.conf:/etc/nginx/nginx.conf'
    depends_on:
      - backend
      - frontend

Соединяем все контейнеры в единую сеть и указываем зависимости:

networks:
  dev:

services:
  nginx:
    image: nginx:stable-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - './nginx.conf:/etc/nginx/nginx.conf'
    depends_on:
      - backend
      - frontend
    networks:
      - dev
    
  backend:
    build:
      context: ./backend
    networks:
      - dev

  frontend:
    build:
      context: ./frontend
    networks:
      - dev

Настройка конфигурации nginx

Используем готовый файл nginx.conf. Называем пользователя root, добавляем необходимое количество worker-нод и порт 80. 

user  root;
worker_processes  1;

events {
}

http {
    server {
        listen 80;
    }
}

Указываем Location, чтобы при запуске выводить фронтенд на главную страницу. После — добавляем proxy_pass, чтобы проксировать запросы через nginx, и порт 3000, как на нашем сервере. 

location / {
    proxy_pass http://frontend:3000/;
}

Далее подключаем бэкенд и добавляем в Location /api/. Порт — 8000, поскольку контейнер с бэкендом использует именно его.

location /api/ {
    proxy_pass http://backend:8000/;
}

Конфигурация nginx готова:

user  root;
worker_processes  1;

events {
}

http {
    server {
        listen       80;

        location / {
            proxy_pass http://frontend:3000/;
        }

        location /api/ {
            proxy_pass http://backend:8000/;
        }
    }
}

Создание облачного сервера

Переходим в панель управления Selectel и создаем новый сервер. Указываем имя, регион и операционную систему — в нашем случае Ubuntu 24.04 LTS 64-bit. Для работы сайта подойдет минимальная конфигурация: 

  • 1 CPU,

  • 1 ГБ RAM,

  • 10 ГБ SSD,  

  • публичный IP-адрес.

Далее добавляем SSH-ключ. Указываем имя и копируем команду для терминала. После получаем публичный ключ и добавляем его в окно создания.

Окно с созданием SSH-ключа.
Окно с созданием SSH-ключа.

Теперь можно автоматически подключаться к серверу без использования пароля, с помощью команды ssh root@<IP-адрес сервера>. Нажимаем Создать сервер.

Создание репозитория

Создаем новый репозиторий для хранения кодовой базы. Если проект не конфиденциален и вы хотите клонировать его на сервер без дополнительной авторизации, сделайте репозиторий публичным. Далее — переходим в терминал (предварительно не забудьте подключиться к серверу) и клонируем репозиторий с помощью команды git clone <адрес репозитория>.

Создание репозитория.
Создание репозитория.

Поскольку у нас около 20 файлов, создаем файл .gitignore. Туда добавляем файлы с зависимостями — venv и node-modules. Пишем initial commit, чтобы отправить их в Git.

venv/
node_modules/

Переходим в терПереходим в терминал и обновляем пакеты, чтобы установить Git. Создаем клон и скачиваем код по HTTPS с GitHub. На выходе получаем папку test-deploy-site, в которой находятся все файлы. 

Код в терминале.
Код в терминале.

Деплой веб-приложения на сервер

Теперь нужно установить Docker. Используем готовый код — копируем и добавляем его в терминал.

Код в Docker.
Код в Docker.

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

docker compose up --build

Адрес бэкенда и фронтенда скрыты от конечного пользователя и мошенников. Напрямую с ними работать не получится, только через nginx. 

Заключение

Нам удалось соединить фронтенд с бэкендом в одном веб-приложении и развернуть его на облачном сервере. Оставляю код на GitHub и видеоверсию инструкции, чтобы вы легко могли повторить все шаги.

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


  1. olku
    27.08.2024 10:00

    Nginx тут лишний.


    1. JohnDorian
      27.08.2024 10:00

      Why?


      1. olku
        27.08.2024 10:00

        TLS не терминируется решением, но 443 выставлен наружу. Идея терминировать сервисом хостера, если такой есть, хорошая. Имена хостов захардкожены, это плохая практика - в Swarm будет один, в Kubernetes другой. Бекенд проксировать не обязательно, если нет задачи делать его открытым всему миру. Спеки OpenAPI фронтендер обычно смотрит лишь во время разработки. Трафик с фронтенда на бекенд гонять через nginx не нужно, как API Gateway он так себе. Фронтенд тоже непонятно зачем проксировать, он самодостаточен.

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


    1. ig_rudenko
      27.08.2024 10:00
      +4

      Уточню, что можно обойтись лишь одним nginx, вместо двух, если в frontend конфигурации прописать для location /api/ перенаправление на backend


    1. trigremm
      27.08.2024 10:00
      +1

      Пояснение.

      Nginx на фронте тут лишний. Так как перед ним уже стоит nginx.


  1. vlyutov
    27.08.2024 10:00
    +1

    Спасибо за статью


  1. gorod0k
    27.08.2024 10:00
    +1

    Ох. Начиналось все с того, что "а давайте сделаем серверный js, чтобы разработчик мог, зная только js разрабатывать и фронт и бэк". Но, надо заметить, получилось в основном довольно удобно в плане реализации, хоть и не без костылей.

    А теперь у них и фронт на отдельном сервере запускается. Так сказать, обмажутся своим node js и ... друг друга в ...