Локально PostgreSQL проще всего запустить в Docker: команда docker run с паролем суперпользователя, портом и именованным томом поднимает сервер, а консольный клиент psql уже есть внутри контейнера. Дальше создают отдельного владельца базы для приложения, описывают таблицы с подходящими типами (bigint, text, numeric для денег, timestamptz для моментов времени) и ограничениями NOT NULL, CHECK, UNIQUE, DEFAULT, а резервную копию делают через pg_dump и обязательно проверяют восстановлением. Актуальная стабильная версия — PostgreSQL 18; урок проверен на 17.11, и всё в нём так же работает в 18, кроме пути, куда монтируется том.
После материала вы сможете
- запустить PostgreSQL в Docker с именованным томом и убедиться, что данные переживают пересоздание контейнера
- работать в psql: подключаться, смотреть базы, таблицы и роли командами
\l,\dt,\d,\du - создать базу с отдельным владельцем и пользователем только для чтения и описать таблицы с типами и ограничениями
NOT NULL,CHECK,UNIQUE,DEFAULT - сделать резервную копию через
pg_dump, восстановить её черезpg_restoreи подключиться к базе из приложения по строке подключения
Проверено на
PostgreSQL 17.11 (postgres:17-alpine)PostgreSQL 18.6 — сверкаDockerpsql 17Node.js 24.18 + pg 8.23.1
Сверено с документацией: 11 октября 2026 г.
Содержание
- Какую версию взять: PostgreSQL 17 или 18
- Запуск в Docker: пароль, порт и том для данных
- psql: подключение и мета-команды
- База и пользователь с минимальными правами
- Типы данных: что выбрать для каждого поля
- Первая таблица: ограничения и изменение данных
- Резервная копия и подключение из приложения
- Частые ошибки и упражнения
Какую версию взять: PostgreSQL 17 или 18
PostgreSQL — реляционная СУБД с открытым исходным кодом: сервер принимает SQL-запросы, хранит данные на диске и сам следит за правилами целостности, которые вы описали в схеме. В уроке «SQL с нуля» вы уже читали данные из готовой учебной базы магазина кофе. Здесь мы поднимем такой сервер сами, создадим ту же базу — таблицы clients, products, orders, order_items — с нуля и научимся её беречь.
По политике версий PostgreSQL новая основная (major) версия выходит примерно раз в год и поддерживается пять лет; минорные выпуски с исправлениями выходят минимум раз в квартал. Состояние на дату проверки урока:
18 (18.6)
- Статус
- Актуальная стабильная, вышла 25.09.2025, поддержка до 14.11.2030
- Что важно для этого урока
- В Docker-образе том монтируется в
/var/lib/postgresql; есть функцияuuidv7();\dtпечатает заголовок «List of tables»
17 (17.11)
- Статус
- Поддерживается до 08.11.2029
- Что важно для этого урока
- Том монтируется в
/var/lib/postgresql/data; UUID генерируетgen_random_uuid()(версия 4)
19
- Статус
- Бета-версия (Beta 4 от 24.09.2026)
- Что важно для этого урока
- Только для пробы, не для работы
Урок написан на образе postgres:17-alpine: на нём идут все уроки трека SQL, и команды с выводом совпадают от урока к уроку. Мы прогнали каждый SQL-файл и каждую демонстрацию из урока и на PostgreSQL 18.6 — результаты те же, отличаются только путь тома и заголовок \dt. Для нового проекта берите 18: её поддерживают на год дольше. Дамп учебной базы, снятый в 17, восстановился в 18.6 без ошибок — так и выглядит переход между основными версиями.
Запуск в Docker: пароль, порт и том для данных
Официальный образ postgres содержит сервер и клиентские утилиты: psql, pg_dump, pg_restore. Единственная обязательная переменная — POSTGRES_PASSWORD, пароль суперпользователя postgres. Данные храним в именованном томе (volume) — области диска, которой управляет Docker и которая живёт отдельно от контейнера.
docker volume create shop-pgdata
docker run -d --rm --name shop-db \
-e POSTGRES_PASSWORD=change-me-locally \
-p 127.0.0.1:5432:5432 \
-v shop-pgdata:/var/lib/postgresql/data \
postgres:17-alpine
docker logs shop-db-d— запустить в фоне;--rm— удалить контейнер после остановки: он одноразовый, всё ценное лежит в томе--name shop-db— имя, по которому мы будем обращаться к контейнеру-p 127.0.0.1:5432:5432— открыть порт только на вашем компьютере; без127.0.0.1база видна всей локальной сети-v shop-pgdata:/var/lib/postgresql/data— подключить том туда, где PostgreSQL 17 хранит данныеpostgres:17-alpine— тег с закреплённой основной версией: минорные обновления придут, а 18 не подменит 17 внезапно
$ docker logs shop-db 2>&1 | tail -1
2026-10-12 01:02:45.235 UTC [1] LOG: database system is ready to accept connections$ docker run -d --rm --name pg-wrong -e POSTGRES_PASSWORD=change-me-locally -v pg-wrong:/var/lib/postgresql postgres:17-alpine
$ docker exec pg-wrong psql -U postgres -c "CREATE DATABASE shop"
CREATE DATABASE
$ docker inspect pg-wrong --format '{{range .Mounts}}{{.Name}} -> {{.Destination}}{{println}}{{end}}'
pg-wrong -> /var/lib/postgresql
6ad9b1ca6430d46b5b43f74b3cc4757853465c4e675bff418b1636558222dc07 -> /var/lib/postgresql/data
$ docker stop pg-wrong
$ docker run -d --rm --name pg-wrong -e POSTGRES_PASSWORD=change-me-locally -v pg-wrong:/var/lib/postgresql postgres:17-alpine
$ docker exec pg-wrong psql -U postgres -c '\l shop'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
------+-------+----------+-----------------+---------+-------+--------+-----------+-------------------
(0 rows)psql: подключение и мета-команды
psql — консольный клиент PostgreSQL. Проще всего запустить его прямо в контейнере через docker exec -it. Внутри контейнера подключение идёт через Unix-сокет, и образ разрешает его без пароля; подключение снаружи, по TCP через опубликованный порт, требует пароль (метод scram-sha-256).
$ docker exec -it shop-db psql -U postgres
postgres=# \conninfo
You are connected to database "postgres" as user "postgres" via socket in "/var/run/postgresql" at port "5432".
postgres=# SELECT version();
version
------------------------------------------------------------------------------------------
PostgreSQL 17.11 on x86_64-pc-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
(1 row)
postgres=# \qКоманды, которые начинаются с обратной косой черты, — мета-команды: их выполняет сам psql, а не сервер. SQL-команда заканчивается точкой с запятой; пока её нет, psql ждёт продолжения и меняет приглашение на shop->. Самые нужные мета-команды:
\l
- Что делает
- список баз данных с владельцами и кодировкой
\c shop
- Что делает
- переключиться на базу
shop
\dt
- Что делает
- таблицы текущей базы
\d orders
- Что делает
- структура таблицы: столбцы, типы, значения по умолчанию, индексы, ограничения, внешние ключи
\du
- Что делает
- роли (пользователи) и их атрибуты
\dp products
- Что делает
- права доступа к таблице
\conninfo
- Что делает
- к какой базе и под каким пользователем вы подключены
\x
- Что делает
- вертикальный вывод — удобно для широких строк
\i file.sql
- Что делает
- выполнить файл с SQL
\? / \h CREATE TABLE
- Что делает
- справка по мета-командам / по синтаксису SQL
\q
- Что делает
- выйти
Без интерактивного режима psql тоже полезен: флаг -c выполняет одну команду, -f — файл, а -v ON_ERROR_STOP=1 останавливает скрипт на первой ошибке и возвращает ненулевой код выхода. По умолчанию psql продолжает выполнять скрипт после ошибки — для скриптов схемы это опасно.
База и пользователь с минимальными правами
Суперпользователь postgres обходит все проверки прав, поэтому приложению его не дают. Создадим две роли: shop_app — владелец базы shop, от его имени работает приложение и создаются таблицы; shop_readonly — только чтение, например для отчётов. Все файлы урока лежат в папке db/ рядом с вами.
-- Выполняет суперпользователь postgres один раз
CREATE ROLE shop_app LOGIN PASSWORD 'change-me-app';
CREATE ROLE shop_readonly LOGIN PASSWORD 'change-me-readonly';
-- Владелец базы — пользователь приложения, а не суперпользователь
CREATE DATABASE shop OWNER shop_app;-- Выполняет владелец shop_app в базе shop
GRANT SELECT ON ALL TABLES IN SCHEMA public TO shop_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO shop_readonly;docker cp db shop-db:/db
docker exec shop-db psql -U postgres -v ON_ERROR_STOP=1 -f /db/roles.sql
docker exec shop-db psql -U shop_app -d shop -v ON_ERROR_STOP=1 \
-f /db/schema.sql -f /db/seed.sql -f /db/grants.sql$ docker exec shop-db psql -U postgres -v ON_ERROR_STOP=1 -f /db/roles.sql
CREATE ROLE
CREATE ROLE
CREATE DATABASE
$ docker exec shop-db psql -U shop_app -d shop -v ON_ERROR_STOP=1 -f /db/schema.sql -f /db/seed.sql -f /db/grants.sql
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 3
INSERT 0 3
INSERT 0 3
INSERT 0 4
GRANT
ALTER DEFAULT PRIVILEGES
$ docker exec shop-db psql -U postgres -c '\l shop' -c '\du'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
------+----------+----------+-----------------+------------+------------+--------+-----------+-------------------
shop | shop_app | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
(1 row)
List of roles
Role name | Attributes
---------------+------------------------------------------------------------
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS
shop_app |
shop_readonly | Проверим, что shop_readonly действительно только читает. Начиная с PostgreSQL 15, схемой public владеет специальная роль pg_database_owner — то есть владелец базы, — а остальным по умолчанию доступно только использование схемы (U), без создания объектов (C).
$ docker exec shop-db psql -U shop_readonly -d shop -c "SELECT name, price FROM products ORDER BY id"
name | price
----------------------------+----------
Espresso Blend 1 kg | 12900.00
Ethiopia Yirgacheffe 250 g | 5600.00
Paper filters V60 | 2500.00
(3 rows)
$ docker exec shop-db psql -U shop_readonly -d shop -c "DELETE FROM orders"
ERROR: permission denied for table orders
$ docker exec shop-db psql -U shop_readonly -d shop -c "CREATE TABLE notes (id int)"
ERROR: permission denied for schema public
LINE 1: CREATE TABLE notes (id int)
^
$ docker exec shop-db psql -U shop_app -d shop -c '\dn+ public'
List of schemas
Name | Owner | Access privileges | Description
--------+-------------------+----------------------------------------+------------------------
public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema
| | =U/pg_database_owner |
(1 row)Типы данных: что выбрать для каждого поля
Тип столбца — первое ограничение: в integer не запишешь текст, в date — 31 февраля. Для типичного бизнес-приложения хватает короткого списка:
Идентификатор строки
- Тип
bigint GENERATED ALWAYS AS IDENTITY- Почему
- Номер выдаёт сама база;
bigintне упрётся в пределinteger(2 147 483 647)
Имя, email, город, комментарий
- Тип
text- Почему
- По документации нет разницы в скорости между
textиvarchar(n); длину при необходимости ограничивают черезCHECK
Количество, вес в граммах
- Тип
integer- Почему
- Целые числа до 2 147 483 647, 4 байта
Цена, сумма заказа
- Тип
numeric(10,2)- Почему
- Точная арифметика: документация рекомендует
numericдля денег.realиdouble precision— приближённые
Дата без времени
- Тип
date- Почему
- День заказа или рождения
Момент события
- Тип
timestamptz- Почему
- Хранится в UTC и показывается в часовом поясе сессии
Да/нет
- Тип
boolean- Почему
true,falseилиNULL— «неизвестно»
Публичный номер заказа
- Тип
uuid- Почему
- Не угадывается перебором; в 17 —
gen_random_uuid(), в 18 добавленаuuidv7()
Гибкие атрибуты товара
- Тип
jsonb- Почему
- Двоичный JSON: быстрее обрабатывается и индексируется; документация советует его вместо
json
Разница видна на практике. Сложение 0.1 + 0.2 в double precision даёт неточный результат, а в numeric — ровно 0.3. Переполнение integer — ошибка, а не тихое искажение. Момент оплаты, записанный как 10:15+05, хранится в UTC и выводится в поясе сессии; в актуальной базе часовых поясов Asia/Almaty — это UTC+5.
shop=> SELECT 0.1::double precision + 0.2::double precision AS float_sum,
shop-> 0.1::numeric + 0.2::numeric AS numeric_sum;
float_sum | numeric_sum
---------------------+-------------
0.30000000000000004 | 0.3
(1 row)
shop=> SELECT 2147483647::integer + 1;
ERROR: integer out of range
shop=> SELECT id, paid_at FROM orders WHERE id = 1;
id | paid_at
----+------------------------
1 | 2026-09-02 05:15:00+00
(1 row)
shop=> SET TimeZone = 'Asia/Almaty';
SET
shop=> SELECT id, paid_at FROM orders WHERE id = 1;
id | paid_at
----+------------------------
1 | 2026-09-02 10:15:00+05
(1 row)
shop=> SELECT name, attrs ->> 'roast' AS roast
shop-> FROM products
shop-> WHERE attrs ? 'roast';
name | roast
----------------------------+-------
Espresso Blend 1 kg | dark
Ethiopia Yirgacheffe 250 g | light
(2 rows)Первая таблица: ограничения и изменение данных
Схема ниже повторяет учебную базу из урока «SQL с нуля» и добавляет несколько столбцов, чтобы показать типы из таблицы выше: public_id (uuid), paid_at (timestamptz), is_active (boolean) и attrs (jsonb).
-- Учебная схема магазина кофе: клиенты, товары, заказы, позиции заказа
CREATE TABLE clients (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL CHECK (name <> ''),
city text,
email text UNIQUE,
created_on date NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
category text NOT NULL,
price numeric(10,2) NOT NULL CHECK (price > 0),
weight_g integer CHECK (weight_g > 0),
is_active boolean NOT NULL DEFAULT true,
attrs jsonb NOT NULL DEFAULT '{}'
);
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
client_id bigint NOT NULL REFERENCES clients (id),
ordered_on date NOT NULL DEFAULT CURRENT_DATE,
status text NOT NULL DEFAULT 'new'
CHECK (status IN ('new', 'paid', 'shipped', 'cancelled')),
promo_code text,
paid_at timestamptz
);
CREATE TABLE order_items (
order_id bigint NOT NULL REFERENCES orders (id) ON DELETE CASCADE,
product_id bigint NOT NULL REFERENCES products (id),
quantity integer NOT NULL CHECK (quantity > 0),
price numeric(10,2) NOT NULL CHECK (price > 0),
PRIMARY KEY (order_id, product_id)
);-- Вымышленные данные: никаких реальных клиентов и контактов
INSERT INTO clients (name, city, email, created_on) VALUES
('Aigerim Seitova', 'Almaty', 'aigerim@example.com', '2026-06-02'),
('Daniyar Akhmetov', 'Astana', NULL, '2026-06-15'),
('Madina Omarova', 'Almaty', 'madina@example.com', '2026-07-01');
INSERT INTO products (name, category, price, weight_g, attrs) VALUES
('Espresso Blend 1 kg', 'coffee', 12900.00, 1000, '{"roast": "dark"}'),
('Ethiopia Yirgacheffe 250 g', 'coffee', 5600.00, 250, '{"roast": "light"}'),
('Paper filters V60', 'gear', 2500.00, NULL, '{"pack": 100}');
INSERT INTO orders (client_id, ordered_on, status, paid_at) VALUES
(1, '2026-09-02', 'shipped', '2026-09-02 10:15:00+05'),
(2, '2026-09-05', 'paid', '2026-09-05 18:40:00+05'),
(1, '2026-09-14', 'new', NULL);
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES
(1, 1, 1, 12900.00),
(1, 3, 2, 2500.00),
(2, 2, 2, 5600.00),
(3, 2, 1, 5600.00);NOT NULL— значение обязательноCHECK (условие)— значение проходит проверку; условие, которое далоNULL, считается выполненным, поэтомуweight_gможно оставить пустымUNIQUE— повторы запрещены, но дваNULLпо умолчанию не считаются равными: несколько клиентов без email допустимыDEFAULT— значение, если столбец не указан вINSERT: сегодняшняя дата, статусnew, новый UUIDPRIMARY KEY—UNIQUE+NOT NULLи автоматически созданный индексREFERENCES— внешний ключ: заказ не может ссылаться на несуществующего клиента. Как выбирать связи и правила удаления, разбираем в уроке о проектировании БД
shop=> \dt
List of relations
Schema | Name | Type | Owner
--------+-------------+-------+----------
public | clients | table | shop_app
public | order_items | table | shop_app
public | orders | table | shop_app
public | products | table | shop_app
(4 rows)
shop=> \d orders
Table "public.orders"
Column | Type | Collation | Nullable | Default
------------+--------------------------+-----------+----------+------------------------------
id | bigint | | not null | generated always as identity
public_id | uuid | | not null | gen_random_uuid()
client_id | bigint | | not null |
ordered_on | date | | not null | CURRENT_DATE
status | text | | not null | 'new'::text
promo_code | text | | |
paid_at | timestamp with time zone | | |
Indexes:
"orders_pkey" PRIMARY KEY, btree (id)
"orders_public_id_key" UNIQUE CONSTRAINT, btree (public_id)
Check constraints:
"orders_status_check" CHECK (status = ANY (ARRAY['new'::text, 'paid'::text, 'shipped'::text, 'cancelled'::text]))
Foreign-key constraints:
"orders_client_id_fkey" FOREIGN KEY (client_id) REFERENCES clients(id)
Referenced by:
TABLE "order_items" CONSTRAINT "order_items_order_id_fkey" FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADEТеперь попробуем нарушить правила. Каждая ошибка называет ограничение и показывает строку, которую база отказалась сохранить:
shop=> INSERT INTO clients (name) VALUES (NULL);
ERROR: null value in column "name" of relation "clients" violates not-null constraint
DETAIL: Failing row contains (4, null, null, null, 2026-10-12).
shop=> INSERT INTO clients (name) VALUES ('');
ERROR: new row for relation "clients" violates check constraint "clients_name_check"
DETAIL: Failing row contains (5, , null, null, 2026-10-12).
shop=> INSERT INTO clients (name, email) VALUES ('Duplicate', 'aigerim@example.com');
ERROR: duplicate key value violates unique constraint "clients_email_key"
DETAIL: Key (email)=(aigerim@example.com) already exists.
shop=> INSERT INTO products (name, category, price) VALUES ('Free sample', 'coffee', 0);
ERROR: new row for relation "products" violates check constraint "products_price_check"
DETAIL: Failing row contains (4, Free sample, coffee, 0.00, null, t, {}).
shop=> INSERT INTO orders (client_id, status) VALUES (1, 'done');
ERROR: new row for relation "orders" violates check constraint "orders_status_check"
DETAIL: Failing row contains (4, 782914a2-f487-46e0-a5e7-6655ab194e20, 1, 2026-10-12, done, null, null).
shop=> INSERT INTO orders (client_id) VALUES (999);
ERROR: insert or update on table "orders" violates foreign key constraint "orders_client_id_fkey"
DETAIL: Key (client_id)=(999) is not present in table "clients".
shop=> INSERT INTO clients (id, name) VALUES (100, 'Manual id');
ERROR: cannot insert a non-DEFAULT value into column "id"
DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.
shop=> INSERT INTO clients (name) VALUES ('No email 1'), ('No email 2') RETURNING id, email;
id | email
----+-------
7 |
8 |
(2 rows)
INSERT 0 2Обратите внимание на номера: отклонённые вставки израсходовали значения 4, 5 и 6, и следующий клиент получил id 7. Это нормально: документация прямо говорит, что последовательности не возвращают номера при откате, поэтому идентификаторы идут с пропусками. Если бизнесу нужна сплошная нумерация документов, её делают отдельным механизмом.
Изменения данных — INSERT, UPDATE, DELETE — умеют возвращать затронутые строки через RETURNING. Это избавляет от второго запроса: приложение сразу получает id новой записи или новые цены.
shop=> INSERT INTO clients (name, city)
shop-> VALUES ('Timur Bekov', 'Shymkent')
shop-> RETURNING id, created_on;
id | created_on
----+------------
9 | 2026-10-12
(1 row)
INSERT 0 1
shop=> UPDATE products
shop-> SET price = price * 1.10
shop-> WHERE category = 'coffee'
shop-> RETURNING id, name, price;
id | name | price
----+----------------------------+----------
1 | Espresso Blend 1 kg | 14190.00
2 | Ethiopia Yirgacheffe 250 g | 6160.00
(2 rows)
UPDATE 2
shop=> DELETE FROM orders
shop-> WHERE status = 'new'
shop-> RETURNING id, public_id;
id | public_id
----+--------------------------------------
3 | 6138bdcb-be64-4598-a3e1-5c2c2ef80458
(1 row)
DELETE 1
shop=> SELECT count(*) AS items_left FROM order_items;
items_left
------------
3
(1 row)Резервная копия и подключение из приложения
Том защищает данные от удаления контейнера. Проверим: остановленный контейнер с --rm исчезает, но новый контейнер на том же томе видит всю базу и пропускает инициализацию.
$ docker stop shop-db
shop-db
$ docker run -d --rm --name shop-db -e POSTGRES_PASSWORD=change-me-locally -p 127.0.0.1:5432:5432 -v shop-pgdata:/var/lib/postgresql/data postgres:17-alpine
$ docker logs shop-db 2>&1 | head -3
PostgreSQL Database directory appears to contain a database; Skipping initialization
$ docker exec shop-db psql -U shop_app -d shop -c "SELECT count(*) AS clients FROM clients"
clients
---------
6
(1 row)От ошибочного DELETE, сбоя диска или удалённого тома защищает только резервная копия. pg_dump делает согласованную копию одной базы, не блокируя читающих и пишущих пользователей. Простой формат — SQL-скрипт, его восстанавливают через psql. Формат custom (-Fc) сжат и восстанавливается через pg_restore, который умеет выбирать отдельные объекты.
# 1. Дамп в формате custom (-Fc): сжатый, читается pg_restore
docker exec shop-db pg_dump -U shop_app -d shop -Fc -f /tmp/shop.dump
docker cp shop-db:/tmp/shop.dump ./shop.dump
# 2. Проверка: восстановить в отдельную пустую базу
docker exec shop-db psql -U postgres -c "CREATE DATABASE shop_restore OWNER shop_app"
docker exec shop-db pg_restore -U shop_app -d shop_restore /tmp/shop.dump$ docker exec shop-db pg_restore -U shop_app -d shop_restore /tmp/shop.dump; echo "exit $?"
exit 0
$ docker exec shop-db psql -U shop_app -d shop -c "SELECT (SELECT count(*) FROM clients) AS clients, (SELECT count(*) FROM products) AS products, (SELECT count(*) FROM orders) AS orders, (SELECT count(*) FROM order_items) AS items"
clients | products | orders | items
---------+----------+--------+-------
6 | 3 | 2 | 3
(1 row)
$ docker exec shop-db psql -U shop_app -d shop_restore -c "SELECT (SELECT count(*) FROM clients) AS clients, (SELECT count(*) FROM products) AS products, (SELECT count(*) FROM orders) AS orders, (SELECT count(*) FROM order_items) AS items"
clients | products | orders | items
---------+----------+--------+-------
6 | 3 | 2 | 3
(1 row)Текстовый дамп начинается со строки \restrict со случайным ключом. Её добавили в выпуске 17.6 по соображениям безопасности: при восстановлении psql блокирует мета-команды, которые могли бы оказаться в данных. Поэтому текстовые дампы восстанавливайте psql той же или более новой версии. Готовый дамп в существующую базу накатывается через pg_restore --clean --if-exists: он удаляет и создаёт объекты заново.
$ docker exec shop-db pg_dump -U shop_app -d shop -f /tmp/shop.sql
$ docker exec shop-db head -8 /tmp/shop.sql
--
-- PostgreSQL database dump
--
\restrict AuNO2dNJw5UDhjrML04TbDxP98WIJkamUGNY5xdp8sLuCz1X2zVIZqKR8umAdei
-- Dumped from database version 17.11
-- Dumped by pg_dump version 17.11Приложение подключается по TCP со строкой подключения формата postgresql://пользователь:пароль@хост:порт/база. Её хранят в переменной окружения, файл .env добавляют в .gitignore. Для проверки возьмём Node.js и драйвер pg.
DATABASE_URL=postgresql://shop_app:change-me-app@localhost:5432/shopimport pg from "pg";
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
const who = await client.query("SELECT current_user, current_database()");
console.log(who.rows[0]);
// Параметр $1 передаётся отдельно от текста запроса — без склейки строк
const cheap = await client.query(
"SELECT name, price FROM products WHERE price < $1 ORDER BY price",
[7000],
);
console.log(cheap.rows);
await client.end();$ npm install pg
$ node --env-file=.env check-db.js
{ current_user: 'shop_app', current_database: 'shop' }
[
{ name: 'Paper filters V60', price: '2500.00' },
{ name: 'Ethiopia Yirgacheffe 250 g', price: '6160.00' }
]Частые ошибки и упражнения
Conflict. The container name "/shop-db" is already in use
- Причина
- Ещё работает контейнер
shop-dbиз урока «SQL с нуля» — он запущен без тома - Что сделать
docker stop shop-db: контейнер с--rmудалится, имя освободится
Bind for 0.0.0.0:5432 failed: port is already allocated
- Причина
- Порт 5432 уже занят другим PostgreSQL или контейнером
- Что сделать
- Опубликуйте другой порт:
-p 127.0.0.1:5433:5432— и укажите 5433 в строке подключения
После пересоздания контейнера база пустая
- Причина
- Том не подключён или подключён не к тому пути
- Что сделать
- Для 17 —
/var/lib/postgresql/data, для 18+ —/var/lib/postgresql; проверьтеdocker inspect
password authentication failed for user "shop_app"
- Причина
- Неверный пароль в строке подключения или
POSTGRES_PASSWORDизменили после первого запуска - Что сделать
- Переменные образа действуют только при инициализации пустого тома; пароль меняют через
\passwordилиALTER ROLE
Скрипты из /docker-entrypoint-initdb.d не выполнились
- Причина
- В томе уже есть база
- Что сделать
- Скрипты запускаются только на пустом каталоге данных
psql ничего не отвечает, приглашение shop->
- Причина
- Команда без точки с запятой
- Что сделать
- Допишите
;или сбросьте ввод командой\r
permission denied for schema public
- Причина
- Таблицу создаёт не владелец базы
- Что сделать
- Создавайте объекты владельцем или выдайте право явно:
GRANT CREATE ON SCHEMA public TO …
psql: error: /db/…: No such file or directory
- Причина
- Контейнер пересоздан, файлы из
docker cpпропали вместе с ним - Что сделать
- Повторите
docker cpили смонтируйте папку в контейнер
Как проверено7 · пройдено1 · частичносверено 11 октября 2026 г.
- запущено, работает
docker volume create + docker run (17-alpine, том, порт на 127.0.0.1), docker logs
Git Bash (MSYS_NO_PATHCONV=1) и PowerShell 7.6.6 на Windows 11, Docker Desktop: имена kb-pg-start / kb-pg-start-data / kb-pg-start-ps, порты 55432/55493 вместо 5432 (5432 занят). В статье имена заменены на shop-db / shop-pgdata, порт — 5432; остальной текст команд и вывод — реальные
- запущено, работает
db/roles.sql, db/schema.sql, db/seed.sql, db/grants.sql (docker cp + psql -f, ON_ERROR_STOP)
postgres:17-alpine (PostgreSQL 17.11): полный прогон в порядке статьи скриптом run-article.sh, все выводы psql в статье — из run-article.log (UUID и время из этого прогона). Те же файлы и демонстрации на postgres:18-alpine (18.6, том в /var/lib/postgresql) — вывод идентичен, кроме заголовка \dt («List of tables») и порядка stdout/stderr
- запущено, работает
ошибки ограничений, NULL в UNIQUE, RETURNING, типы (float/numeric, переполнение integer, timestamptz Asia/Almaty, jsonb), права shop_readonly
реальный вывод psql 17.11; транскрипты с приглашениями shop=> / shop-> собраны из вывода psql -a (эхо ввода) и построчного запуска — ответы сервера не изменены
- запущено, работает
ошибка пути тома в 17 (/var/lib/postgresql) и пересоздание контейнера с правильным томом
docker inspect показал анонимный том на /var/lib/postgresql/data, база shop пропала после перезапуска; с правильным путём — «Skipping initialization», данные на месте
- запущено, работает
pg_dump -Fc + docker cp + pg_restore в shop_restore; текстовый дамп с \restrict; pg_restore --clean --if-exists
exit 0, количество строк совпало (6/3/2/3); --clean --if-exists восстановил удалённые строки; дамп из 17.11 восстановлен в 18.6 (pg_restore --no-owner --no-acl) без ошибок; docker cp дампа проверен и в PowerShell
- запущено, работает
.env + check-db.js (Node.js, pg)
Node.js 24.18.0, pg 8.23.1, node --env-file=.env check-db.js по TCP 127.0.0.1:55432 с паролем — вывод в статье реальный; неверный пароль → password authentication failed for user "shop_app"
- запущено, работает
ошибки занятого порта и занятого имени контейнера
docker run -p 127.0.0.1:5432:5432 при занятом 5432 → «Bind for 0.0.0.0:5432 failed: port is already allocated», контейнер не стартовал; второй docker run с тем же --name → «Conflict. The container name "/kb-pg-name" is already in use» (в статье имя shop-db — то же, что в уроке sql-select)
- частично
упражнения: ex2.sql (stock) и автоинициализация через /docker-entrypoint-initdb.d
ex2 — ALTER/UPDATE/ошибка products_stock_check, вывод в решении реальный (VERIFIED). initdb.d — POSTGRES_DB=shop, 01-schema.sql и 02-seed.sql выполнены (лог entrypoint), order_items = 4; на Windows путь передан как $(pwd -W). Вариант "$PWD/db-init" из статьи — синтаксис bash для Linux/macOS, на Linux не запускался (в Git Bash на Windows он монтирует пустую папку — проверено)
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.