Материал учебного циклаОткрытый материал

От локального приложения к серверу: запуск API и хранение данных

Как перенести API с PostgreSQL и Redis с локального компьютера на сервер: собрать конфигурацию, сохранить данные, проверить готовность и выполнить первый запуск.

Инфраструктура и DevOps #DevOps #Docker Compose #FastAPI #PostgreSQL #инфраструктура #развёртывание
Учебный цикл От Docker до работающего сервиса Материал 1 из 9

От локального приложения к серверу: что меняется после docker compose up

На компьютере разработчика работает сервис обработки обращений. FastAPI принимает запрос, записывает обращение в PostgreSQL и при необходимости читает данные из Redis. Команда запускает docker compose up, открывает localhost:8000 и видит правильный ответ.

Теперь сервисом должна пользоваться команда поддержки. Разработчик переносит файлы на сервер и запускает ту же команду. Контейнеры поднялись, но это ещё не отвечает на вопросы:

  • Сохранится ли обращение после пересоздания контейнера?
  • Как приложение найдёт базу данных после перезапуска?
  • Когда можно направлять на него запросы?
  • Что произойдёт после перезагрузки сервера?
  • Как понять, какая версия запущена и почему она не отвечает?

Локальный запуск доказывает, что компоненты способны работать вместе в текущем окружении. Серверный запуск требует описать где они работают, как получают настройки, какие данные переживают перезапуск и по каким признакам система считается готовой.

В этой главе соберём первый серверный контур на одной виртуальной машине. Образы, контейнеры и основы Compose уже разобраны в материале о Docker. Здесь сосредоточимся на переходе от удобного окружения разработки к управляемому запуску.

Что именно мы переносим

Наш пример состоит из трёх процессов:

  • API принимает обращения и возвращает их состояние.
  • PostgreSQL хранит обращения как постоянные данные.
  • Redis ускоряет повторные чтения. В этом примере он служит кэшем: потеря его содержимого замедлит работу, но не должна уничтожить обращения.

Локально все три процесса могут быть контейнерами. На сервере состав остаётся тем же, однако появляются ещё два уровня. Операционная система предоставляет память, диск и сетевые интерфейсы. Docker Engine запускает контейнеры и управляет их жизненным циклом. Compose читает описание приложения и просит Docker создать нужные ресурсы.

После запуска путь запроса выглядит так:

клиент → входная точка сервера → контейнер API → PostgreSQL; при необходимости API обращается к Redis

Пока мы проверяем сервис только на самом сервере. Входом будет адрес 127.0.0.1:8000. Публичный домен, HTTPS и обратный прокси подключаются следующим шагом. Такое ограничение позволяет проверить приложение, не открывая незавершённую конфигурацию в интернет.

Слово «сервер» здесь обозначает виртуальную машину с Linux, а не особый вид приложения. Контейнеры используют её ресурсы. Если остановится вся машина, одновременно остановятся API, база и кэш. Это единая точка отказа нашего первого контура.

Выводы раздела

  • Compose описывает и запускает набор сервисов; вычислительные ресурсы и диск предоставляет сервер.
  • Данные PostgreSQL должны жить вне заменяемого контейнера.
  • Успешный ответ от API и готовность всей системы проверяются отдельно.

Почему локальный Compose нельзя просто скопировать

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

На сервере приоритеты меняются.

Вопрос Локальная разработка Серверный запуск
Откуда берётся код Часто из папки на компьютере Из заранее собранного образа определённой версии
Что происходит при перезапуске Разработчик может вмешаться вручную Сервис должен запускаться и восстанавливать соединения самостоятельно
Где находятся данные Возможен временный набор Нужен постоянный том и план резервного копирования
Какие порты доступны Удобство проверки важнее Открываются только необходимые входы
Где находятся пароли Нередко в локальном .env Вне репозитория, с ограниченным доступом
Как заметить сбой Видно в терминале Нужны проверки готовности, логи и оповещения

В рекомендациях Docker для Compose в production среди типичных изменений названы отказ от подключения исходного кода как тома, другая конфигурация портов и переменных окружения, а также политика перезапуска.

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

Сначала посчитаем ресурсы сервера

Контейнер не получает отдельную физическую память. API, PostgreSQL, Redis, Docker и сама операционная система расходуют память одной машины. Если её не хватит, часть процессов может быть остановлена системой, а ответы станут нестабильными.

Первую оценку удобно сделать как бюджет:

Запас памяти = память сервера − пик ОС − пик API − пик PostgreSQL − пик Redis

Допустим, для расчёта мы рассматриваем машину с 4 ГиБ памяти и предварительно оцениваем пики так: ОС и служебные процессы - 0,5 ГиБ, API - 0,8 ГиБ, PostgreSQL - 1,3 ГиБ, Redis - 0,2 ГиБ.

Тогда расчётный остаток равен 4 − 0,5 − 0,8 − 1,3 − 0,2 = 1,2 ГиБ. Он понадобится для колебаний нагрузки, файлового кэша и неточности первоначальной оценки. Эти числа служат примером метода расчёта, а не готовой рекомендацией по размеру сервера. После запуска их нужно заменить измеренными значениями.

С диском рассуждение похожее. На нём находятся образы, логи и данные PostgreSQL. Размер базы обычно растёт, а старые образы и журналы занимают место незаметно. Поэтому нужно оценить не только текущий объём, но и скорость роста. Резервные копии следует хранить также вне этой машины: локальный том не поможет, если потерян сам сервер.

Процессор и диск тоже могут стать ограничением. Например, медленный диск увеличит время записи в PostgreSQL даже при свободной памяти. Проверка ресурсов после запуска нужна именно потому, что оценка до нагрузки остаётся гипотезой.

Выводы раздела

  • Сумма пикового потребления компонентов должна оставлять запас машине.
  • Постоянный том защищает от пересоздания контейнера, но не от потери сервера.
  • Размер сервера уточняют по измерениям API, базы, кэша и диска под реальной нагрузкой.

Код должен стать версионированным артефактом

Локально разработчик меняет Python-файл и сразу видит результат. На сервере такой подход затрудняет ответ на простой вопрос: «Какая версия сейчас работает?»

Для развёртывания приложение собирают в образ и помещают в реестр образов. В нашем примере это registry.example.com/requests-api:1.0.0. Адрес вымышленный: при публикации его нужно заменить адресом своего реестра.

Схема выпуска становится явной:

изменение кода → проверка → сборка образа → публикация образа → получение образа сервером → запуск

Тег 1.0.0 помогает человеку определить выпуск. Для строгой воспроизводимости в рабочем процессе полезно фиксировать и digest образа: тег в реестре технически может быть перезаписан. Исходный код при этом не подключают с хоста в контейнер. Новую версию получают через новый образ, а не правкой файлов внутри запущенного процесса.

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

Как выглядит серверная конфигурация

Ниже сокращённый compose.prod.yaml. Он показывает границы между сервисами. Предполагается, что образ API уже собран, приложение умеет читать пароль из файла, а /health/ready реализован. Файл секрета и настройки API создаются на сервере отдельно.

name: requests

services:
  api:
    image: registry.example.com/requests-api:1.0.0
    command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
    env_file:
      - /etc/requests-app/api.env
    secrets:
      - db_password
    ports:
      - "127.0.0.1:8000:8000"
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2)"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    restart: unless-stopped
    stop_grace_period: 30s

  db:
    image: postgres:17
    environment:
      POSTGRES_DB: requests
      POSTGRES_USER: requests_demo
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped

  redis:
    image: redis:7
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped

volumes:
  pgdata:

secrets:
  db_password:
    file: /etc/requests-app/secrets/db_password

Здесь есть несколько решений, которые легко неверно прочитать.

name: requests задаёт имя проекта Compose. От него зависит имя создаваемого тома, например requests_pgdata. Если случайно запустить ту же конфигурацию под другим именем проекта, Compose может создать новый пустой том. База покажется «потерянной», хотя старый том по-прежнему находится на сервере.

image у API указывает на готовый выпуск. Сервер получает образ из реестра. Папка с исходным кодом для запуска ему не нужна. Значения postgres:17 и redis:7 также фиксируют основные версии зависимостей. Обновление PostgreSQL между основными версиями требует отдельного плана миграции данных.

command запускает Uvicorn на 0.0.0.0 внутри контейнера. Это означает, что процесс принимает соединения на сетевом интерфейсе контейнера. Если привязать его только к 127.0.0.1 внутри контейнера, перенаправление порта с хоста не доставит запрос до приложения.

ports привязывает порт на сервере к 127.0.0.1. Запись 127.0.0.1:8000:8000 означает «порт 8000 на локальном интерфейсе сервера ведёт к порту 8000 контейнера». Если убрать адрес хоста и написать просто 8000:8000, Docker может открыть порт на всех интерфейсах машины. Это особенно опасно для сервиса, перед которым ещё не настроены HTTPS, прокси и правила доступа. Синтаксис публикации портов описан в документации Docker.

У PostgreSQL и Redis нет ports. API обращается к ним по внутренней сети Compose, используя имена db и redis. Внутри контейнера API адрес localhost указывает на сам API-контейнер, а не на PostgreSQL. Compose создаёт внутреннюю сеть и разрешает имена сервисов без ручной записи IP-адресов. При пересоздании контейнера IP может поменяться, а имя сервиса останется тем же. Это следует из модели сети Compose.

env_file передаёт несекретные настройки приложению. В /etc/requests-app/api.env могут находиться APP_ENV=production, DB_HOST=db, DB_NAME=requests и REDIS_HOST=redis. Значение DB_PASSWORD_FILE=/run/secrets/db_password сообщает нашему приложению, где прочитать пароль. Само FastAPI-приложение должно реализовать такое чтение: соглашение _FILE не появляется в нём автоматически.

secrets выдаёт файл пароля только тем сервисам, которым он перечислен. Образ PostgreSQL поддерживает POSTGRES_PASSWORD_FILE; Compose предоставляет файл по указанному пути. Файл на сервере должен храниться вне репозитория с ограниченным доступом. Docker рекомендует использовать секреты для чувствительных значений, а поддержка _FILE описана в документации образа PostgreSQL.

pgdata хранит файлы базы вне жизненного цикла контейнера. При пересоздании контейнера PostgreSQL том остаётся. Для выбранного здесь postgres:17 он подключён к /var/lib/postgresql/data. Тома сохраняются после удаления использовавшего их контейнера, но находятся на том же сервере.

redis в примере используется только как кэш. Для него не задан постоянный том. После пересоздания контейнера кэш может оказаться пустым, поэтому API должен уметь получать исходные данные из PostgreSQL и заполнять кэш заново. Если Redis хранит очереди или другую ценную информацию, такая конфигурация уже не подходит.

restart: unless-stopped просит Docker запускать контейнер после сбоя процесса или перезапуска Docker, кроме случаев, когда его остановили вручную. stop_grace_period даёт API время завершить текущие операции после сигнала остановки. Политика перезапуска не исправляет ошибку в приложении и не переносит сервис на другую машину. Точное поведение политик описано в документации Docker.

Есть и важное ограничение примера: POSTGRES_USER: requests_demo создаёт в официальном образе суперпользователя PostgreSQL. Такая роль удобна для учебного первого запуска, но рабочему API следует выдать отдельную роль с необходимыми правами, оставив административную роль для сопровождения. Это свойство POSTGRES_USER прямо указано в документации образа.

«Запущен» и «готов» означают разное

Контейнер PostgreSQL может существовать и работать, пока сама база ещё инициализируется. Контейнер API может принимать TCP-соединения, но отвечать ошибкой на запрос, которому нужны таблицы. Поэтому один статус running мало что говорит о готовности приложения.

В конфигурации PostgreSQL выполняет pg_isready. У API проверка обращается к /health/ready. Параметры interval, timeout, retries и start_period задают частоту проверки, допустимое время ответа, число неудачных попыток и начальный период запуска.

Минимальный смысл двух проверочных адресов API можно задать так:

  • /health/live отвечает, когда сам процесс способен обслужить простой запрос. Проверка не должна выполнять тяжёлую работу.
  • /health/ready отвечает успешно, когда сервис способен выполнить необходимую для обычной работы операцию, например простой запрос к PostgreSQL. При недоступности базы он возвращает ошибку, например HTTP 503.

Если Redis здесь лишь ускоряющий кэш, временная потеря Redis не обязана делать API «неготовым». Если без Redis нельзя выполнить основную операцию, проверка готовности должна учитывать и его. Состав проверки следует из контракта сервиса, а не из списка всех используемых технологий.

depends_on с condition: service_healthy заставляет Compose дождаться успешной проверки базы при создании API через Compose. Оно не гарантирует, что база не отключится позже. После перезагрузки машины контейнеры могут запускаться политиками Docker; приложение всё равно должно повторять неудавшиеся подключения с ограничением по времени и корректно сообщать об ошибке. Документация Compose отдельно различает запуск контейнера и готовность зависимости.

Проверка API тоже имеет предел. Успешный SELECT 1 подтверждает соединение с PostgreSQL, но не доказывает, что установлена нужная версия схемы, верно настроены права и проходят реальные операции. Для этого после запуска нужен отдельный проверочный запрос по основному сценарию.

Выводы раздела

  • running означает состояние процесса, healthy означает успешное выполнение заданной проверки.
  • depends_on помогает при начальном запуске, но не заменяет восстановление соединений в приложении.
  • Проверка готовности отражает критические зависимости; проверка после релиза отражает реальный пользовательский сценарий.

Первый запуск по шагам

До запуска на сервере должны быть установлены Docker Engine и Compose, создан файл секрета, подготовлен образ API и настроено место для конфигурации. Доступ к управлению Docker нужно выдавать только ответственным за сервер: он позволяет управлять контейнерами и их доступом к ресурсам хоста.

Порядок первого развёртывания такой:

  1. Сохранить compose.prod.yaml и настройки в известном месте на сервере. Убедиться, что путь к секрету существует и файл не попал в репозиторий.
  2. Проверить, что образ API доступен серверу и содержит нужную версию приложения.
  3. Проверить итоговую конфигурацию Compose.
  4. Инициализировать базу и применить миграции схемы способом, предусмотренным проектом.
  5. Запустить сервисы и дождаться проверок готовности.
  6. Выполнить запрос к API с самого сервера и проверить запись и чтение обращения.

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

docker compose -f compose.prod.yaml config -q
docker compose -f compose.prod.yaml pull
docker compose -f compose.prod.yaml up -d --wait --wait-timeout 120
docker compose -f compose.prod.yaml ps
curl -fsS http://127.0.0.1:8000/health/ready

config -q проверяет конфигурацию без вывода её содержимого. Обычный docker compose config разворачивает значения настроек, поэтому его полный вывод не стоит бездумно прикладывать к публичному отчёту. pull получает указанные образы. up --wait ожидает состояния running или healthy согласно настроенным проверкам. После этого curl проверяет ответ API на локальном адресе сервера. Поведение этих команд описано в справке config и справке up.

Отдельный этап миграции нужен потому, что существование PostgreSQL не означает существования таблиц. Если проект использует Alembic, миграцию запускают явной командой с предусмотренными для неё правами. При последующих выпусках изменение схемы должно быть совместимо с версией приложения, которая ещё обслуживает запросы во время развёртывания.

Проверочный запрос после запуска должен быть содержательнее /health/ready: создать тестовое обращение в допустимом окружении, прочитать его и убедиться, что поля и статус сохранились. На тестовом сервере полезно затем перезапустить API и повторить чтение. Это проверяет, что обращение хранится в PostgreSQL, а не только в памяти процесса.

Для диагностики доступны:

docker compose -f compose.prod.yaml ps
docker compose -f compose.prod.yaml logs --tail=100 api
docker compose -f compose.prod.yaml logs --tail=100 db

ps показывает состояние контейнеров и порты, logs - их вывод. Логи нужны для разбора причины, но сами по себе не сообщат команде о ночном сбое. Постоянное наблюдение и оповещения разберём дальше в цикле.

Что сломается в первую очередь

Развёртывание становится понятнее, когда мы можем вывести типичные ошибки из устройства системы.

Симптом Вероятный механизм Что проверить и исправить
API работает внутри контейнера, но curl с сервера не подключается Процесс слушает 127.0.0.1 внутри контейнера либо порт опубликован неверно Проверить команду запуска и публикацию порта. Uvicorn должен слушать 0.0.0.0 внутри контейнера
API сообщает, что PostgreSQL недоступен В настройках указан localhost Внутри сети Compose использовать имя сервиса db и порт PostgreSQL
После нового запуска база кажется пустой Поменялось имя проекта Compose, подключён другой том или выбран неверный путь данных Проверить имя проекта и подключённые тома; не инициализировать новую базу поверх подозреваемой ошибки
Контейнеры запущены, а API отвечает HTTP 500 Контейнер работает, но схема не мигрирована, неверны права или ошибка возникает в конкретном запросе Проверить /health/ready, логи и тестовый сценарий отдельно
API перестал работать после перезапуска PostgreSQL Приложение держит разорванное соединение и не восстанавливает его Настроить повторное подключение и проверить поведение при временной недоступности базы
После пересоздания Redis ответы стали медленнее Кэш опустел Читать исходные данные из PostgreSQL и заполнять кэш повторно
Том существует, но данные потеряны после потери виртуальной машины Том находился на этой машине Восстанавливать базу из проверенной внешней резервной копии

Особенно коварно изменение переменных PostgreSQL после первого запуска. Официальный образ использует POSTGRES_DB, POSTGRES_USER и пароль для инициализации пустого каталога данных. Изменение этих значений в Compose при уже существующем томе не переименует базу и не поменяет существующую роль. Для таких операций нужны команды PostgreSQL и управляемая миграция, а не пересоздание контейнера. Это оговорено в описании образа.

Где заканчиваются возможности одного сервера

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

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

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

Главный результат первого этапа - способность объяснить весь путь конкретного обращения:

какой образ запущен → через какой порт запрос попадает в API → по какому имени API находит PostgreSQL → где лежат данные → какая проверка подтверждает готовность → как команда обнаружит и разберёт сбой

Если на каждый шаг есть проверяемый ответ, приложение действительно перешло с компьютера разработчика на сервер. Следующая задача - безопасно открыть его пользователям через домен, HTTPS и обратный прокси.

Продолжить

  1. 01 FastAPI: как устроен современный Python API Подробное введение в FastAPI: ASGI, Uvicorn, маршруты, параметры, Pydantic, валидация, зависимости, async, ошибки, middleware, фоновые задачи, тестирование и структура проекта.
  2. 02 PostgreSQL: как устроена реляционная база данных Введение в PostgreSQL: сервер и клиент, таблицы, ключи, связи, SQL, ограничения, JOIN, транзакции, индексы, JSONB, роли, резервные копии и подключение из FastAPI.
  3. 03 Docker: как контейнеризировать приложение и управлять его окружением Подробное введение в Docker: образы, контейнеры, Dockerfile, слои, порты, volumes, networks, Compose, health checks, безопасность, отладка и контейнеризация FastAPI.

Обратная связь

Материал был полезен?

Нашли ошибку или неточность? Сообщить →

Начните вводить — найдём материал по всей библиотеке.