Бонус. Linux для розробника¶
До цього моменту news_hub і проєкт нотаток жили на твоєму комп'ютері: uvicorn --reload, python manage.py runserver. Модуль 5 переносить їх туди, де працює майже кожен вебзастосунок, — на Linux: у контейнер Docker (урок 49), на сервер з Compose (49), у CI на GitHub Actions (50). Усі три — це Linux, і всі їхні «загадкові» поломки — звичайні речі Linux: процес не отримав сигнал, користувач не має прав на папку, сервер слухає не ту адресу, змінну середовища не передали.
Цей бонус-урок (поза нумерацією 1–52) — мінімум Linux, потрібний для уроків 49–51. Кожна тема тут — з того, на що ми справді наткнулися, переносячи news_hub у Docker.
| Крок | Матеріал | Що вчимо |
|---|---|---|
| 1 | Відкрити вправи в Colab Переглянути розв’язки | 9 вправ у справжньому Linux (Colab — це Ubuntu): пайплайни, коди виходу, права, процеси й сигнали, порти, змінні середовища, скрипт з set -euo pipefail |
| 2 | ця сторінка | ментальна модель і все, що знадобиться в уроках 49–51 |
| 3 | довідник у 18 розділах | від «навіщо Linux» до Kubernetes: термінал, файли, права, процеси, пакети, SSH, секрети, bash, Makefile, деплой Django, nginx, журнали, Docker, Compose, DevOps |
Довідник перевірено: команди запущено в контейнерах Ubuntu, Debian і python:3.12-slim, конфігурації nginx і Compose — перевірені інструментами.
Що потрібно з попередніх уроків: запуск Python-скриптів і pip (М1), subprocess і процеси (урок 28), HTTP і порти (31), змінні середовища й секрети (43, 46).
Після уроку ти зможеш:
- зібрати з маленьких команд пайплайн, що відповідає на питання про журнал;
- читати коди виходу й писати скрипти, які падають вчасно;
- пояснити права
644,600, власника файлу і навіщо застосунку окремий користувач; - знайти процес, надіслати йому сигнал і написати коректну зупинку;
- пояснити, чому сервер у контейнері слухає
0.0.0.0; - передати процесу змінну середовища — і не зіпсувати значення з
$.
Linux за хвилину¶
flowchart TD
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
K["ядро Linux<br>процеси, пам'ять, файли, мережа"]
P["процеси<br>PID, користувач, змінні середовища"]
F["файли<br>власник і права rwx"]
N["мережа<br>адреса + порт"]
S["shell (bash)<br>запускає процеси, з'єднує їх |"]
C["контейнер Docker<br>процеси з власним PID 1, мережею й файлами"]
K --> P
K --> F
K --> N
S --> P
P --> C
F --> C
N --> C
class K decision
class P,F,N,S step
class C success
Контейнер Docker — не віртуальна машина. Це звичайні процеси Linux, яким ядро показує окрему файлову систему, окрему мережу й окрему нумерацію процесів. Тому все нижче працює і на сервері, і в контейнері, і в CI:
$ docker top api
UID PID PPID C STIME TTY TIME CMD
10001 3068 3043 41 08:50 ? 00:00:10 /usr/local/bin/python3.12 /usr/local/bin/uvicorn news_hub.api:app --host 0.0.0.0 --port 8000
На сервері це PID 3068 користувача 10001, а всередині контейнера той самий uvicorn — PID 1 користувача app.
Пайплайни й коди виходу¶
Команда Linux робить одну річ і пише результат у stdout, помилки — у stderr. | передає stdout однієї команди на stdin наступної. Хто найчастіше звертається до сервера (журнал nginx, урок 50):
$ cut -d' ' -f1 access.log | sort | uniq -c | sort -rn | head -3
35 10.0.0.5
20 172.18.0.4
9 192.168.1.20
Кожна програма завершується кодом виходу: 0 — успіх, інше — помилка. На ньому тримається автоматизація: a && b запускає b, лише якщо a вдалась; CI позначає крок червоним, якщо код не 0 (урок 51). Код пайплайна — код його останньої команди:
Скрипт, що працює без людини (деплой, бекап, CI), починають рядком:
set -euo pipefail # -e: зупинитись на помилці; -u: незадана змінна — помилка; pipefail: пайплайн падає, якщо впала будь-яка команда
Детальніше — термінал і shell, bash-скрипти.
Користувачі й права¶
644 = rw- власнику, r-- групі, r-- решті (r=4, w=2, x=1). Хто що може:
Дія від імені student |
Результат | Чому |
|---|---|---|
cat app/api.py |
так | r для всіх |
echo x >> app/api.py |
ні | писати може лише власник (root) |
touch data/news_hub.db |
так | власник папки data — student |
cat .env |
ні | 600: лише власник |
Саме так побудовано образ уроку 49: застосунок працює від користувача app (UID 10001); код належить root і доступний лише для читання — зламаний процес не перепише власний код; у /data (база SQLite) писати можна. root у контейнері — це той самий root ядра, тому застосунок від root — зайвий ризик.
Детальніше — права доступу й користувачі.
Процеси й сигнали¶
Процес зупиняють сигналом:
| Сигнал | Номер | Хто надсилає | Процес може |
|---|---|---|---|
SIGINT |
2 | Ctrl+C | перехопити й завершитись акуратно |
SIGTERM |
15 | kill PID, systemctl stop, docker stop |
перехопити й завершитись акуратно |
SIGKILL |
9 | kill -9, docker stop через 10 с |
нічого: ядро знищує процес |
Процес, убитий сигналом N, має код виходу 128 + N: 143 після SIGTERM, 137 після SIGKILL. Акуратна зупинка вебсервера — дочекатися поточних запитів, закрити з'єднання з базою й Redis. У FastAPI це частина lifespan після yield, у bash — trap:
Сигнал отримує лише той процес, якому його надіслали. Якщо сервер запущено через оболонку (sh -c "uvicorn …"), SIGTERM дістанеться оболонці, а не серверу:
flowchart TD
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
subgraph A["напряму: python server.py"]
direction LR
A1["SIGTERM → PID сервера"] --> A2["обробник: закрити з'єднання"] --> A3["exit 0"]
end
subgraph B["через sh -c 'python server.py'"]
direction LR
B1["SIGTERM → PID оболонки"] --> B2["sh завершилась,<br>сервер не дізнався"] --> B3["сервер живе далі;<br>у Docker — SIGKILL, 137"]
end
subgraph C["sh -c 'exec python server.py'"]
direction LR
C1["exec: сервер замінив<br>оболонку, той самий PID"] --> C2["SIGTERM → сервер"] --> C3["exit 0"]
end
A ~~~ B ~~~ C
class A1,B1,C1 step
class A2,C2 decision
class A3,C3 success
class B2 warning
class B3 error
Справжній запуск з ноутбука:
напряму (exec-форма CMD) {'сервер = перший процес': True, 'журнал': "SIGTERM: закриваю з'єднання", 'сервер пережив SIGTERM': False}
sh -c (shell-форма CMD) {'сервер = перший процес': False, 'журнал': '', 'сервер пережив SIGTERM': True}
sh -c exec … {'сервер = перший процес': True, 'журнал': "SIGTERM: закриваю з'єднання", 'сервер пережив SIGTERM': False}
У контейнері docker stop надсилає SIGTERM процесу з PID 1. Тому в Dockerfile пишуть exec-форму CMD ["uvicorn", …]: з shell-формою news_hub зупинявся 10.2 с з кодом 137, з exec-формою — 1.8 с з кодом 0 (урок 49).
Детальніше — процеси, порти, сервіси.
Порти: 127.0.0.1 чи 0.0.0.0¶
Сервер слухає адресу й порт. 127.0.0.1 (loopback) приймає з'єднання лише з тієї ж машини, 0.0.0.0 — з усіх мережевих інтерфейсів:
$ python3 -m http.server 8765 --bind 127.0.0.1 & python3 -m http.server 8766 --bind 0.0.0.0 &
порт 8765, з'єднання на 127.0.0.1 → True
порт 8765, з'єднання на 192.0.2.2 → False
порт 8766, з'єднання на 127.0.0.1 → True
порт 8766, з'єднання на 192.0.2.2 → True
192.0.2.2 — адреса машини в мережі (hostname -I).
У контейнера власна мережа: «та сама машина» для нього — він сам. Звідси два правила уроку 49:
- сервер у контейнері слухає
0.0.0.0, інакше-p 8000:8000не допоможе; localhostуREDIS_URLусередині контейнера — сам контейнер; інші контейнери — за іменем у спільній мережі (redis://redis:6379).
На сервері навпаки: PostgreSQL і Redis не відкривають назовні — лише в мережі Compose (урок 50), а назовні слухає тільки nginx на 80/443.
Детальніше — процеси, порти, сервіси, nginx, gunicorn, uvicorn.
Змінні середовища¶
Процес отримує змінні середовища від батька. Змінна shell без export дочірнім процесам не передається, а NAME=value команда задає змінну лише цій команді (у виводі — початок кожного рядка скрипта):
Так news_hub отримує DATABASE_URL, REDIS_URL і секрети — і так їх передає Docker (-e, --env-file) та Compose (environment, env_file). Пастка — $ у значенні. bcrypt-хеш пароля адміна (урок 47) — $2b$12$…, а в bash $2 у подвійних лапках — «другий аргумент»:
Одинарні лапки — «усе буквально». Але кожен інструмент читає .env по-своєму: source у bash знімає лапки й підставляє змінні, Compose знімає лапки, а docker run --env-file бере значення разом з лапками (урок 49).
Детальніше — змінні середовища й секрети.
Знайди помилку¶
Нічний бекап бази: pg_dump → gzip → файл. Пароль до бази змінився, скрипт відпрацював з кодом 0, файл з'явився. Скрипт (pg_dump замінено функцією, що падає, як з неправильним паролем):
#!/usr/bin/env bash
set -e
pg_dump() { echo "pg_dump: error: connection failed: password authentication failed" >&2; return 1; }
pg_dump news_hub | gzip > backup.sql.gz
echo "бекап готовий: $(stat -c %s backup.sql.gz) байт"
Відповідь
Код пайплайна — код останньої команди. gzip успішно стиснув порожній вхід (20 байт — лише заголовок gzip), тож set -e помилки не побачив. set -eo pipefail — і скрипт завершується з кодом 1, не друкуючи «бекап готовий». Такий самий бекап PostgreSQL з контейнера буде в уроці 50 — з set -euo pipefail.
Як обрати: systemd чи Docker¶
Довідник показує два способи тримати застосунок запущеним на сервері: сервіс systemd (деплой Django на Linux) і контейнер (уроки 49–50).
flowchart TD
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
Q0["застосунок на сервері"]
Q1{"кілька сервісів:<br>база, Redis, nginx, бот?"}
Q2{"однакове середовище<br>локально, у CI і на сервері?"}
Q3{"один процес Python,<br>решта вже встановлена?"}
D["Docker + Compose<br>уроки 49–50"]
S["systemd-сервіс + venv<br>довідник, розділ 11"]
Q0 --> Q1
Q1 -->|так| D
Q1 -->|ні| Q2
Q2 -->|так| D
Q2 -->|ні| Q3
Q3 -->|так| S
Q3 -->|ні| D
class Q0 step
class Q1,Q2,Q3 decision
class D,S success
Обидва спираються на те саме: процес від окремого користувача, зупинка сигналом SIGTERM, налаштування — у змінних середовища, журнали — у stdout (journalctl чи docker logs). У курсі — Docker: news_hub і проєкт нотаток мають по кілька сервісів, і CI (урок 51) збирає той самий образ, що й сервер.
Довідник¶
| № | Розділ | Коли знадобиться |
|---|---|---|
| 1 | Ментальна модель Linux | навіщо Linux веброзробнику, що таке сервер |
| 2 | Термінал і shell | stdin/stdout/stderr, коди виходу, пайплайни |
| 3 | Файлова система | /etc, /var/log, навігація, пошук файлів |
| 4 | Права доступу й користувачі | chmod, chown, sudo; користувач app в образі (урок 49) |
| 5 | Процеси, порти, сервіси | ps, kill, systemd, хто слухає порт |
| 6 | Пакетні менеджери | apt проти pip, venv |
| 7 | SSH і сервер | ключі, підключення, scp, rsync — деплой (урок 50) |
| 8 | Змінні середовища й секрети | .env, що не можна комітити |
| 9 | Bash-скрипти | set -euo pipefail, аргументи, функції |
| 10 | Makefile | короткі команди для проєкту |
| 11 | Деплой Django на Linux | без Docker: venv, gunicorn, systemd, nginx |
| 12 | Nginx, Gunicorn, Uvicorn | reverse proxy, WSGI і ASGI (урок 50) |
| 13 | Журнали й дебаг | journalctl, tail -f, «сайт не відкривається» |
| 14 | Docker basics | образ, контейнер, порти — перед уроком 49 |
| 15 | Docker Compose | кілька сервісів однією командою — перед уроком 50 |
| 16 | DevOps workflow | CI/CD, staging, production — перед уроком 51 |
| 17 | Kubernetes: огляд | що далі після Compose |
| 18 | Roadmap | куди рухатись далі |
Підсумок¶
| Поняття | Що запам'ятати |
|---|---|
| пайплайн | a \| b: stdout → stdin; код пайплайна — код останньої команди, якщо немає pipefail |
| код виходу | 0 — успіх; 128 + N — убитий сигналом N (143, 137) |
| права | 644 код, 600 секрети; застосунок — не від root |
| сигнали | SIGTERM можна обробити (lifespan, trap), SIGKILL — ні; сигнал отримує лише адресат |
| порти | 127.0.0.1 — лише ця машина (у контейнері — сам контейнер), 0.0.0.0 — усі інтерфейси |
| змінні середовища | передаються дочірнім процесам після export; $ у значенні — в одинарних лапках |
Самоперевірка¶
- Чому
false | trueзавершується з кодом 0 і що з цим робитьpipefail? - Що означає
600для.envі хто його прочитає? - Процес завершився з кодом 137. Що сталось?
- Чому
python -m http.server --bind 127.0.0.1у контейнері недоступний з-p 8000:8000? - Чому
ADMIN_PASSWORD_HASH="$2b$12$…"у bash псує хеш?
Відповіді
- Код пайплайна — код останньої команди (
true→ 0). Зpipefail— код останньої з тих, що впали (тут 1). - Читати й писати може лише власник файлу (і root); група й решта — нічого.
- 128 + 9: процес убито
SIGKILL— наприклад,docker stop, коли процес не завершився за 10 с післяSIGTERM, або ядро через брак пам'яті. 127.0.0.1усередині контейнера — сам контейнер; з'єднання через проброшений порт приходять на інший інтерфейс контейнера.- У подвійних лапках bash підставляє змінні:
$2і$1— аргументи скрипта (порожні),$eBD…— незадана змінна. Лишаєтьсяb2.
Документація і джерела¶
- Довідник — 18 розділів + зміст (Linux для розробника).
- Bash: GNU Bash manual — The Set Builtin (
-e,-u,pipefail), Quoting, Exit Status. - Сигнали:
man 7 signal(man7.org); права:man 1 chmod; порти:man 8 ss. - Docker: docker stop (SIGTERM, потім SIGKILL).