SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-13

Django чи Flask на VPS із 1–2 GB RAM

Порівняння Django і Flask на VPS із 1–2 GB RAM: скільки resident memory споживає один worker gunicorn і скільки worker-процесів витримає сервер.

Що коштують Django і Flask на невеликому VPS

Порівняння Django і Flask на невеликому VPS спочатку зводиться до питання використання пам’яті. Django завантажує свій object relational mapper (ORM), механізм міграцій і, якщо його ввімкнути, адміністративну панель у кожен запущений процес worker. Flask завантажує router і об’єкт request. На сервері з 1 GB ця різниця визначає, скільки worker-процесів можна запустити, а їхня кількість — скільки запитів можна обробляти одночасно.

Ці витрати слід враховувати для Django лише в тому разі, якщо ви не плануєте самостійно реалізувати надані ним можливості. Застосунку з обліковими записами користувачів, сесіями та адміністративною панеллю потрібен Django: RAM на кожен worker — це ціна коду, який вам не доведеться писати. Для JSON API перед datastore, який ви вже використовуєте, краще обрати Flask, оскільки жодна з готових можливостей Django вам не знадобиться. Це питання відповідності потребам. Наведені нижче вимірювання покажуть, до якого варіанта належить ваш застосунок.

Скільки пам’яті використовує один worker gunicorn?

ChartMemory per gunicorn worker, minimal app, three workers with preload on
The data behind this chart
[
  {
    "label": "Bare Python 3.12 process",
    "rss_mb": 14,
    "pss_mb": 9
  },
  {
    "label": "Flask, one route",
    "rss_mb": 42,
    "pss_mb": 26
  },
  {
    "label": "Flask + SQLAlchemy",
    "rss_mb": 58,
    "pss_mb": 38
  },
  {
    "label": "Django, admin disabled",
    "rss_mb": 78,
    "pss_mb": 47
  },
  {
    "label": "Django, admin enabled",
    "rss_mb": 96,
    "pss_mb": 58
  }
]

Це типові опубліковані показники для застосунку hello world кожного типу в Ubuntu 24.04 з Python 3.12, трьома worker gunicorn і ввімкненою опцією preload. Вважайте їх мінімальними значеннями, оскільки власні імпорти додають споживання пам’яті. Worker Django з увімкненим admin використовує 96 MB resident-пам’яті, а його пропорційна частка пам’яті становить 58 MB. Різниця між цими двома значеннями є основною темою наступного розділу.

Виконайте такі самі вимірювання на власному сервері.

sudo apt update && sudo apt install -y python3-venv
python3 -m venv /srv/site1/.venv
/srv/site1/.venv/bin/pip install django gunicorn setproctitle

Встановіть setproctitle. Після його встановлення gunicorn перейменовує свої процеси на gunicorn: master [site1] і gunicorn: worker [site1]. Завдяки цьому наступні команди знаходять worker за іменем, а не вгадують потрібні процеси.

pgrep -af gunicorn
ps -o pid,rss,args -p $(pgrep -d, -f 'gunicorn: worker')

Стовпець rss — це resident set size у кілобайтах: усі сторінки пам’яті, які процес наразі утримує в RAM. Якщо підсумувати його значення для всіх worker, результат буде завищеним, оскільки worker, створений через fork, спільно використовує сторінки з батьківським процесом і сусідніми worker. Тому одна й та сама сторінка враховується кілька разів. Натомість запросіть у kernel proportional set size (PSS). Він розподіляє кожну спільну сторінку між процесами, які її відображають.

for pid in $(pgrep -f 'gunicorn: worker'); do awk -v p="$pid" '/^Pss:/ {printf "%s  %d MB\n", p, $2/1024}' /proc/$pid/smaps_rollup; done

Виконайте команду від імені користувача, якому належать worker, або використайте sudo. Для планування бюджету пам’яті слід використовувати PSS, оскільки PSS правильно підсумовується, а RSS — ні.

Django споживає більше пам’яті через те, що робить django.setup(). Він імпортує кожен елемент із INSTALLED_APPS, створює реєстр застосунків і ініціалізує кожен клас моделі разом з окремим об’єктом Python для кожного його поля. Додавання django.contrib.admin запускає автоматичне виявлення admin-компонентів. Воно імпортує модуль admin кожного застосунку та додатково завантажує рівні forms і templates. Worker Flask імпортує Werkzeug і Jinja2 та завершує ініціалізацію.

Є важливе застереження: framework часто становить лише невелику частину споживання пам’яті. Worker, який імпортує cloud SDK або будь-яку numeric-бібліотеку, може використовувати більше пам’яті через них, ніж через Django. Спочатку виміряйте власний застосунок, а вже потім робіть висновок, що проблема у framework.

Copy on write і чому preload змінює показник

Master-процес Gunicorn створює worker-процеси через fork. Одразу після fork() дочірній процес спільно використовує всі сторінки пам’яті з батьківським процесом, а ядро копіює сторінку лише тоді, коли одна зі сторін записує в неї. Тому те, чи існує реєстр моделей Django на сервері в одному екземплярі чи в чотирьох, залежить від того, у якому процесі його було створено — до fork чи після нього.

Якщо preload_app вимкнено, кожен worker імпортує застосунок після fork, тому створює власну приватну копію. Якщо його ввімкнено, master імпортує застосунок один раз, а worker-процеси успадковують ці сторінки пам’яті.

import gc

bind = "unix:/run/site1/gunicorn.sock"
umask = 0o007
workers = 3
timeout = 30
preload_app = True
max_requests = 500
max_requests_jitter = 50

def when_ready(server):
    gc.freeze()

CPython працює не на користь copy on write. Заголовок кожного об’єкта містить лічильник посилань, і звернення до об’єкта змінює цей заголовок, тому спільні сторінки поступово копіюються, коли garbage collector обходить heap. gc.freeze() переміщує все вже виділене в постійне покоління, яке collector більше не перевіряє. Це дає змогу зберегти спільними більше таких сторінок. when_ready є правильним hook, оскільки він виконується після preload і до створення першого worker через fork. Виміряйте PSS до та після його додавання, оскільки обсяг заощадженої пам’яті залежить від того, яка частина стану застосунку створюється під час імпорту.

Preload має одну особливість, яка часто стає несподіванкою під час розгортання. systemctl reload надсилає HUP, а задокументована поведінка gunicorn у разі HUP полягає в перезавантаженні конфігурації та запуску нових worker-процесів. Якщо застосунок було попередньо завантажено, gunicorn не імпортує ваш код повторно. Тому новий реліз не запущено, навіть якщо worker-процеси є новими. Після зміни коду використовуйте systemctl restart. Якщо потрібно спочатку дочекатися завершення старих worker-процесів, виконайте послідовність USR2, потім WINCH.

Скільки worker-процесів насправді може працювати на VPS із 1 GB RAM?

ChartWhere a 1 GB VPS goes, typical idle figures before any traffic
The data behind this chart
[
  {
    "label": "Ubuntu 24.04 base",
    "ram_mb": 190
  },
  {
    "label": "nginx",
    "ram_mb": 12
  },
  {
    "label": "PostgreSQL, default config",
    "ram_mb": 120
  },
  {
    "label": "Headroom you must leave",
    "ram_mb": 150
  },
  {
    "label": "Left for gunicorn workers",
    "ram_mb": 550
  }
]

Це показники простою на сервері, який нічого не обслуговує. Для worker-процесів застосунку залишається приблизно 550 MB, і це ще до надходження першого запиту.

Тепер діліть цей обсяг і використовуйте песимістичну оцінку. Під час обробки запиту потрібна додаткова пам’ять: queryset може завантажити кілька тисяч рядків, після чого виконується рендеринг шаблону. Пікове споживання пам’яті worker-процесом часто майже вдвічі перевищує показник у простої, тому закладайте подвійний обсяг. Django з admin за значення 58 MB у простої дає змогу запустити на цьому сервері чотири worker-процеси. Flask із SQLAlchemy за значення 38 MB дає змогу запустити сім.

Рекомендація Gunicorn (2 x cores) + 1 передбачає, що дефіцитним ресурсом є CPU, а RAM достатньо. На невеликому VPS ситуація протилежна. Один shared vCPU також дає менше роботи, ніж повне ядро, коли хост перевантажений. Це важливо врахувати, перш ніж звинувачувати свій код: час steal CPU від перевантаженого сусіднього VPS відображається в top як показник st.

Якщо ваші view переважно очікують на базу даних або upstream API, тут ефективніші threads, а не процеси. --worker-class gthread --workers 2 --threads 4 дає змогу обробляти вісім одночасних запитів із витратами пам’яті лише двох worker-процесів, оскільки threads спільно використовують одну завантажену копію інтерпретатора та фреймворку. Global interpreter lock означає, що threads не допомагають view, який інтенсивно використовує CPU.

Додайте swap. VPS із 1 GB RAM без swap перетворює стрибок споживання пам’яті на завершення процесу, тоді як swapfile перетворює такий самий стрибок на повільний запит.

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system

Потім обмежте сам застосунок. MemoryMax=600M у unit Gunicorn означає, що kernel забере пам’ять у cgroup вашого застосунку, а не вибиратиме процес для завершення на всьому сервері. Тому неконтрольований запит не позбавить вас SSH-сеансу.

Холодний запуск і поведінка під час перезапуску

ChartImport to ready, minimal app, one shared vCPU
The data behind this chart
[
  {
    "label": "Flask, one route",
    "cold_start_ms": 90
  },
  {
    "label": "Flask + SQLAlchemy",
    "cold_start_ms": 260
  },
  {
    "label": "Django, admin disabled",
    "cold_start_ms": 480
  },
  {
    "label": "Django, admin enabled",
    "cold_start_ms": 720
  }
]

Витрати на запуск виникають двічі: під час кожного розгортання та під час кожного автоматичного перезапуску після збою. Мінімальний застосунок Flask готовий приблизно за 90 мс, а Django з увімкненою адміністративною панеллю запускається приблизно за 720 мс на тому самому shared vCPU. Обидва значення є типовими опублікованими показниками. Виміряйте власні значення, оскільки саме залежності найбільше впливають на результат.

cd /srv/site1
DJANGO_SETTINGS_MODULE=site1.settings /srv/site1/.venv/bin/python -X importtime -c "import django; django.setup()" 2>&1 | tail -20

Останні рядки містять список найповільніших імпортів із сумарним часом у мікросекундах. Для Flask виконайте той самий прапорець для свого модуля: python -X importtime -c "import app".

Якщо preload увімкнено, master сплачує цю вартість один раз, а кожен forked worker запускається миттєво. Якщо preload вимкнено, кожен worker сплачує цю вартість окремо, а timeout у gunicorn охоплює як запуск, так і обробку запитів. Worker, який не повідомив про готовність протягом timeout секунд, завершується та замінюється, тому важкий застосунок на повільному shared vCPU може застрягти в циклі перезапусків і не обробити жодного запиту. У журналі буде:

[2026-08-09 09:14:02 +0000] [981] [CRITICAL] WORKER TIMEOUT (pid:1004)

Міграції мають виконуватися в unit, а не в коді запуску застосунку. ExecStartPre виконується один раз до запуску будь-якого worker. Якщо розмістити migrate усередині застосунку, три worker одночасно змагатимуться за той самий schema lock.

Форма розгортання майже однакова

Менеджер процесів

Обидва фреймворки працюють під керуванням gunicorn, а gunicorn працює під керуванням systemd.

[Unit]
Description=gunicorn for site1
After=network.target

[Service]
User=site1
Group=www-data
WorkingDirectory=/srv/site1
RuntimeDirectory=site1
Environment="PATH=/srv/site1/.venv/bin"
ExecStartPre=/srv/site1/.venv/bin/python manage.py migrate --noinput
ExecStart=/srv/site1/.venv/bin/gunicorn -c /srv/site1/gunicorn.conf.py site1.wsgi:application
Restart=on-failure
RestartSec=3
MemoryMax=600M

[Install]
WantedBy=multi-user.target

RuntimeDirectory=site1 створює /run/site1 під час запуску та видаляє його під час зупинки, тому шлях до сокета завжди існує та має правильного власника. Рядок umask = 0o007 у конфігурації gunicorn робить цей сокет доступним для запису групі www-data. Завдяки цьому nginx може звертатися до нього.

Unit для Flask — це той самий файл зі зміненим одним рядком: ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app, без ExecStartPre. Аргумент app:app має формат «модуль, потім callable», тому помилка Failed to find attribute 'app' in 'app'. означає, що у вашому модулі не визначено змінну з такою назвою. Заплановані завдання працюють за тією самою схемою. Таймер systemd замінює cron для команди керування Django без додавання черги завдань на сервер такого розміру.

Статичні файли

Django з DEBUG = False взагалі не обслуговує статичні файли. Встановіть STATIC_ROOT, виконайте python manage.py collectstatic і вкажіть вебсерверу каталог із результатом. Якщо пропустити цей крок, панель адміністратора завантажиться без стилів, а журнал заповниться повідомленнями Not Found: /static/admin/css/base.css.

Є два прийнятні способи обслуговувати ці файли. Блок nginx alias не створює навантаження на застосунок. WhiteNoise, доданий як middleware, обслуговує файли з worker-процесу та усуває потребу в блоці nginx, але споживає трохи процесорного часу worker-процесу для кожного файла. Flask обслуговує власний каталог static/ у середовищі розробки. У production проксі потрібно вказати на цей каталог з тієї самої причини.

Reverse proxy

server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /srv/site1/static/;
        expires 30d;
    }

    location / {
        proxy_pass http://unix:/run/site1/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

За будь-яким проксі Django потрібно повідомити, що початковий запит було виконано через HTTPS. Інакше перевірки cross-site request forgery (CSRF) відхилятимуть ваші власні форми.

ALLOWED_HOSTS = ["example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Якщо на сервері вже працюють контейнери, Traefik перед кількома застосунками Docker Compose виконує те саме завдання за допомогою labels контейнера, а не окремого файла для кожного сайту.

Яку базу даних обрати

SQLite цілком підходить для одного сервера застосунків із частотою запису кілька операцій за секунду. Вона також усуває потребу в окремому daemon у межах доступної пам’яті. Увімкніть write-ahead logging (WAL) і задайте для драйвера busy timeout.

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
        "OPTIONS": {
            "timeout": 20,
            "init_command": "PRAGMA journal_mode=WAL;",
        },
    }
}

Без цих двох параметрів ви зіткнетеся з django.db.utils.OperationalError: database is locked, щойно два worker-процеси одночасно виконуватимуть запис. Стандартний режим журналювання блокує читачів під час запису, а стандартний timeout завершується майже одразу. Докладніше, зокрема про те, коли SQLite перестає бути правильним вибором, описано в матеріалі Запуск SQLite у production на VPS.

PostgreSQL на тому самому сервері з 1 GB пам’яті займає 120 MB із наведеного вище бюджету, а також запускає по одному backend-процесу для кожного постійного підключення. CONN_MAX_AGE у Django підтримує відкрите підключення для кожного worker-процесу, тому чотири worker-процеси означають чотири backend-процеси. Зазвичай це виправданий компроміс. Проте порахуйте їх перед встановленням кількості worker-процесів. Якщо ви хочете тримати базу даних у контейнері поруч із застосунком, Запуск Docker на VPS передбачає той самий компроміс, але з більшими мінімальними витратами ресурсів, оскільки daemon і кожен контейнер додають накладні витрати, важливі для сервера такого розміру.

Що дають додаткові компоненти і скільки вони коштують

Додаткові мегабайти Django — це набір компонентів, які вже існують і узгоджено працюють разом: ORM із міграціями, система сесій та автентифікації, модель дозволів, рівень форм із захистом CSRF, рушій шаблонів, команди керування та адміністративна панель. Адміністративну панель часто недооцінюють. Це готовий редактор даних у базі для ваших моделей із пошуком і фільтрами, який налаштовується одним рядком у INSTALLED_APPS.

Flask працює за протилежним принципом. Ви отримуєте маршрутизацію, об’єкт запиту, шаблони Jinja2 і об’єкт конфігурації. Усе інше ви обираєте самостійно. Для невеликого застосунку це реальна перевага, оскільки ORM не завантажується, якщо ви її не імпортуєте.

Проблеми починаються в проміжному варіанті. Додайте SQLAlchemy для моделей, Alembic для міграцій, Flask-Login для сесій, Flask-WTF для форм і CSRF та розширення адміністративної панелі — і ви отримаєте систему з профілем використання пам’яті як у Django, але без його узгодженості. Кожен компонент має власний цикл випуску та власне бачення того, як слід підключати застосунок. Саме на цьому етапі Django стає дешевшим рішенням — і за обсягом RAM, і за кількістю годин, які ви витрачаєте на оновлення.

Django проти Flask: правило вибору

Використовуйте Django, якщо застосунок має облікові записи, редагований контент, схему, яка постійно змінюватиметься, і back office, яким справді користуватимуться. Використовуйте Flask, якщо застосунок є JSON-інтерфейсом до наявного сховища даних або приймачем webhook без HTML.

Вирішальним критерієм є письмовий список. Запишіть усі пакети, які ви встановили б у Flask, щоб отримати потрібний набір функцій. Якщо в цьому списку є ORM і інструмент міграцій, ви вже обрали Django, але витрачаєте додаткові ресурси, щоб повільно дійти до того самого результату.

Є один випадок, коли Flask справді має перевагу на слабкому обладнанні: кілька невеликих сервісів на одному сервері. Кожен сервіс Flask працює як окремий легкий процес у власному unit. Три сайти Django на одному VPS із 1 GB RAM означають, що одночасно в пам’яті перебувають три копії фреймворку, і наведений вище розрахунок більше не працює. Якщо результат постійно не відповідає потребам, чесним рішенням часто є більший тарифний план, а скільки насправді коштує VPS на місяць — коротша розмова, ніж переписування робочого застосунку.

Типові збої та повідомлення, які ви побачите

Воркери зникають і знову запускаються. Gunicorn виводить [ERROR] Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Це повідомлення з’являється і тоді, коли Gunicorn завершує воркер, який пропустив heartbeat тайм-ауту, і тоді, коли процес завершує kernel. Розрізнити ці випадки можна за допомогою dmesg -T | grep -i "killed process". Рядок у цьому виводі означає проблеми з пам’яттю, тому зменште кількість воркерів або додайте swap.

Кожна сторінка повертає 400, а в журналі зазначено Invalid HTTP_HOST header. Повне повідомлення має вигляд Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.. Django відхиляє запит до того, як він досягає вашого коду, оскільки ALLOWED_HOSTS порожній або не містить імені, яке проксі передав у Host.

Форми не працюють і з’являється Origin checking failed. На сторінці зазначено, що перевірка CSRF не пройдена. Це трапляється за використання проксі з TLS termination: застосунок бачить звичайний HTTP, формує origin http:// і порівнює його із запитом, який надійшов через https://. Налаштуйте SECURE_PROXY_SSL_HEADER і CSRF_TRUSTED_ORIGINS та перевірте, що проксі справді передає X-Forwarded-Proto.

nginx одразу повертає 502. У журналі помилок зазначено причину: connect() to unix:/run/site1/gunicorn.sock failed (2: No such file or directory) означає, що unit не запущений, а (13: Permission denied) — що сокет існує, але nginx не може відкрити його через неправильні налаштування umask і групи.

В адміністративній панелі немає стилів. collectstatic не виконано або шлях alias не відповідає STATIC_ROOT. У журналі доступу видно відповіді 404 для шляхів під /static/admin/.

Запис не працює за невисокого навантаження. database is locked у SQLite означає, що WAL вимкнено або тайм-аут очікування звільнення ресурсу надто короткий для двох воркерів, які одночасно виконують запис.

FAQ

Чи не надто важкий Django для VPS із 1 GB RAM?

Ні. Django з невеликою кількістю worker-процесів, nginx попереду та SQLite як базою даних працює без проблем на VPS із 1 GB RAM. Запас пам’яті швидко зменшується, якщо додати PostgreSQL із налаштуваннями за замовчуванням, кеш, фоновий worker і Docker на той самий сервер. Виміряйте proportional set size одного worker-процесу, подвойте це значення для врахування пікових навантажень і порівняйте результат із пам’яттю, що залишилася після операційної системи та бази даних.

Скільки gunicorn worker-процесів запускати на одному vCPU?

Почніть із трьох і виміряйте споживання ресурсів. На невеликому VPS обмеженням зазвичай є пам’ять, тому поділіть RAM, що залишилася після операційної системи та бази даних, на подвоєний proportional set size одного worker-процесу. Якщо ваші view-функції переважно очікують на базу даних або upstream API, використовуйте worker class gthread із невеликою кількістю worker-процесів і кількома потоками в кожному. Потоки спільно використовують одну завантажену копію framework і споживають значно менше пам’яті, ніж додаткові процеси.

Чи потрібен PostgreSQL, чи достатньо SQLite?

SQLite підходить для одного сервера застосунку з помірною інтенсивністю запису. Він також усуває окремий daemon зі складу споживачів пам’яті. Увімкніть write-ahead logging і встановіть busy timeout, інакше одночасні записи завершуватимуться помилкою database is locked. Перейдіть на PostgreSQL, коли записувати дані має більше однієї машини або коли потрібні можливості, яких немає в SQLite, наприклад підтримка великої кількості одночасних записів чи керування доступом за ролями.

Чи варто використовувати uvicorn замість gunicorn?

Лише якщо у вас є async view-функції та реальна операція, на яку потрібно очікувати. Flask — це WSGI-застосунок, тому async view-функція запускається в новому event loop усередині worker-потоку та завершується до початку обробки наступного запиту. Додаткової конкурентності це не дає. Для роботи async view-функцій Django потрібен ASGI-сервер. У нових releases uvicorn його gunicorn worker class винесено в окремий пакет. Тому користуйтеся актуальною документацією uvicorn, а не копіюйте застарілий прапорець worker class зі старого tutorial.

Чому мій worker зник без traceback?

Процес, завершений kernel out-of-memory killer, отримує SIGKILL і не може нічого записати в журнал перед завершенням. Тому журнал застосунку просто обривається. Gunicorn помічає цю паузу та виводить Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Підтвердьте це за допомогою dmesg -T | grep -i "killed process". Рішення — зменшити кількість worker-процесів або створити swapfile, щоб стрибок споживання пам’яті призводив до повільного запиту, а не до завершення процесу.

#django#flask#python#gunicorn#deployment#vps