diff --git a/.docker.env.example b/.docker.env.example
index 59c33c5..2c321be 100644
--- a/.docker.env.example
+++ b/.docker.env.example
@@ -6,10 +6,9 @@ MYSQL_VERSION=9.7.1
POSTGRES_VERSION=18.4
ADMINER_VERSION=5.4.2
-# Имена контейнеров
-MYSQL_CONTAINER=mysql
-POSTGRES_CONTAINER=postgres
-ADMINER_CONTAINER=adminer
+# Публикация портов только на loopback по умолчанию.
+# При необходимости можно явно указать другой адрес, например адрес VPN-интерфейса.
+BIND_ADDRESS=127.0.0.1
# Порты хоста
MYSQL_PORT=3306
diff --git a/.gitattributes b/.gitattributes
index 7929293..b53897c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -16,3 +16,15 @@
*.conf text eol=lf
*.ini text eol=lf
*.make text eol=lf
+
+# Бинарные изображения и архивы
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.webp binary
+*.ico binary
+*.pdf binary
+*.zip binary
+*.gz binary
+*.tgz binary
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..8e4c7cd
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+custom:
+ - "https://boosty.to/backendbezpafosa"
+ - "https://sponsr.ru/backendbezpafosa/"
diff --git a/.github/workflows/sql-lab-check.yml b/.github/workflows/sql-lab-check.yml
index 93f17ef..4184281 100644
--- a/.github/workflows/sql-lab-check.yml
+++ b/.github/workflows/sql-lab-check.yml
@@ -27,6 +27,9 @@ jobs:
docker --version
docker compose version
+ - name: Test managed storage path validation
+ run: make test-storage-paths
+
- name: Initialize environment
run: make init
@@ -39,6 +42,9 @@ jobs:
- name: Check complete database stand
run: make check
+ - name: Smoke-test trusted SQL imports
+ run: make test-sql-imports
+
- name: Confirm databases are running without Adminer
run: |
set -Eeuo pipefail
@@ -54,6 +60,38 @@ jobs:
- name: Start Adminer
run: make up-ui
+ - name: Confirm all published ports bind to loopback
+ run: |
+ set -Eeuo pipefail
+ set -a
+ source .docker.env
+ set +a
+
+ compose=(docker compose --env-file .docker.env -p "${COMPOSE_PROJECT_NAME}" --profile ui)
+
+ assert_loopback_binding() {
+ local service="$1"
+ local container_port="$2"
+ local container_id
+ local host_ip
+
+ container_id="$("${compose[@]}" ps --quiet "${service}")"
+ if [[ -z "${container_id}" ]]; then
+ echo "ERROR: Compose service ${service} has no container ID" >&2
+ exit 1
+ fi
+
+ host_ip="$(docker inspect --format "{{(index (index .NetworkSettings.Ports \"${container_port}/tcp\") 0).HostIp}}" "${container_id}")"
+ if [[ "${host_ip}" != "127.0.0.1" ]]; then
+ echo "ERROR: ${service} (${container_id}) publishes ${container_port}/tcp on HostIp '${host_ip}', expected '127.0.0.1'" >&2
+ exit 1
+ fi
+ }
+
+ assert_loopback_binding mysql 3306
+ assert_loopback_binding postgres 5432
+ assert_loopback_binding adminer 8080
+
- name: Smoke-test Adminer login page
run: |
set -Eeuo pipefail
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..2a42bf8
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025-2026 Aleksandr Yurchenko
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
index 26e27c4..d1d7d59 100644
--- a/Makefile
+++ b/Makefile
@@ -9,9 +9,9 @@ SHELL := bash
.PHONY: help init check-env pull config up up-no-ui up-mysql up-mysql-ui \
up-postgres up-postgres-ui up-ui down-ui wait-mysql wait-postgres \
down status logs log in mysql mysql-user postgres postgres-user sh \
- samples-mysql samples-postgres mysql-grants mysql-import check check-mysql-access \
+ samples-mysql samples-postgres mysql-grants mysql-import postgres-import check check-mysql-access \
check-postgres-access dump restore clean-mysql clean-postgres clean-all \
- reinit-mysql reinit-postgres reinit-all
+ reinit-mysql reinit-postgres reinit-all test-storage-paths test-sql-imports
PROJECT_DIR := $(CURDIR)
ENV_FILE_EXAMPLE := .docker.env.example
@@ -20,7 +20,6 @@ HOST_UID := $(shell id -u)
HOST_GID := $(shell id -g)
REQUIRED_ENV_VARS := COMPOSE_PROJECT_NAME MYSQL_VERSION POSTGRES_VERSION ADMINER_VERSION \
- MYSQL_CONTAINER POSTGRES_CONTAINER ADMINER_CONTAINER \
MYSQL_PORT POSTGRES_PORT ADMINER_PORT \
MYSQL_DATA_DIR POSTGRES_DATA_DIR MYSQL_CONF_FILE \
MYSQL_INITDB_DIR POSTGRES_INITDB_DIR MYSQL_SAMPLES_DIR POSTGRES_SAMPLES_DIR \
@@ -33,9 +32,14 @@ COMPOSE_UI = $(COMPOSE) --profile ui
MYSQL_SAMPLES_TMP_DIR := .tmp/mysql-samples
SAKILA_URL := https://downloads.mysql.com/docs/sakila-db.zip
+SAKILA_SHA256 := c2ecb3dec28d752241ccfca02974ba970de3c3fc5d98887fd3f9d5843f946672
POSTGRES_SAMPLES_TMP_DIR := .tmp/postgres-samples
PAGILA_REF := 5ba5a57aeb159f75f02aca2432d3c262186d13d3
PAGILA_BASE_URL := https://raw.githubusercontent.com/devrimgunduz/pagila/$(PAGILA_REF)
+PAGILA_LICENSE_URL := $(PAGILA_BASE_URL)/LICENSE.txt
+PAGILA_LICENSE_BLOB := c6078c708c6f55b56e24f3687c591ca12df567ca
+PAGILA_SCHEMA_BLOB := 23718a3adef90ced002e19ad4e1ac98d22aa5870
+PAGILA_DATA_BLOB := b7c016861fd0f84008645153c6a1d9e5a99b9cc6
CHINOOK_REF := 4a944a942426e1f3263fe539155fb7ef92b04b4a
CHINOOK_BASE_URL := https://raw.githubusercontent.com/lerocha/chinook-database/$(CHINOOK_REF)
CHINOOK_LICENSE_URL := $(CHINOOK_BASE_URL)/LICENSE.md
@@ -73,6 +77,10 @@ help:
@echo " make check проверить Compose и доступ DB_USER к обеим СУБД"
@echo " make samples-mysql скачать optional samples Chinook и Sakila"
@echo " make samples-postgres скачать optional samples Pagila и Chinook"
+ @echo " make test-storage-paths проверить защиту managed storage paths"
+ @echo " make test-sql-imports проверить trusted SQL imports в запущенных СУБД"
+ @echo " make mysql-import FILE=... DATABASE=... импортировать доверенный text SQL в MySQL"
+ @echo " make postgres-import FILE=... DATABASE=... импортировать доверенный text SQL в PostgreSQL"
@echo " make clean-{mysql,postgres,all} CONFIRM=1"
@echo " make reinit-{mysql,postgres,all} CONFIRM=1"
@@ -98,10 +106,18 @@ check-env: $(ENV_FILE)
echo "ERROR: обязательные MYSQL_DATABASE и POSTGRES_DATABASE должны называться demo" >&2; \
exit 1; \
fi; \
- if [[ "$$(realpath -m "$${MYSQL_DATA_DIR}")" == "$$(realpath -m "$${POSTGRES_DATA_DIR}")" ]]; then \
- echo "ERROR: MYSQL_DATA_DIR и POSTGRES_DATA_DIR должны быть разными каталогами" >&2; \
- exit 1; \
- fi
+ "$(PROJECT_DIR)/scripts/validate-storage-paths.sh" \
+ --project-dir "$(PROJECT_DIR)" \
+ --mysql-data "$${MYSQL_DATA_DIR}" \
+ --postgres-data "$${POSTGRES_DATA_DIR}" \
+ --mysql-samples "$${MYSQL_SAMPLES_DIR}" \
+ --postgres-samples "$${POSTGRES_SAMPLES_DIR}"
+
+test-storage-paths:
+ @./scripts/test-storage-paths.sh
+
+test-sql-imports:
+ @./scripts/test-sql-imports.sh
init: check-env
@echo "Проверяем каталоги, конфигурацию и init-скрипты..."
@@ -255,6 +271,7 @@ samples-mysql: check-env
@command -v curl >/dev/null || { echo "ERROR: требуется curl" >&2; exit 1; }
@command -v unzip >/dev/null || { echo "ERROR: требуется unzip" >&2; exit 1; }
@command -v git >/dev/null || { echo "ERROR: требуется git для проверки Git blob SHA" >&2; exit 1; }
+ @command -v sha256sum >/dev/null || { echo "ERROR: требуется sha256sum" >&2; exit 1; }
@set -Eeuo pipefail; $(LOAD_ENV) \
tmp_root="$(MYSQL_SAMPLES_TMP_DIR)"; \
download_dir="$${tmp_root}/download"; \
@@ -291,6 +308,7 @@ samples-mysql: check-env
"$(CHINOOK_MYSQL_URL)" -o "$${download_dir}/Chinook_MySql.sql"; \
curl --fail --location --retry 3 --retry-all-errors --connect-timeout 15 --max-time 180 \
"$(SAKILA_URL)" -o "$${download_dir}/sakila-db.zip"; \
+ echo "$(SAKILA_SHA256) $${download_dir}/sakila-db.zip" | sha256sum --check --status || { echo "ERROR: неожиданный SHA-256 sakila-db.zip" >&2; exit 1; }; \
test "$$(git hash-object --no-filters "$${download_dir}/LICENSE.md")" = "$(CHINOOK_LICENSE_BLOB)" || { echo "ERROR: неожиданный Git blob SHA LICENSE.md" >&2; exit 1; }; \
test "$$(git hash-object --no-filters "$${download_dir}/Chinook_MySql.sql")" = "$(CHINOOK_MYSQL_BLOB)" || { echo "ERROR: неожиданный Git blob SHA Chinook_MySql.sql" >&2; exit 1; }; \
test -s "$${download_dir}/LICENSE.md" || { echo "ERROR: LICENSE.md пуст" >&2; exit 1; }; \
@@ -326,6 +344,10 @@ samples-mysql: check-env
sakila_data_source="$$(find "$${download_dir}/sakila" -type f -name sakila-data.sql -print -quit)"; \
test -n "$$sakila_schema_source" || { echo "ERROR: архив Sakila не содержит sakila-schema.sql" >&2; exit 1; }; \
test -n "$$sakila_data_source" || { echo "ERROR: архив Sakila не содержит sakila-data.sql" >&2; exit 1; }; \
+ for sakila_source in "$$sakila_schema_source" "$$sakila_data_source"; do \
+ grep -Fq 'Redistribution and use in source and binary forms' "$$sakila_source" || { echo "ERROR: в $${sakila_source} отсутствует ожидаемый New BSD notice" >&2; exit 1; }; \
+ grep -Fq 'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS' "$$sakila_source" || { echo "ERROR: в $${sakila_source} отсутствует ожидаемый New BSD disclaimer" >&2; exit 1; }; \
+ done; \
cp "$$sakila_schema_source" "$${ready_dir}/020_sakila_schema.sql"; \
cp "$$sakila_data_source" "$${ready_dir}/021_sakila_data.sql"; \
if [[ -f "$${target_dir}/.gitkeep" ]]; then cp "$${target_dir}/.gitkeep" "$${ready_dir}/.gitkeep"; else touch "$${ready_dir}/.gitkeep"; fi; \
@@ -377,10 +399,16 @@ samples-postgres: check-env
"$(PAGILA_BASE_URL)/pagila-schema.sql" -o "$${download_dir}/pagila-schema.sql"; \
curl --fail --location --retry 3 --retry-all-errors --connect-timeout 15 --max-time 180 \
"$(PAGILA_BASE_URL)/pagila-data.sql" -o "$${download_dir}/pagila-data.sql"; \
+ curl --fail --location --retry 3 --retry-all-errors --connect-timeout 15 --max-time 180 \
+ "$(PAGILA_LICENSE_URL)" -o "$${download_dir}/LICENSE.pagila.txt"; \
curl --fail --location --retry 3 --retry-all-errors --connect-timeout 15 --max-time 180 \
"$(CHINOOK_LICENSE_URL)" -o "$${download_dir}/LICENSE.md"; \
curl --fail --location --retry 3 --retry-all-errors --connect-timeout 15 --max-time 180 \
"$(CHINOOK_POSTGRES_URL)" -o "$${download_dir}/Chinook_PostgreSql.sql"; \
+ test "$$(git hash-object --no-filters "$${download_dir}/LICENSE.pagila.txt")" = "$(PAGILA_LICENSE_BLOB)" || { echo "ERROR: неожиданный Git blob SHA Pagila LICENSE.txt" >&2; exit 1; }; \
+ test "$$(git hash-object --no-filters "$${download_dir}/pagila-schema.sql")" = "$(PAGILA_SCHEMA_BLOB)" || { echo "ERROR: неожиданный Git blob SHA pagila-schema.sql" >&2; exit 1; }; \
+ test "$$(git hash-object --no-filters "$${download_dir}/pagila-data.sql")" = "$(PAGILA_DATA_BLOB)" || { echo "ERROR: неожиданный Git blob SHA pagila-data.sql" >&2; exit 1; }; \
+ test -s "$${download_dir}/LICENSE.pagila.txt" || { echo "ERROR: Pagila LICENSE.txt пуст" >&2; exit 1; }; \
test -s "$${download_dir}/pagila-schema.sql" || { echo "ERROR: pagila-schema.sql пуст" >&2; exit 1; }; \
test -s "$${download_dir}/pagila-data.sql" || { echo "ERROR: pagila-data.sql пуст" >&2; exit 1; }; \
grep -Fq 'CREATE TABLE public.actor' "$${download_dir}/pagila-schema.sql" || { echo "ERROR: в schema нет таблицы actor" >&2; exit 1; }; \
@@ -406,8 +434,24 @@ samples-postgres: check-env
test "$$(grep -Fxc "$${expected_line}" "$${chinook_source}" || true)" = 1 || { echo "ERROR: неожиданный формат database-level строки: $${expected_line}" >&2; exit 1; }; \
done; \
test "$$(awk 'BEGIN { in_comment = 0; count = 0 } { upper = toupper($$0) } /^[[:space:]]*\/\*/ { in_comment = 1 } !in_comment && (upper ~ /^[[:space:]]*(DROP|CREATE)[[:space:]]+DATABASE/ || upper ~ /^[[:space:]]*\\(C|CONNECT)([[:space:]]|$$)/) { count++ } /\*\// { in_comment = 0 } END { print count }' "$${chinook_source}")" = 3 || { echo "ERROR: Chinook PostgreSQL содержит неожиданные database-level statements" >&2; exit 1; }; \
- cp "$${download_dir}/pagila-schema.sql" "$${ready_dir}/010_pagila_schema.sql"; \
- cp "$${download_dir}/pagila-data.sql" "$${ready_dir}/020_pagila_data.sql"; \
+ sed 's/\r$$//' "$${download_dir}/LICENSE.pagila.txt" > "$${download_dir}/LICENSE.pagila.normalized.txt"; \
+ { \
+ echo '-- Pagila license notice (upstream LICENSE.txt):'; \
+ sed 's/^/-- /' "$${download_dir}/LICENSE.pagila.normalized.txt"; \
+ echo; \
+ cat "$${download_dir}/pagila-schema.sql"; \
+ } > "$${ready_dir}/010_pagila_schema.sql"; \
+ { \
+ echo '-- Pagila license notice (upstream LICENSE.txt):'; \
+ sed 's/^/-- /' "$${download_dir}/LICENSE.pagila.normalized.txt"; \
+ echo; \
+ cat "$${download_dir}/pagila-data.sql"; \
+ } > "$${ready_dir}/020_pagila_data.sql"; \
+ while IFS= read -r license_line || [[ -n "$${license_line}" ]]; do \
+ for pagila_ready_file in "$${ready_dir}/010_pagila_schema.sql" "$${ready_dir}/020_pagila_data.sql"; do \
+ grep -Fxq -- "-- $${license_line}" "$${pagila_ready_file}" || { echo "ERROR: Pagila notice перенесён не полностью в $${pagila_ready_file}" >&2; exit 1; }; \
+ done; \
+ done < "$${download_dir}/LICENSE.pagila.normalized.txt"; \
{ \
echo '-- Chinook Database MIT license notice (upstream LICENSE.md):'; \
sed 's/^/-- /' "$${download_dir}/LICENSE.normalized.md"; \
@@ -438,14 +482,46 @@ samples-postgres: check-env
mysql-grants: wait-mysql
$(COMPOSE) exec -T mysql /docker-entrypoint-initdb.d/090_grant_training_access.sh
-mysql-import: wait-mysql
- @test -n "$(FILE)" || { echo "ERROR: укажите FILE=path/to/database.sql" >&2; exit 1; }
- @test -f "$(FILE)" || { echo "ERROR: файл $(FILE) не найден" >&2; exit 1; }
- @echo "Импортируем $(FILE) от имени root..."
- $(COMPOSE) exec -T mysql sh -c 'MYSQL_PWD="$$MYSQL_ROOT_PASSWORD" mysql -uroot' < "$(FILE)"
- @$(MAKE) --no-print-directory mysql-grants
+mysql-import:
+ @set -Eeuo pipefail; \
+ if [[ -z "$${FILE:-}" ]]; then echo "ERROR: укажите FILE=path/to/file.sql" >&2; exit 1; fi; \
+ if [[ -z "$${DATABASE:-}" ]]; then echo "ERROR: укажите DATABASE=database_name" >&2; exit 1; fi; \
+ if [[ ! -f "$${FILE}" || ! -r "$${FILE}" || ! -s "$${FILE}" ]]; then echo "ERROR: FILE должен быть существующим читаемым непустым обычным файлом" >&2; exit 1; fi; \
+ if [[ ! "$${DATABASE}" =~ ^[a-z][a-z0-9_]{0,62}$$ ]]; then echo "ERROR: недопустимое имя MySQL database: $${DATABASE}" >&2; exit 1; fi; \
+ case "$${DATABASE}" in mysql|information_schema|performance_schema|sys) echo "ERROR: импорт в системную MySQL database запрещён: $${DATABASE}" >&2; exit 1 ;; esac
+ @$(MAKE) --no-print-directory wait-mysql
+ @set -Eeuo pipefail; \
+ $(LOAD_ENV) \
+ if ! $(COMPOSE) exec -T -e IMPORT_DATABASE="$${DATABASE}" mysql sh -c 'MYSQL_PWD="$$DB_PASSWORD" exec mysql --host=127.0.0.1 --user="$$DB_USER" --batch --skip-column-names "$$IMPORT_DATABASE" --execute "SELECT 1;"' >/dev/null; then \
+ echo "ERROR: DB_USER cannot connect to MySQL database $${DATABASE}; import was not started" >&2; exit 1; \
+ fi; \
+ echo "Импортируем $${FILE} в MySQL database $${DATABASE} от имени DB_USER..."; \
+ $(COMPOSE) exec -T -e IMPORT_DATABASE="$${DATABASE}" mysql sh -c 'MYSQL_PWD="$$DB_PASSWORD" exec mysql --host=127.0.0.1 --user="$$DB_USER" --binary-mode=1 "$$IMPORT_DATABASE"' < "$${FILE}"; \
+ if ! $(COMPOSE) exec -T -e IMPORT_DATABASE="$${DATABASE}" mysql sh -c 'MYSQL_PWD="$$DB_PASSWORD" exec mysql --host=127.0.0.1 --user="$$DB_USER" --batch --skip-column-names "$$IMPORT_DATABASE" --execute "SELECT 1;"' >/dev/null; then \
+ echo "ERROR: DB_USER cannot reconnect to MySQL database $${DATABASE} after import" >&2; exit 1; \
+ fi
@$(MAKE) --no-print-directory check-mysql-access
+postgres-import:
+ @set -Eeuo pipefail; \
+ if [[ -z "$${FILE:-}" ]]; then echo "ERROR: укажите FILE=path/to/file.sql" >&2; exit 1; fi; \
+ if [[ -z "$${DATABASE:-}" ]]; then echo "ERROR: укажите DATABASE=database_name" >&2; exit 1; fi; \
+ if [[ ! -f "$${FILE}" || ! -r "$${FILE}" || ! -s "$${FILE}" ]]; then echo "ERROR: FILE должен быть существующим читаемым непустым обычным файлом" >&2; exit 1; fi; \
+ if [[ ! "$${DATABASE}" =~ ^[a-z][a-z0-9_]{0,62}$$ ]]; then echo "ERROR: недопустимое имя PostgreSQL database: $${DATABASE}" >&2; exit 1; fi; \
+ case "$${DATABASE}" in postgres|template0|template1) echo "ERROR: импорт в системную PostgreSQL database запрещён: $${DATABASE}" >&2; exit 1 ;; esac
+ @$(MAKE) --no-print-directory wait-postgres
+ @set -Eeuo pipefail; \
+ $(LOAD_ENV) \
+ if ! $(COMPOSE) exec -T -e IMPORT_DATABASE="$${DATABASE}" postgres sh -c 'PGPASSWORD="$$DB_PASSWORD" exec psql --host=127.0.0.1 --username="$$DB_USER" --dbname="$$IMPORT_DATABASE" --no-psqlrc --set=ON_ERROR_STOP=1 --command="SELECT 1;"' >/dev/null; then \
+ echo "ERROR: DB_USER cannot connect to PostgreSQL database $${DATABASE}; import was not started" >&2; exit 1; \
+ fi; \
+ echo "Импортируем $${FILE} в PostgreSQL database $${DATABASE} от имени DB_USER..."; \
+ $(COMPOSE) exec -T -e IMPORT_DATABASE="$${DATABASE}" postgres sh -c 'PGPASSWORD="$$DB_PASSWORD" exec psql --host=127.0.0.1 --username="$$DB_USER" --dbname="$$IMPORT_DATABASE" --no-psqlrc --set=ON_ERROR_STOP=1 --file=-' < "$${FILE}"; \
+ if ! $(COMPOSE) exec -T -e IMPORT_DATABASE="$${DATABASE}" postgres sh -c 'PGPASSWORD="$$DB_PASSWORD" exec psql --host=127.0.0.1 --username="$$DB_USER" --dbname="$$IMPORT_DATABASE" --no-psqlrc --set=ON_ERROR_STOP=1 --command="SELECT 1;"' >/dev/null; then \
+ echo "ERROR: DB_USER cannot reconnect to PostgreSQL database $${DATABASE} after import" >&2; exit 1; \
+ fi
+ @$(MAKE) --no-print-directory check-postgres-access
+
check-mysql-access: wait-mysql
$(COMPOSE) exec -T mysql /docker-entrypoint-initdb.d/099_check_training_access.sh
@@ -474,10 +550,15 @@ clean-mysql: check-env
@$(LOAD_ENV) \
project_dir_abs="$$(realpath -m "$(PROJECT_DIR)")"; \
data_dir_abs="$$(realpath -m "$${MYSQL_DATA_DIR}")"; \
- case "$${data_dir_abs}" in "$${project_dir_abs}"/*) ;; *) echo "ERROR: MYSQL_DATA_DIR должен находиться внутри проекта: $${data_dir_abs}" >&2; exit 1 ;; esac; \
- data_dir_rel="$$(realpath --relative-to="$${project_dir_abs}" "$${data_dir_abs}")"; \
+ data_dir_rel="$${data_dir_abs#"$${project_dir_abs}"/}"; \
echo "🗑️ Удаление только данных MySQL: $${MYSQL_DATA_DIR}"; \
- $(COMPOSE_UI) down --remove-orphans; \
+ container_id="$$( $(COMPOSE) ps --all --quiet mysql )"; \
+ if [[ -n "$${container_id}" ]]; then \
+ echo "⏹️ Остановка и удаление только MySQL..."; \
+ $(COMPOSE) rm --stop --force mysql; \
+ else \
+ echo "Контейнер MySQL отсутствует; останавливать нечего."; \
+ fi; \
docker run --rm --user 0:0 --entrypoint sh \
-e DATA_DIR_REL="$${data_dir_rel}" -e HOST_UID="$(HOST_UID)" -e HOST_GID="$(HOST_GID)" \
-v "$${project_dir_abs}:/workspace" "mysql:$${MYSQL_VERSION}" \
@@ -489,10 +570,15 @@ clean-postgres: check-env
@$(LOAD_ENV) \
project_dir_abs="$$(realpath -m "$(PROJECT_DIR)")"; \
data_dir_abs="$$(realpath -m "$${POSTGRES_DATA_DIR}")"; \
- case "$${data_dir_abs}" in "$${project_dir_abs}"/*) ;; *) echo "ERROR: POSTGRES_DATA_DIR должен находиться внутри проекта: $${data_dir_abs}" >&2; exit 1 ;; esac; \
- data_dir_rel="$$(realpath --relative-to="$${project_dir_abs}" "$${data_dir_abs}")"; \
+ data_dir_rel="$${data_dir_abs#"$${project_dir_abs}"/}"; \
echo "🗑️ Удаление только данных PostgreSQL: $${POSTGRES_DATA_DIR}"; \
- $(COMPOSE_UI) down --remove-orphans; \
+ container_id="$$( $(COMPOSE) ps --all --quiet postgres )"; \
+ if [[ -n "$${container_id}" ]]; then \
+ echo "⏹️ Остановка и удаление только PostgreSQL..."; \
+ $(COMPOSE) rm --stop --force postgres; \
+ else \
+ echo "Контейнер PostgreSQL отсутствует; останавливать нечего."; \
+ fi; \
docker run --rm --user 0:0 --entrypoint sh \
-e DATA_DIR_REL="$${data_dir_rel}" -e HOST_UID="$(HOST_UID)" -e HOST_GID="$(HOST_GID)" \
-v "$${project_dir_abs}:/workspace" "postgres:$${POSTGRES_VERSION}" \
@@ -505,11 +591,8 @@ clean-all: check-env
project_dir_abs="$$(realpath -m "$(PROJECT_DIR)")"; \
mysql_abs="$$(realpath -m "$${MYSQL_DATA_DIR}")"; \
postgres_abs="$$(realpath -m "$${POSTGRES_DATA_DIR}")"; \
- for data_dir_abs in "$$mysql_abs" "$$postgres_abs"; do \
- case "$$data_dir_abs" in "$${project_dir_abs}"/*) ;; *) echo "ERROR: data-каталоги должны находиться внутри проекта: $$data_dir_abs" >&2; exit 1 ;; esac; \
- done; \
- mysql_rel="$$(realpath --relative-to="$${project_dir_abs}" "$$mysql_abs")"; \
- postgres_rel="$$(realpath --relative-to="$${project_dir_abs}" "$$postgres_abs")"; \
+ mysql_rel="$${mysql_abs#"$${project_dir_abs}"/}"; \
+ postgres_rel="$${postgres_abs#"$${project_dir_abs}"/}"; \
echo "🗑️ Удаление данных MySQL и PostgreSQL..."; \
$(COMPOSE_UI) down --remove-orphans; \
docker run --rm --user 0:0 --entrypoint sh \
@@ -528,5 +611,5 @@ reinit-postgres: clean-postgres
@$(MAKE) --no-print-directory check-postgres-access
reinit-all: clean-all
- @$(MAKE) --no-print-directory up-no-ui
+ @$(MAKE) --no-print-directory up
@$(MAKE) --no-print-directory check
diff --git a/README.md b/README.md
index a4ada5a..bdb689b 100644
--- a/README.md
+++ b/README.md
@@ -1,551 +1,235 @@
-# SQL Lab (Docker)
+
+
+
-Локальный учебный SQL-стенд на Docker Compose с независимыми сервисами:
+# Docker SQL Lab
-- MySQL 9.7.1 LTS;
-- PostgreSQL 18.4 — последняя стабильная major-ветка с пятилетним сроком
- поддержки;
-- один опциональный Adminer Docker Official Image 5.4.2 для обеих СУБД.
+## Выберите язык
-Upstream Adminer уже выпускает 5.4.4, но официальный Docker image пока
-закреплён на 5.4.2. Поэтому стенд использует точный официальный тег 5.4.2 и не
-собирает собственный образ только ради расхождения версий.
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| **Выбран** | [English](docs/langs/README_en.md) | [Español](docs/langs/README_es.md) | [中文](docs/langs/README_zh.md) | [Français](docs/langs/README_fr.md) | [Deutsch](docs/langs/README_de.md) |
-В MySQL и PostgreSQL всегда создаётся небольшая база `demo`. Для MySQL
-опционально доступны Sakila и Chinook, для PostgreSQL — Pagila и та же
-Chinook. Это позволяет сравнивать запросы к одинаковой учебной модели в двух
-СУБД.
+Локальный Docker Compose-стенд для практики SQL и знакомства с MySQL и
+PostgreSQL. Каждую СУБД можно запускать отдельно или обе вместе. Компактная
+`demo` создаётся автоматически, а необязательные учебные базы Sakila, Pagila
+и Chinook дают готовые данные для упражнений. Adminer подключается только при
+необходимости.
-## Быстрый старт
-
-Полный стенд с Adminer:
-
-```bash
-make init
-make up
-```
-
-Полный стенд без UI:
-
-```bash
-make up-no-ui
-```
-
-Одиночные режимы:
-
-```bash
-make up-mysql
-make up-mysql-ui
-make up-postgres
-make up-postgres-ui
-```
+## Скринкасты
-Adminer можно добавить к уже работающим СУБД или остановить отдельно:
+В записях используется PhpStorm. Вместо него подойдут DataGrip, DBeaver,
+Adminer или другой клиент MySQL/PostgreSQL.
-```bash
-make up-ui
-make down-ui
-```
-
-Команды одиночного запуска не останавливают уже работающие сервисы. Они лишь
-не запускают другие сервисы автоматически.
-
-## Режимы запуска
-
-| Команда | MySQL | PostgreSQL | Adminer |
+| Сценарий | Яндекс.Диск | Google Drive | Что показано |
|---|---|---|---|
-| `make up` | запускает | запускает | запускает |
-| `make up-no-ui` | запускает или оставляет активным | запускает или оставляет активным | останавливает, если запущен |
-| `make up-mysql` | запускает | не запускает автоматически | не запускает автоматически |
-| `make up-mysql-ui` | запускает | не запускает автоматически | запускает |
-| `make up-postgres` | не запускает автоматически | запускает | не запускает автоматически |
-| `make up-postgres-ui` | не запускает автоматически | запускает | запускает |
-| `make up-ui` | не меняет состояние | не меняет состояние | запускает |
-| `make down-ui` | не меняет состояние | не меняет состояние | останавливает |
+| Первый запуск с обязательной `demo`, затем добавление учебных баз | [Смотреть](https://disk.yandex.ru/i/Kj4TcMSBuIDVeA "docker-sql-lab-demo-then-training-databases.mp4") | [Смотреть](https://drive.google.com/file/d/1HzYWbMuBEobXlbGQYNfHYVAq95TLqEPf/view?usp=sharing "docker-sql-lab-demo-then-training-databases.mp4") | Запуск MySQL и PostgreSQL с обязательной `demo`; проверка; подготовка Sakila, Pagila и Chinook; подтверждённая переинициализация; повторная проверка и SQL-запросы. |
+| Первый запуск с заранее подготовленными учебными базами | [Смотреть](https://disk.yandex.ru/i/nFgJZto8agbdWw "docker-sql-lab-training-databases-first-start.mp4") | [Смотреть](https://drive.google.com/file/d/1nKiGrJ4QINLCQcRk-k6vfTakpWsw-JS7/view?usp=sharing "docker-sql-lab-training-databases-first-start.mp4") | Подготовка Sakila, Pagila и Chinook до первого запуска; единая инициализация обязательной `demo` и учебных баз; проверка доступа и SQL-запросы. |
-`make up-no-ui` не выполняет общий `docker compose down`: сначала он
-останавливает только Adminer, затем запускает или оставляет запущенными обе
-СУБД и ждёт их готовности.
+## Стек
-## Порты
+- MySQL 9.7.1 LTS
+- PostgreSQL 18.4
+- Adminer 5.4.2 Docker Official Image
+- Docker Compose v2
+- GNU Make и Bash для команд проекта и сценариев инициализации
-Порты задаются в `.docker.env`:
+Закреплённые значения по умолчанию заданы в
+[`.docker.env.example`](.docker.env.example); `make init` создаёт из него
+локальный `.docker.env`. Сервисы описаны в
+[`docker-compose.yml`](docker-compose.yml).
-| Сервис | Переменная | Значение по умолчанию |
-|---|---|---|
-| MySQL | `MYSQL_PORT` | `3306` |
-| PostgreSQL | `POSTGRES_PORT` | `5432` |
-| Adminer | `ADMINER_PORT` | `8081` |
-
-При значениях по умолчанию Adminer открыт на
-`http://127.0.0.1:8081`.
-
-## Credentials
+
+⚠️ Важно: это учебное окружение
-`.docker.env.example` содержит только локальные учебные значения:
+Проект не является готовым шаблоном для промышленной эксплуатации. Для
+внешнего использования нужны отдельные решения по учётным данным, публикации
+сервисов в сети, хранению данных, резервному копированию и эксплуатации.
-| Назначение | Пользователь | Пароль |
-|---|---|---|
-| Администратор MySQL | `MYSQL_ROOT_PASSWORD` задаёт пароль пользователя `root` | `MYSQL_ROOT_PASSWORD` |
-| Администратор PostgreSQL | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
-| Общий учебный пользователь | `DB_USER` | `DB_PASSWORD` |
+
-Административная PostgreSQL-роль и `DB_USER` обязаны различаться. Реальный
-`.docker.env` исключён из Git.
+## Основные возможности
-Значения из примера нельзя использовать в production, публичном окружении или
-на доступном извне сервере.
+- MySQL и PostgreSQL работают независимо или одновременно.
+- Обязательная `demo` в каждой СУБД содержит одинаковые начальные записи.
+- Необязательные Sakila и Chinook доступны для MySQL, Pagila и Chinook — для
+ PostgreSQL.
+- Adminer — отдельный дополнительный веб-интерфейс для обеих СУБД.
+- Каталоги данных, инициализации и учебных баз разделены по СУБД и подключены
+ в контейнеры с хоста.
+- Проверки конфигурации и доступа, импорт доверенных SQL-файлов и команды
+ очистки и переинициализации, удаляющие данные,
+ собраны в [`Makefile`](Makefile).
-## Один Adminer для двух СУБД
+## Требования
-Adminer находится в общей Compose-сети с MySQL и PostgreSQL, не зависит от их
-состояния и включается профилем `ui`. Это лёгкий optional UI с дизайном
-`nette`; без профиля он не запускается и не расходует ресурсы.
+1. Docker Engine или Docker Desktop с Docker Compose v2.
+2. GNU Make, Bash и базовые утилиты командной строки Unix, используемые
+ сценариями.
-Plugin `adminer/plugins-enabled/001-login-servers.php` заменяет свободный ввод
-движка и сервера выпадающим списком из двух допустимых подключений:
+Рекомендуемая среда: Linux; macOS с Docker Desktop; Windows с Docker Desktop
+и WSL2. Выполняйте команды из корня репозитория. Ветка проекта по умолчанию —
+`master`.
-```text
-MySQL (mysql)
-PostgreSQL (postgres)
-```
-
-Невалидный default server `db` в форме отсутствует. Пользователь выбирает
-одно подключение, затем вводит общий `DB_USER`/`DB_PASSWORD` и базу.
-
-Страница входа содержит локальную подсказку: выбрать MySQL или PostgreSQL,
-использовать значения `DB_USER`/`DB_PASSWORD` из `.docker.env` и базу `demo`.
-После отдельной подготовки samples доступны также Sakila и Chinook для MySQL,
-Pagila и Chinook для PostgreSQL.
-
-Вход в MySQL:
-
-```text
-Server: MySQL (mysql)
-Username: значение DB_USER
-Password: значение DB_PASSWORD
-Database: demo, sakila, chinook или пустое поле
-```
-
-Вход в PostgreSQL:
-
-```text
-Server: PostgreSQL (postgres)
-Username: значение DB_USER
-Password: значение DB_PASSWORD
-Database: demo, pagila или chinook, если optional sample установлен
-```
-
-Имена `mysql` и `postgres` применяются только внутри Docker-сети.
-
-## Внешние клиенты
-
-Полноценные IDE-клиенты — PhpStorm, DataGrip, DBeaver — и CLI на хосте
-подключаются к `127.0.0.1` и опубликованному порту, а не к имени
-Compose-сервиса.
-
-MySQL:
-
-```text
-Host: 127.0.0.1
-Port: значение MYSQL_PORT
-User: значение DB_USER
-Password: значение DB_PASSWORD
-Database: demo, sakila или chinook, если optional sample установлен
-```
-
-PostgreSQL:
-
-```text
-Host: 127.0.0.1
-Port: значение POSTGRES_PORT
-User: значение DB_USER
-Password: значение DB_PASSWORD
-Database: demo, pagila или chinook, если optional sample установлен
-```
-
-CLI внутри контейнеров не требует размещать пароль в shell history:
+## Быстрый старт
```bash
-make mysql
-make mysql-user
-make postgres
-make postgres-user
+make init
+make up
```
-## Учебные базы
-
-MySQL:
-
-- `demo` — обязательная база с таблицей `demo.demo_users`;
-- `sakila` — опциональная официальная учебная база;
-- `chinook` — опциональная база с музыкальным каталогом и продажами.
-
-PostgreSQL:
-
-- `demo` — обязательная база с таблицей `public.demo_users`;
-- `pagila` — опциональный PostgreSQL-порт Sakila с фильмами, актёрами,
- клиентами и прокатом;
-- `chinook` — та же модель музыкального каталога и продаж, что в MySQL.
-
-Обе таблицы `demo_users` имеют одинаковую смысловую структуру:
-
-| Поле | Назначение |
-|---|---|
-| `id` | автоматически создаваемый integer primary key |
-| `name` | `varchar(100) NOT NULL` |
-| `email` | `varchar(150) NOT NULL UNIQUE` |
-| `created_at` | обязательный timestamp с `CURRENT_TIMESTAMP` по умолчанию |
+`make init` создаёт локальный `.docker.env` из отслеживаемого
+[`.docker.env.example`](.docker.env.example), проверяет контролируемые пути
+хранения и создаёт рабочие каталоги. При первом запуске официальные точки
+входа инициализируют обе СУБД. Даже без необязательных учебных баз вы получите
+рабочие MySQL и PostgreSQL с обязательной `demo` и начальными записями.
-MySQL использует `TIMESTAMP`, PostgreSQL — `timestamptz`. В обе базы
-идемпотентно добавляются одинаковые обязательные строки:
-
-| Name | Email | Created at |
-|---|---|---|
-| Alice | `alice@example.com` | `2025-01-10 09:00:00+03` |
-| Bob | `bob@example.com` | `2025-01-11 10:15:00+03` |
-| Carol | `carol@example.com` | `2025-01-12 11:30:00+03` |
-| Dave | `dave@example.com` | `2025-01-13 12:45:00+03` |
-| Eve | `eve@example.com` | `2025-01-14 14:00:00+03` |
-
-Дополнительные пользовательские строки разрешены и не считаются ошибкой при
-проверках.
-
-Обычные `make init`, `make up`, `make up-no-ui`, `make up-mysql` и
-`make up-postgres` не скачивают и не подготавливают optional samples. Обе СУБД
-полностью работоспособны только с обязательной `demo`.
+`make up` запускает MySQL, PostgreSQL и Adminer; `make up-no-ui` — обе СУБД
+без Adminer. С настройками по умолчанию Adminer доступен по адресу
+`http://127.0.0.1:8081`.
-## Optional samples MySQL
+Подробно о запуске, подключениях и учётных данных:
+[Начало работы](docs/langs/ru/getting-started.md).
-Скачать Chinook и официальный архив Sakila и подготовить локальные SQL-файлы:
+### Нужны готовые учебные данные?
-```bash
-make samples-mysql
-```
+Учебные базы не обязательны: `demo` создаётся всегда; для MySQL доступны
+Sakila и Chinook, для PostgreSQL — Pagila и Chinook.
-Для нового пустого каталога данных:
-
-```bash
-make samples-mysql
-make up-mysql
-```
-
-Для уже инициализированного MySQL:
+**Первый запуск, каталоги данных пусты**
```bash
+make init
make samples-mysql
-make reinit-mysql CONFIRM=1
-```
-
-MySQL выполняет init-файлы только при первом запуске с пустым
-`MYSQL_DATA_DIR`. Загрузка samples не изменяет данные и не перезапускает
-контейнеры автоматически.
-
-`make samples-mysql` больше не скачивает World. Файлы сохраняются
-детерминированно:
-
-```text
-samples/mysql/
-├── .gitkeep
-├── 010_chinook.sql
-├── 020_sakila_schema.sql
-└── 021_sakila_data.sql
-```
-
-Эти загруженные SQL-файлы считаются локально сгенерированными и исключены из
-Git. `initdb/mysql/050_load_optional_samples.sh` пропускает отсутствующие
-samples, безопасно пропускает уже полную `chinook` и прекращает init с ошибкой
-при неполной/неожиданной `chinook` или неполной паре schema/data Sakila.
-
-## Optional samples PostgreSQL: Pagila и Chinook
-
-Подготовить Pagila и Chinook отдельной явной командой:
-
-```bash
make samples-postgres
+make up
```
-Для существующей локальной установки `.docker.env` не перезаписывается
-автоматически: добавьте в него вручную
-`POSTGRES_SAMPLES_DIR=./samples/postgres`.
-
-Для нового пустого каталога данных:
-
-```bash
-make samples-postgres
-make up-postgres
-```
-
-Для уже инициализированного PostgreSQL требуется явное пересоздание только его
-data-каталога:
-
-```bash
-make samples-postgres
-make reinit-postgres CONFIRM=1
-```
-
-`make samples-postgres` только скачивает и проверяет SQL обеих баз: команда не
-запускает контейнеры, не удаляет данные и не выполняет reinit. Официальный
-PostgreSQL entrypoint читает `/docker-entrypoint-initdb.d` лишь при
-инициализации пустого `POSTGRES_DATA_DIR`, поэтому добавление файлов не меняет
-существующую базу.
-
-Используется upstream
-[`devrimgunduz/pagila`](https://github.com/devrimgunduz/pagila), закреплённый
-на immutable commit
-[`5ba5a57aeb159f75f02aca2432d3c262186d13d3`](https://github.com/devrimgunduz/pagila/commit/5ba5a57aeb159f75f02aca2432d3c262186d13d3).
-Загружаются только `pagila-schema.sql` и COPY-вариант `pagila-data.sql`;
-альтернативный insert-файл не используется. Pagila распространяется по
-PostgreSQL License.
-
-Проверенная ревизия Pagila использует схему `public`, стандартные
-PL/pgSQL-функции и `COPY FROM stdin`; дополнительных extensions, пакетов или
-собственного Docker image не требуется. Подготовленные файлы сохраняются
-детерминированно:
-
-```text
-samples/postgres/
-├── 010_pagila_schema.sql
-├── 020_pagila_data.sql
-└── 030_chinook.sql
-```
+Учебные базы подготавливаются до первой инициализации; официальные точки входа
+загрузят их вместе с `demo`.
-Все файлы локальные и исключены из Git. Отсутствие пары Pagila безопасно
-пропускается, а наличие только одного файла останавливает чистую инициализацию
-с ошибкой. Chinook обрабатывается независимо: она также optional. Повторный
-загрузчик отдельно пропускает уже полные Pagila и Chinook с ожидаемым
-владельцем, но не удаляет и не исправляет автоматически неполную базу или
-неверное владение. Для этого требуется явный
-`make reinit-postgres CONFIRM=1`. Штатные команды очистки data-каталогов не
-удаляют `samples/postgres`.
-
-## Источник и безопасная подготовка Chinook
-
-Оба варианта Chinook берутся только из официального upstream
-[`lerocha/chinook-database`](https://github.com/lerocha/chinook-database) на
-immutable commit
-[`4a944a942426e1f3263fe539155fb7ef92b04b4a`](https://github.com/lerocha/chinook-database/commit/4a944a942426e1f3263fe539155fb7ef92b04b4a),
-соответствующем release `v1.4.5`. Chinook распространяется по MIT license;
-полный copyright и permission notice из закреплённого `LICENSE.md` добавляется
-SQL-комментариями в каждую подготовленную локальную копию.
-
-Upstream SQL нельзя выполнять напрямую: он содержит `DROP DATABASE`,
-`CREATE DATABASE` и выбор базы. Команды подготовки проверяют Git blob SHA,
-версию, целевую СУБД, ключевые таблицы и точный формат трёх setup-строк, затем
-удаляют только эти известные строки. Готовый SQL повторно проверяется на
-отсутствие database-level setup и публикуется атомарно вместе с остальными
-sample-файлами. Он загружается только в заранее выбранную базу `chinook`.
-
-Chinook выбрана вместо MySQL World, потому что upstream явно указывает MIT
-license и предоставляет одинаковые MySQL/PostgreSQL datasets. Существующая
-база `world` автоматически не удаляется. Чтобы убрать старую `world` и
-получить `chinook` в уже инициализированном MySQL, сначала подготовьте samples,
-затем осознанно выполните `make reinit-mysql CONFIRM=1`; эта команда удалит
-данные только MySQL.
-
-## Инициализация и порядок файлов
-
-```text
-initdb/
-├── mysql/
-│ ├── 001_demo.sql
-│ ├── 030_training_database.sql.example
-│ ├── 050_load_optional_samples.sh
-│ ├── 090_grant_training_access.sh
-│ └── 099_check_training_access.sh
-└── postgres/
- ├── 001_create_training_role.sh
- ├── 010_initialize_demo.sh
- ├── 030_training_database.sh.example
- ├── 050_load_optional_samples.sh
- └── 099_check_training_access.sh
-```
+> **Внимание:** переинициализация удаляет данные выбранной СУБД. Резервная
+> копия нужна, только если требуется сохранить собственные данные; для
+> одноразового учебного стенда без ценных изменений она не обязательна.
-Файлы `.example` служат шаблонами и не выполняются entrypoint автоматически.
-MySQL-шаблон показывает добавление новой базы перед grants. PostgreSQL-шаблон
-безопасно передаёт значения через psql variables, назначает `DB_USER`
-владельцем новой базы и создаёт начальную таблицу от его имени.
+
+📦 Стенд уже запускался: добавить или повторно использовать учебные базы
-Оба официальных entrypoint обрабатывают init-каталог только для пустого
-data-каталога. Изменение init-файлов не обновляет существующую базу.
+**Уже инициализировано без учебных баз.** Обычный `make up` не применит новые
+файлы инициализации или учебных баз. Сначала сохраните важные данные, затем
+выполните нужный вариант:
-## Конфигурация СУБД
+- MySQL: `make samples-mysql`, затем `make reinit-mysql CONFIRM=1`.
+- PostgreSQL: `make samples-postgres`, затем `make reinit-postgres CONFIRM=1`.
+- Обе СУБД: `make samples-mysql`, `make samples-postgres`, затем `make reinit-all CONFIRM=1`.
-```text
-conf/
-├── mysql/
-│ └── my.cnf
-└── postgres/
- └── postgresql.conf.example
-```
+> **Внимание:** `reinit-*` удаляет данные выбранной СУБД и выполняется только с точным `CONFIRM=1`.
-`conf/mysql/my.cnf` подключается к MySQL и фиксирует `utf8mb4`, collation
-`utf8mb4_0900_ai_ci`, строгий SQL mode с `ONLY_FULL_GROUP_BY`, часовой пояс
-`+03:00` и отключение DNS lookup клиентов. Performance-настройки оставлены
-только понятными закомментированными примерами: универсальных значений для
-лимитов памяти, соединений и slow query log нет.
-
-`conf/postgres/postgresql.conf.example` полностью закомментирован и
-автоматически к Compose не подключается. Он показывает, как ресурсы и тип
-нагрузки влияют на `shared_buffers`, `work_mem`, `maintenance_work_mem` и
-временную диагностику медленных запросов. Активного PostgreSQL performance
-tuning и отдельного logging collector нет: сервер продолжает писать в штатные
-Docker logs.
-
-## Права учебного пользователя
-
-В MySQL `090_grant_training_access.sh` создаёт или обновляет `DB_USER` и
-выдаёт ему `ALL PRIVILEGES` отдельно на каждую фактически существующую
-несистемную базу. Глобальные административные права на `*.*` не выдаются.
-
-В PostgreSQL роль `DB_USER` получает `LOGIN`, владеет обязательной базой
-`demo` и схемой `public`, но остаётся без `SUPERUSER`, `CREATEDB`,
-`CREATEROLE`, `REPLICATION` и `BYPASSRLS`. Таблица `demo_users` создаётся от
-имени этой роли. Если подготовлена Pagila, база создаётся с владельцем
-`DB_USER`, а schema и data загружаются от его имени. Закреплённый upstream dump
-содержит `OWNER TO postgres`; загрузчик безопасно заменяет эти фиксированные
-owner-выражения на quoted psql-переменную `DB_USER`, не меняя локальные
-SQL-файлы и не повышая права роли. PostgreSQL Chinook также создаётся с
-владельцем `DB_USER`; этой роли принадлежат схема `public`, таблицы и все
-созданные в ней последовательности, представления, функции и пользовательские
-типы. Пароли не хардкодятся и берутся только из environment контейнеров.
-
-## Проверки
-
-Проверка каждой СУБД отдельно:
+**Учебные базы уже установлены.** Используйте обычный `make up` или выбранную
+цель `make up-*`: повторная загрузка и переинициализация не нужны, базы
+сохраняются в каталогах хоста, подключённых в контейнеры.
-```bash
-make check-mysql-access
-make check-postgres-access
-```
+
-MySQL-проверка требует `demo.demo_users`, все пять обязательных email,
-проверяет temporary read/write и пробный откатываемый `INSERT` в `demo_users`.
-Она также проверяет доступ ко всем существующим пользовательским базам и
-`sakila.actor` только при наличии sample. Для optional Chinook отдельно
-проверяются таблицы с точным регистром `Artist`, `Album`, `Track`, `Customer`,
-`Invoice`, данные, join и откатываемая запись.
-
-PostgreSQL-проверка подключается как `DB_USER` по TCP к работающему серверу,
-проверяет владение базой и `demo_users`, все пять обязательных email, создаёт
-временную таблицу, записывает и читает строку, выполняет откатываемый `INSERT`
-в `demo_users` и подтверждает отсутствие всех административных атрибутов роли.
-Наличие Pagila и Chinook определяется независимо по фактическим базам, а не по
-sample-файлам. Для каждой установленной базы дополнительно проверяются
-владелец, ожидаемые таблицы и владельцы объектов, данные, читающий join,
-временный объект и откатываемый `INSERT` без остаточных данных. Поэтому
-поддерживаются все варианты: только `demo`, `demo + pagila`,
-`demo + chinook`, `demo + pagila + chinook`.
-
-Для полного стенда:
+Подробнее: [Базы и учебные данные](docs/langs/ru/databases.md).
-```bash
-make check
-```
+## Режимы запуска
-Команда проверяет Compose-конфигурацию и фактический доступ `DB_USER` к обеим
-СУБД.
+| Команда | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Запускает | Запускает | Запускает |
+| `make up-no-ui` | Запускает | Запускает | Останавливает |
+| `make up-mysql` | Запускает | Не запускает | Не запускает |
+| `make up-postgres` | Не запускает | Запускает | Не запускает |
-## Troubleshooting optional Chinook
+Команды одной СУБД не останавливают уже работающую другую; Adminer управляется
+отдельно. Полный набор целей описан в [`Makefile`](Makefile).
-Если загрузчик сообщает, что `chinook` уже существует, но неполна или имеет
-неожиданного владельца, он намеренно ничего не удаляет и не пытается исправить
-базу поверх существующих объектов. Проверьте, что sample подготовлен текущей
-командой `make samples-mysql` или `make samples-postgres`, сохраните нужные
-данные, затем при необходимости явно выполните reinit соответствующей СУБД с
-`CONFIRM=1`. Reinit удаляет data-каталог выбранной СУБД; обычные `make up*`
-этого не делают.
+## Подключения и доступные базы
-Если подготовленный `010_chinook.sql` или `030_chinook.sql` отклонён до
-загрузки, не запускайте raw upstream SQL вручную. Повторите подготовку и
-проверьте сеть; несовпадение Git blob SHA или формата setup-строк считается
-ошибкой безопасности.
+Внутри Compose-сети Adminer использует серверы `mysql` и `postgres`. Клиенты на
+хосте используют `127.0.0.1` и настроенные `MYSQL_PORT` или `POSTGRES_PORT`.
+Для обычной работы укажите `DB_USER` и `DB_PASSWORD`.
-## Остановка и очистка данных
+| СУБД | Доступна всегда | После инициализации необязательных учебных баз |
+|---|---|---|
+| MySQL | `demo` | `sakila`, `chinook` |
+| PostgreSQL | `demo` | `pagila`, `chinook` |
-Остановить все сервисы без удаления bind-mounted данных:
+Имена необязательных баз действительны только после их фактической инициализации.
+Подробности: [запуск и подключения](docs/langs/ru/getting-started.md) ·
+[базы и учебные данные](docs/langs/ru/databases.md).
-```bash
-make down
-```
+## Кратко об учётных данных
-Удаление данных всегда требует явного `CONFIRM=1`:
+| Назначение | Пользователь | Пароль |
+|---|---|---|
+| Общий учебный пользователь | `DB_USER` | `DB_PASSWORD` |
+| Администратор MySQL | `root` | `MYSQL_ROOT_PASSWORD` |
+| Суперпользователь PostgreSQL | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
-```bash
-make clean-mysql CONFIRM=1
-make clean-postgres CONFIRM=1
-make clean-all CONFIRM=1
-```
+`POSTGRES_SUPERUSER` и `DB_USER` должны быть разными ролями. Для упражнений
+используйте учебного пользователя и замените примерные пароли до публикации
+сервисов.
-Первые две команды удаляют только каталог соответствующей СУБД. `clean-all`
-удаляет только `data/mysql` и `data/postgres`. `.docker.env`, конфигурация,
-init-файлы, optional samples и backup сохраняются. Перед удалением проверяется,
-что data-каталог находится внутри проекта; пустой каталог возвращается
-текущему UID/GID.
+## Базы и ключевые проверки
-Чистая инициализация с последующей проверкой:
+Обе `demo` содержат эквивалентную таблицу `demo_users` с пятью строками.
+Статические проверки не требуют запущенных СУБД:
```bash
-make reinit-mysql CONFIRM=1
-make reinit-postgres CONFIRM=1
-make reinit-all CONFIRM=1
+make check-env
+make config
+make test-storage-paths
```
-Одиночные команды запускают только выбранную СУБД без Adminer. `reinit-all`
-запускает обе СУБД без Adminer и выполняет общую проверку.
-
-## Основные команды
-
-| Команда | Назначение |
-|---|---|
-| `make init` | Создать `.docker.env`, data/init/samples-каталоги и проверить скрипты |
-| `make pull` | Скачать три образа |
-| `make config` | Проверить итоговую Compose-конфигурацию |
-| `make samples-mysql` | Подготовить optional Chinook и Sakila без запуска контейнеров |
-| `make samples-postgres` | Подготовить optional Pagila и Chinook без запуска контейнеров |
-| `make status` | Показать MySQL, PostgreSQL и профильный Adminer |
-| `make logs` | Смотреть общие логи |
-| `make log postgres` | Смотреть лог выбранного сервиса (`SERVICE=postgres` также поддерживается) |
-| `make in postgres` | Открыть shell выбранного контейнера (`SERVICE=postgres` также поддерживается) |
-| `make wait-mysql` | Дождаться MySQL |
-| `make wait-postgres` | Дождаться PostgreSQL |
-| `make mysql-grants` | Повторно применить MySQL grants |
-| `make mysql-import FILE=...` | Импортировать MySQL dump, применить grants и проверить доступ |
-| `make dump` / `make restore` | Создать / восстановить backup MySQL `demo` |
-
-## Структура данных и Git
-
-Данные СУБД разделены:
-
-```text
-data/
-├── mysql/
-└── postgres/
-```
+После запуска `make check` проверяет `demo` и доступ `DB_USER`, а
+`make test-sql-imports` — публичные цели импорта доверенных SQL-файлов. Порядок
+и ограничения: [Проверки и эксплуатация](docs/langs/ru/operations.md).
-Контейнеры могут присвоить файлам числовые UID/GID своих системных
-пользователей. Не редактируйте содержимое data-каталогов вручную.
+## Безопасность и жизненный цикл
-Для официального образа PostgreSQL 18 host-каталог `data/postgres` подключён к
-`/var/lib/postgresql`; фактический versioned data directory образ создаёт
-внутри этого bind mount.
+- `BIND_ADDRESS=127.0.0.1` публикует сервисы только на интерфейсе обратной петли
+ (loopback).
+- `BIND_ADDRESS=0.0.0.0` открывает их на всех интерфейсах: заранее настройте
+ межсетевой экран, надёжные учётные данные и доверенную сеть.
+- Официальные точки входа выполняют инициализацию только для пустого каталога
+ данных.
+- `make mysql-import` и `make postgres-import` принимают только доверенный SQL.
+ Это не изолированная среда (`sandbox`): возможна частичная запись без полного
+ автоматического отката. Перед важным импортом проверьте SQL-файл и сделайте
+ подходящую резервную копию.
+- Встроенные `make dump` и `make restore` покрывают только MySQL `demo`;
+ встроенной цели резервного копирования для PostgreSQL нет.
+- Все цели `clean-*` и `reinit-*` удаляют данные и требуют точного
+ `CONFIRM=1`.
-В Git не должны попадать:
+Безопасные последовательности:
+[Проверки и эксплуатация](docs/langs/ru/operations.md).
+При ошибках сначала соберите диагностику:
+[Диагностика](docs/langs/ru/troubleshooting.md).
-```text
-.docker.env
-.env
-data/
-backup/
-.tmp/
-samples/mysql/*.sql
-samples/postgres/*.sql
-```
+## Лицензии учебных данных
-Обязательные init-скрипты и оба шаблона `.example` остаются отслеживаемыми.
+Необязательные учебные наборы данных сохраняют лицензии и уведомления об
+авторских правах и лицензиях исходных проектов. Происхождение, закреплённые
+ревизии, сведения о целостности и тексты лицензий приведены в
+[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
---
-Автор: **Александр Юрченко**
-
-Лицензия: MIT
+
+
+
+
+
+ yaleksandr89.github.io
+
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
new file mode 100644
index 0000000..9b20573
--- /dev/null
+++ b/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,119 @@
+# Third-Party Notices
+
+SQL Lab itself is licensed under the MIT License in [`LICENSE.md`](LICENSE.md).
+
+The optional sample databases described below are third-party works with their
+own license terms. Their SQL files are downloaded locally by explicit
+`make samples-*` commands and are excluded from Git. They are not relicensed
+under the SQL Lab license.
+
+## Chinook Database
+
+- Upstream: `lerocha/chinook-database`
+- Pinned revision:
+ `4a944a942426e1f3263fe539155fb7ef92b04b4a`
+- Files used:
+ - `ChinookDatabase/DataSources/Chinook_MySql.sql`
+ - `ChinookDatabase/DataSources/Chinook_PostgreSql.sql`
+ - `LICENSE.md`
+- Integrity:
+ - license Git blob:
+ `7487a9edc2d42e50d7a38ab1fbdba33ac63230f7`
+ - MySQL SQL Git blob:
+ `cdbd482f1be7fde54644480ec7c794ff2764b109`
+ - PostgreSQL SQL Git blob:
+ `d93a20d08239ac6bdd8a56601e148f5d4d048593`
+
+The complete upstream license notice is inserted into each locally prepared
+Chinook SQL file.
+
+### Chinook license notice
+
+Chinook Database
+
+Copyright (c) 2008-2024 Luis Rocha
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Pagila
+
+- Upstream: `devrimgunduz/pagila`
+- Pinned revision:
+ `5ba5a57aeb159f75f02aca2432d3c262186d13d3`
+- Files used:
+ - `pagila-schema.sql`
+ - `pagila-data.sql`
+ - `LICENSE.txt`
+- Integrity:
+ - license Git blob:
+ `c6078c708c6f55b56e24f3687c591ca12df567ca`
+ - schema Git blob:
+ `23718a3adef90ced002e19ad4e1ac98d22aa5870`
+ - data Git blob:
+ `b7c016861fd0f84008645153c6a1d9e5a99b9cc6`
+
+The pinned upstream README describes Pagila as being available under the
+PostgreSQL License, while the pinned `LICENSE.txt` contains the permission text
+reproduced below. SQL Lab preserves that exact upstream text without assigning
+a different SPDX identifier. The complete notice is inserted into both locally
+prepared Pagila SQL files.
+
+### Pagila license notice
+
+Copyright (c) Devrim Gündüz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Sakila Sample Database
+
+- Official archive:
+ `https://downloads.mysql.com/docs/sakila-db.zip`
+- Verified archive SHA-256:
+ `c2ecb3dec28d752241ccfca02974ba970de3c3fc5d98887fd3f9d5843f946672`
+- Files used:
+ - `sakila-schema.sql`
+ - `sakila-data.sql`
+- Official license reference:
+ `https://dev.mysql.com/doc/sakila/en/sakila-license.html`
+
+MySQL states that `sakila-schema.sql` and `sakila-data.sql` are licensed under
+the New BSD license. Other materials in the distribution are not covered by
+that open license.
+
+SQL Lab extracts only those two SQL files. It does not publish the Sakila
+documentation or `sakila.mwb`. Before extraction, the archive SHA-256 is
+verified. Both SQL files must contain the expected New BSD notice and
+disclaimer, and they are copied without modifying their embedded copyright and
+license text.
diff --git a/docker-compose.yml b/docker-compose.yml
index 76ab6bf..909ca3b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,7 +1,6 @@
services:
mysql:
image: mysql:${MYSQL_VERSION}
- container_name: ${MYSQL_CONTAINER}
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
@@ -10,10 +9,10 @@ services:
DB_PASSWORD: ${DB_PASSWORD}
TZ: Europe/Moscow
ports:
- - "${MYSQL_PORT}:3306"
+ - "${BIND_ADDRESS:-127.0.0.1}:${MYSQL_PORT}:3306"
volumes:
- "${MYSQL_DATA_DIR}:/var/lib/mysql"
- - "${MYSQL_CONF_FILE}:/etc/mysql/conf.d/z-stepik.cnf:ro"
+ - "${MYSQL_CONF_FILE}:/etc/mysql/conf.d/sql-lab.cnf:ro"
- "${MYSQL_INITDB_DIR}:/docker-entrypoint-initdb.d:ro"
- "${MYSQL_SAMPLES_DIR}:/opt/mysql-samples:ro"
healthcheck:
@@ -24,7 +23,6 @@ services:
postgres:
image: postgres:${POSTGRES_VERSION}
- container_name: ${POSTGRES_CONTAINER}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_SUPERUSER}
@@ -34,7 +32,7 @@ services:
DB_PASSWORD: ${DB_PASSWORD}
TZ: Europe/Moscow
ports:
- - "${POSTGRES_PORT}:5432"
+ - "${BIND_ADDRESS:-127.0.0.1}:${POSTGRES_PORT}:5432"
volumes:
- "${POSTGRES_DATA_DIR}:/var/lib/postgresql"
- "${POSTGRES_INITDB_DIR}:/docker-entrypoint-initdb.d:ro"
@@ -47,7 +45,6 @@ services:
adminer:
image: adminer:${ADMINER_VERSION}
- container_name: ${ADMINER_CONTAINER}
restart: unless-stopped
profiles:
- ui
@@ -55,7 +52,7 @@ services:
ADMINER_DESIGN: nette
TZ: Europe/Moscow
ports:
- - "${ADMINER_PORT}:8080"
+ - "${BIND_ADDRESS:-127.0.0.1}:${ADMINER_PORT}:8080"
volumes:
- "./adminer/plugins-enabled/001-login-servers.php:/var/www/html/plugins-enabled/001-login-servers.php:ro"
- "./adminer/plugins-enabled/002-login-help.php:/var/www/html/plugins-enabled/002-login-help.php:ro"
diff --git a/docs/assets/docker-sql-lab-cover.png b/docs/assets/docker-sql-lab-cover.png
new file mode 100644
index 0000000..190165f
Binary files /dev/null and b/docs/assets/docker-sql-lab-cover.png differ
diff --git a/docs/assets/ya-logo-dark-50px.png b/docs/assets/ya-logo-dark-50px.png
new file mode 100644
index 0000000..e0c821f
Binary files /dev/null and b/docs/assets/ya-logo-dark-50px.png differ
diff --git a/docs/langs/README_de.md b/docs/langs/README_de.md
new file mode 100644
index 0000000..8c7696c
--- /dev/null
+++ b/docs/langs/README_de.md
@@ -0,0 +1,218 @@
+
+
+
+
+# Docker SQL Lab
+
+## Sprache auswählen
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../../README.md) | [English](README_en.md) | [Español](README_es.md) | [中文](README_zh.md) | [Français](README_fr.md) | **Ausgewählt** |
+
+Eine lokale Docker-Compose-Umgebung zum Üben von SQL sowie zum Kennenlernen
+und Vergleichen von MySQL und PostgreSQL. Beide DBMS lassen sich einzeln oder
+gemeinsam starten. Die kompakte `demo`-Datenbank entsteht automatisch;
+optionale Sakila-, Pagila- und Chinook-Datensätze liefern sofort abfragbare
+Übungsdaten. Adminer wird nur bei Bedarf aktiviert.
+
+## Bildschirmaufzeichnungen
+
+Die Aufzeichnungen verwenden PhpStorm; DataGrip, DBeaver, Adminer oder ein
+anderer MySQL-/PostgreSQL-Client eignen sich ebenfalls. Sie wurden auf Russisch aufgenommen.
+
+| Szenario | Yandex Disk | Google Drive | Gezeigt wird |
+|---|---|---|---|
+| Erster Start mit obligatorischem `demo`, danach Übungsdatenbanken hinzufügen | [Ansehen](https://disk.yandex.ru/i/Kj4TcMSBuIDVeA "docker-sql-lab-demo-then-training-databases.mp4") | [Ansehen](https://drive.google.com/file/d/1HzYWbMuBEobXlbGQYNfHYVAq95TLqEPf/view?usp=sharing "docker-sql-lab-demo-then-training-databases.mp4") | Startet MySQL und PostgreSQL mit obligatorischem `demo`; prüft; bereitet Sakila, Pagila und Chinook vor; bestätigt die Neuinitialisierung; prüft erneut und führt SQL-Abfragen aus. |
+| Erster Start mit vorab vorbereiteten Übungsdatenbanken | [Ansehen](https://disk.yandex.ru/i/nFgJZto8agbdWw "docker-sql-lab-training-databases-first-start.mp4") | [Ansehen](https://drive.google.com/file/d/1nKiGrJ4QINLCQcRk-k6vfTakpWsw-JS7/view?usp=sharing "docker-sql-lab-training-databases-first-start.mp4") | Bereitet Sakila, Pagila und Chinook vor dem ersten Start vor; initialisiert obligatorisches `demo` und Übungsdatenbanken zusammen; prüft Zugriff und führt SQL-Abfragen aus. |
+
+## Stack und festgelegte Versionen
+
+- MySQL 9.7.1 LTS
+- PostgreSQL 18.4
+- Adminer 5.4.2 Docker Official Image
+- Docker Compose v2
+- GNU Make und Bash für Projektbefehle und Initialisierungsskripte
+
+Die festgelegten Standardwerte stehen in
+[`.docker.env.example`](../../.docker.env.example); `make init` erstellt daraus
+die lokale `.docker.env`. Die Services stehen in
+[`docker-compose.yml`](../../docker-compose.yml).
+
+
+⚠️ Wichtig: Dies ist eine Lernumgebung
+
+Das Projekt ist kein produktionsfertiges Template. Für externe Nutzung sind
+eigene Entscheidungen zu Zugangsdaten, Netzwerkfreigabe, Storage, Backups und
+Betrieb nötig.
+
+
+
+## Hauptfunktionen
+
+- MySQL und PostgreSQL laufen einzeln oder gemeinsam.
+- Jedes DBMS hat eine obligatorische `demo` mit denselben Seed-Datensätzen.
+- Optional: Sakila und Chinook für MySQL, Pagila und Chinook für PostgreSQL.
+- Adminer ist eine unabhängige optionale Oberfläche für beide DBMS.
+- Daten-, Init- und Sample-Verzeichnisse sind je DBMS getrennt eingebunden.
+- Prüfungen, vertrauenswürdige SQL-Imports und destructive Aktionen bündelt das
+ [`Makefile`](../../Makefile).
+
+## Voraussetzungen
+
+1. Docker Engine oder Docker Desktop mit Docker Compose v2.
+2. GNU Make, Bash und die von den Skripten verwendeten Unix-CLI-Basiswerkzeuge.
+
+Empfohlen: Linux; macOS mit Docker Desktop; Windows mit Docker Desktop und
+WSL2. Befehle werden im Repository-Stamm ausgeführt. Der Standardbranch ist
+`master`.
+
+## Schnellstart
+
+```bash
+make init
+make up
+```
+
+`make init` erzeugt die lokale `.docker.env` aus
+[`.docker.env.example`](../../.docker.env.example), prüft verwaltete Pfade und
+legt Arbeitsverzeichnisse an. Beim ersten Containerstart initialisieren die
+offiziellen Entrypoints beide DBMS. Auch ohne optionale Samples erhalten Sie
+MySQL und PostgreSQL mit `demo` und Seed-Datensätzen.
+
+`make up` startet MySQL, PostgreSQL und Adminer; `make up-no-ui` startet beide
+DBMS ohne Adminer. Standardmäßig ist Adminer unter
+`http://127.0.0.1:8081` erreichbar.
+
+Startmodi, Verbindungen und Zugangsdaten:
+[Erste Schritte](de/getting-started.md).
+
+### Fertige Übungsdaten gewünscht?
+
+Samples sind optional: `demo` wird immer erstellt; Sakila und Chinook sind für MySQL verfügbar, Pagila und Chinook für PostgreSQL.
+
+**Erster Start mit leeren Datenverzeichnissen**
+
+```bash
+make init
+make samples-mysql
+make samples-postgres
+make up
+```
+
+Bereiten Sie Samples vor der ersten Initialisierung vor; die Entrypoints laden sie zusammen mit `demo`.
+
+> **Warnung:** Die Neuinitialisierung löscht die Daten des gewählten DBMS.
+> Sichern Sie nur eigene Daten, die erhalten bleiben sollen; ein einmaliges Lab ohne wertvolle Änderungen braucht kein Backup.
+
+
+📦 Das Lab lief bereits: Samples hinzufügen oder erneut verwenden
+
+**Ohne Samples initialisiert.** `make up` wendet neue Init-/Sample-Dateien nicht an. Wenn wichtige Daten erhalten bleiben sollen, sichern Sie sie und wählen dann die passende Variante:
+
+- MySQL: `make samples-mysql`, danach `make reinit-mysql CONFIRM=1`.
+- PostgreSQL: `make samples-postgres`, danach `make reinit-postgres CONFIRM=1`.
+- Beide DBMS: `make samples-mysql`, `make samples-postgres`, danach `make reinit-all CONFIRM=1`.
+
+> **Warnung:** `reinit-*` löscht die gewählten Daten und läuft nur mit dem exakten `CONFIRM=1`.
+
+**Samples bereits installiert.** Nutzen Sie `make up` oder das gewünschte `make up-*`: Erneuter download und reinit entfallen; die Datenbanken bleiben im bind-mounted storage erhalten.
+
+
+
+Details: [Datenbanken und Samples](de/databases.md).
+
+## Startmodi
+
+| Befehl | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Startet | Startet | Startet |
+| `make up-no-ui` | Startet | Startet | Stoppt |
+| `make up-mysql` | Startet | Startet nicht | Startet nicht |
+| `make up-postgres` | Startet nicht | Startet | Startet nicht |
+
+Ein Einzel-DBMS-Befehl stoppt das andere nicht; Adminer wird separat verwaltet.
+Alle Targets stehen im [`Makefile`](../../Makefile).
+
+## Verbindungen und verfügbare Datenbanken
+
+Im Compose-Netz nutzt Adminer `mysql` und `postgres`. Host-Clients nutzen
+`127.0.0.1` und `MYSQL_PORT` oder `POSTGRES_PORT`. Für Übungen dienen
+`DB_USER` und `DB_PASSWORD`.
+
+| DBMS | Immer verfügbar | Nach optionaler Sample-Initialisierung |
+|---|---|---|
+| MySQL | `demo` | `sakila`, `chinook` |
+| PostgreSQL | `demo` | `pagila`, `chinook` |
+
+Optionale Datenbanken existieren erst nach der tatsächlichen Initialisierung.
+Details: [Start und Verbindungen](de/getting-started.md) ·
+[Datenbanken und Samples](de/databases.md).
+
+## Zugangsdaten im Überblick
+
+| Zweck | Benutzer | Passwort |
+|---|---|---|
+| Gemeinsamer Lernbenutzer | `DB_USER` | `DB_PASSWORD` |
+| MySQL-Administrator | `root` | `MYSQL_ROOT_PASSWORD` |
+| PostgreSQL-Superuser | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` und `DB_USER` müssen verschieden sein. Verwenden Sie für
+Übungen den Lernbenutzer und ersetzen Sie Beispielpasswörter vor Freigaben.
+
+## Datenbanken und wichtige Prüfungen
+
+Beide `demo`-Datenbanken enthalten eine äquivalente Tabelle `demo_users` mit
+fünf Zeilen. Diese Prüfungen brauchen keine laufenden DBMS:
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+Nach dem Start prüft `make check` die `demo` und den Zugriff von `DB_USER`;
+`make test-sql-imports` testet die öffentlichen Trusted-Import-Targets.
+Reihenfolge und Grenzen:
+[Prüfungen und Betrieb](de/operations.md).
+
+## Sicherheit und Lebenszyklus
+
+- `BIND_ADDRESS=127.0.0.1` veröffentlicht nur auf Loopback.
+- `BIND_ADDRESS=0.0.0.0` öffnet alle Interfaces; richten Sie vorher Firewall,
+ starke Zugangsdaten und ein vertrauenswürdiges Netz ein.
+- Offizielle Entrypoints führen Init nur bei leeren Daten aus.
+- `make mysql-import` und `make postgres-import` akzeptieren nur
+ vertrauenswürdiges SQL. Sie sind keine Sandbox: partielle Ausführung ohne
+ vollständigen automatischen Rollback ist möglich.
+ Prüfen Sie vor einem wichtigen Import die SQL-Datei und erstellen Sie ein geeignetes Backup.
+- `make dump` und `make restore` sichern nur MySQL `demo`; ein eingebautes
+ PostgreSQL-Backup-Target fehlt.
+- Alle `clean-*`- und `reinit-*`-Befehle sind destructive und verlangen exakt
+ `CONFIRM=1`.
+
+Sichere Abläufe: [Prüfungen und Betrieb](de/operations.md). Bei Fehlern zuerst
+Diagnosedaten sammeln:
+[Diagnose und Fehlerbehebung](de/troubleshooting.md).
+
+## Lizenzen der Übungsdaten
+
+Optionale Datensätze behalten Lizenzen und Hinweise ihrer Upstream-Projekte.
+Herkunft, festgelegte Revisionen, Integrität und Lizenztexte stehen in
+[`THIRD_PARTY_NOTICES.md`](../../THIRD_PARTY_NOTICES.md).
+
+
+
+
+
+
+ yaleksandr89.github.io
+
diff --git a/docs/langs/README_en.md b/docs/langs/README_en.md
new file mode 100644
index 0000000..df88aaa
--- /dev/null
+++ b/docs/langs/README_en.md
@@ -0,0 +1,220 @@
+
+
+
+
+# Docker SQL Lab
+
+## Choose a language
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../../README.md) | **Selected** | [Español](README_es.md) | [中文](README_zh.md) | [Français](README_fr.md) | [Deutsch](README_de.md) |
+
+A local Docker Compose lab for practicing SQL and exploring and comparing
+MySQL and PostgreSQL. Run either DBMS independently or both together. A compact
+`demo` database is created automatically, while optional Sakila, Pagila, and
+Chinook datasets provide ready-to-query training data. Enable Adminer only
+when you need it.
+
+## Screencasts
+
+The recordings use PhpStorm. DataGrip, DBeaver, Adminer, or another
+MySQL/PostgreSQL client will work instead. The screencasts are recorded in Russian.
+
+| Scenario | Yandex Disk | Google Drive | What it shows |
+|---|---|---|---|
+| First start with required `demo`, then add training databases | [Watch](https://disk.yandex.ru/i/Kj4TcMSBuIDVeA "docker-sql-lab-demo-then-training-databases.mp4") | [Watch](https://drive.google.com/file/d/1HzYWbMuBEobXlbGQYNfHYVAq95TLqEPf/view?usp=sharing "docker-sql-lab-demo-then-training-databases.mp4") | Start MySQL and PostgreSQL with required `demo`; check them; prepare Sakila, Pagila, and Chinook; confirm reinitialization; check again and run SQL queries. |
+| First start with training databases prepared in advance | [Watch](https://disk.yandex.ru/i/nFgJZto8agbdWw "docker-sql-lab-training-databases-first-start.mp4") | [Watch](https://drive.google.com/file/d/1nKiGrJ4QINLCQcRk-k6vfTakpWsw-JS7/view?usp=sharing "docker-sql-lab-training-databases-first-start.mp4") | Prepare Sakila, Pagila, and Chinook before the first start; initialize required `demo` and the training databases together; check access and run SQL queries. |
+
+## Stack
+
+- MySQL 9.7.1 LTS
+- PostgreSQL 18.4
+- Adminer 5.4.2 Docker Official Image
+- Docker Compose v2
+- GNU Make and Bash for project commands and initialization scripts
+
+Pinned defaults are defined in
+[`.docker.env.example`](../../.docker.env.example); `make init` creates the
+local `.docker.env` from it. Services are defined in
+[`docker-compose.yml`](../../docker-compose.yml).
+
+
+⚠️ Important: this is a training environment
+
+This project is not a production-ready template. External use requires
+separate decisions about credentials, network exposure, storage, backups, and
+operations.
+
+
+
+## Features
+
+- MySQL and PostgreSQL run independently or together.
+- A required `demo` database in each DBMS contains equivalent seed rows.
+- Optional Sakila and Chinook are available for MySQL; Pagila and Chinook for
+ PostgreSQL.
+- Adminer is a separate optional UI shared by both DBMSs.
+- Each DBMS has separate bind-mounted data, init, and sample directories.
+- Configuration and access checks, trusted SQL imports, and destructive
+ commands are collected in the [`Makefile`](../../Makefile).
+
+## Requirements
+
+1. Docker Engine or Docker Desktop with Docker Compose v2.
+2. GNU Make, Bash, and the basic Unix CLI utilities used by the scripts.
+
+Recommended environments: Linux; macOS with Docker Desktop; Windows with
+Docker Desktop and WSL2. Run commands from the repository root. The project's
+default branch is `master`.
+
+## Quick start
+
+```bash
+make init
+make up
+```
+
+`make init` creates the local `.docker.env` from the tracked
+[`.docker.env.example`](../../.docker.env.example), validates managed paths,
+and creates working directories. On the first container start, the official
+entrypoints initialize both DBMSs. Even without optional samples, you get
+working MySQL and PostgreSQL instances with the required `demo` database and
+seed rows.
+
+`make up` starts MySQL, PostgreSQL, and Adminer; `make up-no-ui` starts both
+DBMSs without Adminer. With the defaults, Adminer is available at
+`http://127.0.0.1:8081`.
+
+For startup modes, connections, and credentials, see
+[Getting started](en/getting-started.md).
+
+### Want ready-made training data?
+
+Optional samples are not required: `demo` is always created; MySQL supports Sakila and Chinook, while PostgreSQL supports Pagila and Chinook.
+
+**First start with empty data directories**
+
+```bash
+make init
+make samples-mysql
+make samples-postgres
+make up
+```
+
+Prepare samples before the first initialization; the official entrypoints load them alongside `demo`.
+
+> **Warning:** reinitialization deletes the selected DBMS data. Back up only
+> custom data you need to keep; a one-off lab with no valuable changes needs no backup.
+
+
+📦 The lab has run before: add or reuse samples
+
+**Initialized without samples.** A regular `make up` does not apply new init/sample files. If you need to preserve important data, back it up, then use the appropriate option:
+
+- MySQL: `make samples-mysql`, then `make reinit-mysql CONFIRM=1`.
+- PostgreSQL: `make samples-postgres`, then `make reinit-postgres CONFIRM=1`.
+- Both DBMSs: `make samples-mysql`, `make samples-postgres`, then `make reinit-all CONFIRM=1`.
+
+> **Warning:** `reinit-*` deletes the selected DBMS data and runs only with the exact `CONFIRM=1`.
+
+**Samples already installed.** Use the regular `make up` or a selected `make up-*`: no repeated download or reinitialization is needed, and databases persist in bind-mounted storage.
+
+
+
+Details: [Databases and samples](en/databases.md).
+
+## Startup modes
+
+| Command | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Starts | Starts | Starts |
+| `make up-no-ui` | Starts | Starts | Stops |
+| `make up-mysql` | Starts | Does not start | Does not start |
+| `make up-postgres` | Does not start | Starts | Does not start |
+
+A single-DBMS command does not stop the other running DBMS; Adminer is managed
+separately. The complete target list is in the [`Makefile`](../../Makefile).
+
+## Connections and available databases
+
+Inside the Compose network, Adminer uses `mysql` and `postgres`. Host clients
+use `127.0.0.1` with the configured `MYSQL_PORT` or `POSTGRES_PORT`. Sign in
+for routine work with `DB_USER` and `DB_PASSWORD`.
+
+| DBMS | Always available | After optional sample initialization |
+|---|---|---|
+| MySQL | `demo` | `sakila`, `chinook` |
+| PostgreSQL | `demo` | `pagila`, `chinook` |
+
+Optional database names are valid only after actual initialization. Details:
+[startup and connections](en/getting-started.md) ·
+[databases and samples](en/databases.md).
+
+## Credentials at a glance
+
+| Purpose | User | Password |
+|---|---|---|
+| Shared training user | `DB_USER` | `DB_PASSWORD` |
+| MySQL administrator | `root` | `MYSQL_ROOT_PASSWORD` |
+| PostgreSQL superuser | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` and `DB_USER` must be different roles. Use the training
+user for exercises, and replace example passwords before publishing services.
+
+## Databases and key checks
+
+Both `demo` databases contain an equivalent `demo_users` table with five rows.
+These static checks do not require running DBMSs:
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+After startup, `make check` verifies `demo` and `DB_USER` access;
+`make test-sql-imports` exercises the public trusted import targets. For the
+safe order and limitations, see
+[Validation and operations](en/operations.md).
+
+## Safety and lifecycle
+
+- `BIND_ADDRESS=127.0.0.1` publishes services only on loopback.
+- `BIND_ADDRESS=0.0.0.0` exposes them on every interface; configure the
+ firewall, strong credentials, and network trust first.
+- Official entrypoints run init only for an empty data directory.
+- `make mysql-import` and `make postgres-import` accept trusted SQL only.
+ This is not a sandbox: partial execution is possible without a guaranteed
+ full automatic rollback.
+ Review the SQL file and create a suitable backup before an important import.
+- Built-in `make dump` and `make restore` cover only MySQL `demo`; there is no
+ built-in PostgreSQL backup target.
+- Every `clean-*` and `reinit-*` command is destructive and requires the exact
+ `CONFIRM=1`.
+
+Safe sequences: [Validation and operations](en/operations.md). For failures,
+collect evidence first: [Troubleshooting](en/troubleshooting.md).
+
+## Training data licenses
+
+Optional sample datasets retain the licenses and notices of their upstream
+projects. Provenance, pinned revisions, integrity information, and license
+texts are in
+[`THIRD_PARTY_NOTICES.md`](../../THIRD_PARTY_NOTICES.md).
+
+
+
+
+
+
+ yaleksandr89.github.io
+
diff --git a/docs/langs/README_es.md b/docs/langs/README_es.md
new file mode 100644
index 0000000..c660f48
--- /dev/null
+++ b/docs/langs/README_es.md
@@ -0,0 +1,214 @@
+
+
+
+
+# Docker SQL Lab
+
+## Elija un idioma
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../../README.md) | [English](README_en.md) | **Seleccionado** | [中文](README_zh.md) | [Français](README_fr.md) | [Deutsch](README_de.md) |
+
+Un laboratorio local con Docker Compose para practicar SQL y conocer y comparar
+MySQL y PostgreSQL. Puede iniciar cada SGBD por separado o ambos a la vez.
+La base compacta `demo` se crea automáticamente; los datasets opcionales
+Sakila, Pagila y Chinook ofrecen datos listos para consultar. Active Adminer
+solo cuando lo necesite.
+
+## Videotutoriales
+
+Las grabaciones usan PhpStorm; también sirven DataGrip, DBeaver, Adminer u otro
+cliente MySQL/PostgreSQL. Los screencasts están grabados en ruso.
+
+| Escenario | Yandex Disk | Google Drive | Qué muestra |
+|---|---|---|---|
+| Primer inicio con `demo` obligatoria y posterior adición de bases didácticas | [Ver](https://disk.yandex.ru/i/Kj4TcMSBuIDVeA "docker-sql-lab-demo-then-training-databases.mp4") | [Ver](https://drive.google.com/file/d/1HzYWbMuBEobXlbGQYNfHYVAq95TLqEPf/view?usp=sharing "docker-sql-lab-demo-then-training-databases.mp4") | Inicia MySQL y PostgreSQL con `demo` obligatoria; comprueba; prepara Sakila, Pagila y Chinook; confirma la reinicialización; vuelve a comprobar y ejecuta consultas SQL. |
+| Primer inicio con las bases didácticas preparadas de antemano | [Ver](https://disk.yandex.ru/i/nFgJZto8agbdWw "docker-sql-lab-training-databases-first-start.mp4") | [Ver](https://drive.google.com/file/d/1nKiGrJ4QINLCQcRk-k6vfTakpWsw-JS7/view?usp=sharing "docker-sql-lab-training-databases-first-start.mp4") | Prepara Sakila, Pagila y Chinook antes del primer inicio; inicializa juntas `demo` obligatoria y las bases didácticas; comprueba el acceso y ejecuta consultas SQL. |
+
+## Stack
+
+- MySQL 9.7.1 LTS
+- PostgreSQL 18.4
+- Adminer 5.4.2 Docker Official Image
+- Docker Compose v2
+- GNU Make y Bash para los comandos y scripts de inicialización
+
+Los valores predeterminados fijados se definen en
+[`.docker.env.example`](../../.docker.env.example); `make init` crea desde él
+el `.docker.env` local. Los servicios se definen en
+[`docker-compose.yml`](../../docker-compose.yml).
+
+
+⚠️ Importante: este es un entorno didáctico
+
+El proyecto no es una plantilla lista para producción. El uso externo exige
+decisiones propias sobre credenciales, exposición de red, almacenamiento,
+backups y operación.
+
+
+
+## Funciones principales
+
+- MySQL y PostgreSQL funcionan por separado o juntos.
+- Cada SGBD incluye una base `demo` obligatoria con los mismos seed rows.
+- MySQL admite Sakila y Chinook opcionales; PostgreSQL, Pagila y Chinook.
+- Adminer es una interfaz opcional e independiente para ambos SGBD.
+- Cada SGBD tiene directorios bind-mounted separados para datos, init y samples.
+- Las comprobaciones, imports SQL de confianza y acciones destructivas se
+ centralizan en el [`Makefile`](../../Makefile).
+
+## Requisitos
+
+1. Docker Engine o Docker Desktop con Docker Compose v2.
+2. GNU Make, Bash y las utilidades Unix CLI básicas usadas por los scripts.
+
+Entornos recomendados: Linux; macOS con Docker Desktop; Windows con Docker
+Desktop y WSL2. Ejecute los comandos desde la raíz del repositorio. La rama
+predeterminada del proyecto es `master`.
+
+## Inicio rápido
+
+```bash
+make init
+make up
+```
+
+`make init` crea el `.docker.env` local desde
+[`.docker.env.example`](../../.docker.env.example), valida las rutas gestionadas
+y crea los directorios de trabajo. En el primer arranque, los entrypoints
+oficiales inicializan ambos SGBD. Sin samples opcionales seguirá teniendo MySQL
+y PostgreSQL operativos con `demo` y sus seed rows.
+
+`make up` inicia MySQL, PostgreSQL y Adminer; `make up-no-ui` inicia ambos SGBD
+sin Adminer. Por defecto, Adminer está en `http://127.0.0.1:8081`.
+
+Modos, conexiones y credenciales:
+[Primeros pasos](es/getting-started.md).
+
+### ¿Necesita datos didácticos preparados?
+
+Los samples son opcionales: `demo` se crea siempre; MySQL ofrece Sakila y Chinook, y PostgreSQL, Pagila y Chinook.
+
+**Primer arranque con directorios de datos vacíos**
+
+```bash
+make init
+make samples-mysql
+make samples-postgres
+make up
+```
+
+Prepare los samples antes de la primera inicialización; los entrypoints los cargarán junto con `demo`.
+
+> **Advertencia:** la reinicialización elimina los datos del SGBD elegido. Haga
+> backup solo de los datos propios que quiera conservar; un laboratorio puntual sin cambios valiosos no lo necesita.
+
+
+📦 El laboratorio ya se inició: añadir o reutilizar samples
+
+**Inicializado sin samples.** `make up` no aplica nuevos archivos init/sample. Si necesita conservar datos importantes, haga backup y use la opción adecuada:
+
+- MySQL: `make samples-mysql` y después `make reinit-mysql CONFIRM=1`.
+- PostgreSQL: `make samples-postgres` y después `make reinit-postgres CONFIRM=1`.
+- Ambos SGBD: `make samples-mysql`, `make samples-postgres` y después `make reinit-all CONFIRM=1`.
+
+> **Advertencia:** `reinit-*` elimina los datos seleccionados y solo se ejecuta con `CONFIRM=1` exacto.
+
+**Samples ya instalados.** Use `make up` o el `make up-*` elegido: no repita download ni reinit; las bases persisten en el almacenamiento bind-mounted.
+
+
+
+Detalles: [Bases y samples](es/databases.md).
+
+## Modos de inicio
+
+| Comando | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Inicia | Inicia | Inicia |
+| `make up-no-ui` | Inicia | Inicia | Detiene |
+| `make up-mysql` | Inicia | No inicia | No inicia |
+| `make up-postgres` | No inicia | Inicia | No inicia |
+
+Un comando de un solo SGBD no detiene al otro; Adminer se gestiona aparte. La
+lista completa está en el [`Makefile`](../../Makefile).
+
+## Conexiones y bases disponibles
+
+Dentro de Compose, Adminer usa `mysql` y `postgres`. Los clientes del host usan
+`127.0.0.1` y `MYSQL_PORT` o `POSTGRES_PORT`. Para el trabajo habitual, use
+`DB_USER` y `DB_PASSWORD`.
+
+| SGBD | Siempre disponible | Tras inicializar samples opcionales |
+|---|---|---|
+| MySQL | `demo` | `sakila`, `chinook` |
+| PostgreSQL | `demo` | `pagila`, `chinook` |
+
+Las bases opcionales solo existen tras su inicialización real. Detalles:
+[inicio y conexiones](es/getting-started.md) ·
+[bases y samples](es/databases.md).
+
+## Credenciales resumidas
+
+| Finalidad | Usuario | Contraseña |
+|---|---|---|
+| Usuario didáctico común | `DB_USER` | `DB_PASSWORD` |
+| Administrador MySQL | `root` | `MYSQL_ROOT_PASSWORD` |
+| Superusuario PostgreSQL | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` y `DB_USER` deben ser roles distintos. Use el usuario
+didáctico para ejercicios y cambie las contraseñas de ejemplo antes de publicar.
+
+## Bases y comprobaciones clave
+
+Ambas `demo` contienen una tabla `demo_users` equivalente con cinco filas.
+Estas comprobaciones no requieren SGBD en ejecución:
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+Tras el arranque, `make check` valida `demo` y el acceso de `DB_USER`;
+`make test-sql-imports` prueba los imports públicos de confianza. Orden y
+límites: [Comprobaciones y operaciones](es/operations.md).
+
+## Seguridad y ciclo de vida
+
+- `BIND_ADDRESS=127.0.0.1` publica solo en loopback.
+- `BIND_ADDRESS=0.0.0.0` expone todos los interfaces; configure antes firewall,
+ credenciales robustas y una red de confianza.
+- Los entrypoints oficiales ejecutan init solo con datos vacíos.
+- `make mysql-import` y `make postgres-import` aceptan únicamente SQL de
+ confianza. No son un sandbox: puede haber ejecución parcial sin rollback
+ automático completo.
+ Antes de un import importante, revise el archivo SQL y cree un backup adecuado.
+- `make dump` y `make restore` cubren solo `demo` de MySQL; no hay target de
+ backup integrado para PostgreSQL.
+- Cada comando `clean-*` y `reinit-*` es destructivo y exige `CONFIRM=1` exacto.
+
+Secuencias seguras: [Comprobaciones y operaciones](es/operations.md).
+Ante fallos, reúna primero diagnósticos: [Diagnóstico](es/troubleshooting.md).
+
+## Licencias de los datos didácticos
+
+Los datasets opcionales conservan las licencias y avisos de sus proyectos
+upstream. Procedencia, revisiones fijadas, integridad y textos están en
+[`THIRD_PARTY_NOTICES.md`](../../THIRD_PARTY_NOTICES.md).
+
+
+
+
+
+
+ yaleksandr89.github.io
+
diff --git a/docs/langs/README_fr.md b/docs/langs/README_fr.md
new file mode 100644
index 0000000..d653abe
--- /dev/null
+++ b/docs/langs/README_fr.md
@@ -0,0 +1,215 @@
+
+
+
+
+# Docker SQL Lab
+
+## Choisissez une langue
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../../README.md) | [English](README_en.md) | [Español](README_es.md) | [中文](README_zh.md) | **Sélectionné** | [Deutsch](README_de.md) |
+
+Un environnement Docker Compose local pour pratiquer SQL, découvrir et
+comparer MySQL et PostgreSQL. Lancez chaque SGBD séparément ou les deux
+ensemble. La petite base `demo` est créée automatiquement ; les datasets
+optionnels Sakila, Pagila et Chinook offrent des données prêtes à interroger.
+Activez Adminer uniquement au besoin.
+
+## Vidéos de démonstration
+
+Les enregistrements utilisent PhpStorm ; DataGrip, DBeaver, Adminer ou un autre
+client MySQL/PostgreSQL conviennent aussi. Les screencasts sont enregistrés en russe.
+
+| Scénario | Yandex Disk | Google Drive | Contenu |
+|---|---|---|---|
+| Premier démarrage avec `demo` obligatoire, puis ajout des bases d’entraînement | [Voir](https://disk.yandex.ru/i/Kj4TcMSBuIDVeA "docker-sql-lab-demo-then-training-databases.mp4") | [Voir](https://drive.google.com/file/d/1HzYWbMuBEobXlbGQYNfHYVAq95TLqEPf/view?usp=sharing "docker-sql-lab-demo-then-training-databases.mp4") | Démarre MySQL et PostgreSQL avec `demo` obligatoire ; vérifie ; prépare Sakila, Pagila et Chinook ; confirme la réinitialisation ; vérifie de nouveau et exécute des requêtes SQL. |
+| Premier démarrage avec les bases d’entraînement préparées à l’avance | [Voir](https://disk.yandex.ru/i/nFgJZto8agbdWw "docker-sql-lab-training-databases-first-start.mp4") | [Voir](https://drive.google.com/file/d/1nKiGrJ4QINLCQcRk-k6vfTakpWsw-JS7/view?usp=sharing "docker-sql-lab-training-databases-first-start.mp4") | Prépare Sakila, Pagila et Chinook avant le premier démarrage ; initialise ensemble `demo` obligatoire et les bases d’entraînement ; vérifie l’accès et exécute des requêtes SQL. |
+
+## Stack et versions épinglées
+
+- MySQL 9.7.1 LTS
+- PostgreSQL 18.4
+- Adminer 5.4.2 Docker Official Image
+- Docker Compose v2
+- GNU Make et Bash pour les commandes et scripts d'initialisation
+
+Les valeurs par défaut épinglées sont définies dans
+[`.docker.env.example`](../../.docker.env.example) ; `make init` crée à partir
+de ce fichier le `.docker.env` local. Les services sont définis dans
+[`docker-compose.yml`](../../docker-compose.yml).
+
+
+⚠️ Important : environnement de formation
+
+Ce projet n'est pas un template prêt pour la production. Un usage externe
+nécessite des choix dédiés pour les identifiants, l'exposition réseau, le
+stockage, les sauvegardes et l'exploitation.
+
+
+
+## Fonctionnalités principales
+
+- MySQL et PostgreSQL fonctionnent séparément ou ensemble.
+- Chaque SGBD possède une base `demo` obligatoire avec les mêmes seed rows.
+- MySQL propose Sakila et Chinook en option ; PostgreSQL, Pagila et Chinook.
+- Adminer est une interface optionnelle et indépendante pour les deux SGBD.
+- Données, init et samples ont des bind mounts distincts pour chaque SGBD.
+- Contrôles, imports SQL de confiance et actions destructives sont regroupés
+ dans le [`Makefile`](../../Makefile).
+
+## Prérequis
+
+1. Docker Engine ou Docker Desktop avec Docker Compose v2.
+2. GNU Make, Bash et les utilitaires Unix CLI de base utilisés par les scripts.
+
+Environnements conseillés : Linux ; macOS avec Docker Desktop ; Windows avec
+Docker Desktop et WSL2. Exécutez les commandes depuis la racine du dépôt. La
+branche par défaut du projet est `master`.
+
+## Démarrage rapide
+
+```bash
+make init
+make up
+```
+
+`make init` crée le `.docker.env` local depuis
+[`.docker.env.example`](../../.docker.env.example), valide les chemins gérés et
+crée les répertoires de travail. Au premier démarrage, les entrypoints officiels
+initialisent les deux SGBD. Sans samples optionnels, MySQL et PostgreSQL restent
+opérationnels avec `demo` et ses seed rows.
+
+`make up` lance MySQL, PostgreSQL et Adminer ; `make up-no-ui` lance les deux
+SGBD sans Adminer. Par défaut, Adminer répond sur `http://127.0.0.1:8081`.
+
+Modes, connexions et identifiants :
+[Prise en main](fr/getting-started.md).
+
+### Besoin de données d'entraînement prêtes à l'emploi ?
+
+Les samples sont optionnels : `demo` est toujours créée ; Sakila et Chinook sont proposés pour MySQL, Pagila et Chinook pour PostgreSQL.
+
+**Premier démarrage avec data vide**
+
+```bash
+make init
+make samples-mysql
+make samples-postgres
+make up
+```
+
+Préparez les samples avant la première initialisation ; les entrypoints les chargeront avec `demo`.
+
+> **Attention :** la réinitialisation supprime les données du SGBD choisi. Ne
+> sauvegardez que les données personnelles à conserver ; un lab ponctuel sans changement précieux n’exige pas de sauvegarde.
+
+
+📦 Le laboratoire a déjà démarré : ajouter ou réutiliser les samples
+
+**Déjà initialisé sans samples.** `make up` n'applique pas les nouveaux fichiers init/sample. Si vous devez conserver des données importantes, sauvegardez-les, puis choisissez l'option adaptée :
+
+- MySQL : `make samples-mysql`, puis `make reinit-mysql CONFIRM=1`.
+- PostgreSQL : `make samples-postgres`, puis `make reinit-postgres CONFIRM=1`.
+- Les deux SGBD : `make samples-mysql`, `make samples-postgres`, puis `make reinit-all CONFIRM=1`.
+
+> **Attention :** `reinit-*` supprime les données sélectionnées et exige exactement `CONFIRM=1`.
+
+**Samples déjà installés.** Utilisez `make up` ou le `make up-*` choisi : aucun nouveau download ni reinit ; les bases persistent dans le stockage bind-mounted.
+
+
+
+Détails : [Bases et samples](fr/databases.md).
+
+## Modes de démarrage
+
+| Commande | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Lance | Lance | Lance |
+| `make up-no-ui` | Lance | Lance | Arrête |
+| `make up-mysql` | Lance | Ne lance pas | Ne lance pas |
+| `make up-postgres` | Ne lance pas | Lance | Ne lance pas |
+
+Une commande mono-SGBD n'arrête pas l'autre ; Adminer se gère séparément. La
+liste complète figure dans le [`Makefile`](../../Makefile).
+
+## Connexions et bases disponibles
+
+Dans le réseau Compose, Adminer utilise `mysql` et `postgres`. Les clients de
+l'hôte utilisent `127.0.0.1` et `MYSQL_PORT` ou `POSTGRES_PORT`. Pour les
+exercices, utilisez `DB_USER` et `DB_PASSWORD`.
+
+| SGBD | Toujours disponible | Après initialisation des samples |
+|---|---|---|
+| MySQL | `demo` | `sakila`, `chinook` |
+| PostgreSQL | `demo` | `pagila`, `chinook` |
+
+Les bases optionnelles n'existent qu'après leur initialisation effective.
+Détails : [démarrage et connexions](fr/getting-started.md) ·
+[bases et samples](fr/databases.md).
+
+## Identifiants en bref
+
+| Usage | Utilisateur | Mot de passe |
+|---|---|---|
+| Utilisateur de formation commun | `DB_USER` | `DB_PASSWORD` |
+| Administrateur MySQL | `root` | `MYSQL_ROOT_PASSWORD` |
+| Superuser PostgreSQL | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` et `DB_USER` doivent être distincts. Utilisez le compte de
+formation et remplacez les mots de passe d'exemple avant toute publication.
+
+## Bases et contrôles essentiels
+
+Les deux `demo` contiennent une table `demo_users` équivalente de cinq lignes.
+Ces contrôles ne nécessitent aucun SGBD lancé :
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+Après démarrage, `make check` vérifie `demo` et l'accès de `DB_USER` ;
+`make test-sql-imports` teste les imports publics de confiance. Ordre et
+limites : [Contrôles et opérations](fr/operations.md).
+
+## Sécurité et cycle de vie
+
+- `BIND_ADDRESS=127.0.0.1` publie uniquement sur loopback.
+- `BIND_ADDRESS=0.0.0.0` expose toutes les interfaces ; configurez d'abord
+ firewall, identifiants robustes et réseau de confiance.
+- Les entrypoints officiels exécutent init uniquement sur des données vides.
+- `make mysql-import` et `make postgres-import` n'acceptent que du SQL de
+ confiance. Ce n'est pas un sandbox : une exécution partielle sans rollback
+ automatique complet reste possible.
+ Avant un import important, vérifiez le fichier SQL et créez un backup adapté.
+- `make dump` et `make restore` couvrent seulement `demo` MySQL ; aucun target
+ de backup PostgreSQL n'est intégré.
+- Tout `clean-*` et `reinit-*` est destructif et exige exactement `CONFIRM=1`.
+
+Séquences sûres : [Contrôles et opérations](fr/operations.md). En cas d'échec,
+collectez d'abord le diagnostic :
+[Diagnostic et dépannage](fr/troubleshooting.md).
+
+## Licences des données d'entraînement
+
+Les datasets optionnels conservent les licences et notices de leurs projets
+upstream. Provenance, révisions épinglées, intégrité et textes figurent dans
+[`THIRD_PARTY_NOTICES.md`](../../THIRD_PARTY_NOTICES.md).
+
+
+
+
+
+
+ yaleksandr89.github.io
+
diff --git a/docs/langs/README_zh.md b/docs/langs/README_zh.md
new file mode 100644
index 0000000..2fecfa5
--- /dev/null
+++ b/docs/langs/README_zh.md
@@ -0,0 +1,211 @@
+
+
+
+
+# Docker SQL Lab
+
+## 选择语言
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../../README.md) | [English](README_en.md) | [Español](README_es.md) | **已选择** | [Français](README_fr.md) | [Deutsch](README_de.md) |
+
+用于练习 SQL、了解并比较 MySQL 与 PostgreSQL 的本地 Docker Compose
+实验环境。两种数据库既可单独启动,也可同时运行。精简的 `demo`
+数据库会自动创建;可选的 Sakila、Pagila 和 Chinook 数据集提供开箱即用
+的查询练习数据。仅在需要时启用 Adminer。
+
+## 录屏演示
+
+录屏使用 PhpStorm;也可使用 DataGrip、DBeaver、Adminer 或其他
+MySQL/PostgreSQL 客户端。录屏以俄语录制。
+
+| 场景 | Yandex Disk | Google Drive | 演示内容 |
+|---|---|---|---|
+| 首次启动必需的 `demo`,随后添加练习数据库 | [观看](https://disk.yandex.ru/i/Kj4TcMSBuIDVeA "docker-sql-lab-demo-then-training-databases.mp4") | [观看](https://drive.google.com/file/d/1HzYWbMuBEobXlbGQYNfHYVAq95TLqEPf/view?usp=sharing "docker-sql-lab-demo-then-training-databases.mp4") | 使用必需的 `demo` 启动 MySQL 和 PostgreSQL;检查;准备 Sakila、Pagila 和 Chinook;确认重新初始化;再次检查并执行 SQL 查询。 |
+| 首次启动前已准备好练习数据库 | [观看](https://disk.yandex.ru/i/nFgJZto8agbdWw "docker-sql-lab-training-databases-first-start.mp4") | [观看](https://drive.google.com/file/d/1nKiGrJ4QINLCQcRk-k6vfTakpWsw-JS7/view?usp=sharing "docker-sql-lab-training-databases-first-start.mp4") | 在首次启动前准备 Sakila、Pagila 和 Chinook;一起初始化必需的 `demo` 和练习数据库;检查访问并执行 SQL 查询。 |
+
+## 技术栈与固定版本
+
+- MySQL 9.7.1 LTS
+- PostgreSQL 18.4
+- Adminer 5.4.2 Docker Official Image
+- Docker Compose v2
+- GNU Make 与 Bash,用于项目命令和初始化脚本
+
+固定的默认值定义在
+[`.docker.env.example`](../../.docker.env.example);`make init` 会据此创建
+本地 `.docker.env`。服务定义在
+[`docker-compose.yml`](../../docker-compose.yml)。
+
+
+⚠️ 重要:这是学习环境
+
+本项目不是可直接用于 production 的模板。对外使用前,必须另行设计
+credentials、网络暴露、存储、备份与运维方案。
+
+
+
+## 主要功能
+
+- MySQL 与 PostgreSQL 可单独运行,也可同时运行。
+- 每种数据库都有必需的 `demo`,并包含相同的 seed rows。
+- MySQL 可选 Sakila 和 Chinook;PostgreSQL 可选 Pagila 和 Chinook。
+- Adminer 是供两种数据库共用、独立启用的可选界面。
+- 两种数据库分别使用 bind-mounted data、init 与 sample 目录。
+- 配置与访问检查、可信 SQL 导入和 destructive 操作集中在
+ [`Makefile`](../../Makefile)。
+
+## 要求
+
+1. Docker Engine 或带有 Docker Compose v2 的 Docker Desktop。
+2. GNU Make、Bash 以及脚本使用的基础 Unix CLI 工具。
+
+推荐环境:Linux;装有 Docker Desktop 的 macOS;Docker Desktop +
+WSL2 的 Windows。所有命令都从仓库根目录执行。项目默认分支为
+`master`。
+
+## 快速开始
+
+```bash
+make init
+make up
+```
+
+`make init` 从受版本控制的
+[`.docker.env.example`](../../.docker.env.example) 创建本地 `.docker.env`,
+检查 managed paths 并创建工作目录。首次启动容器时,官方 entrypoints
+会初始化两种数据库。即使不安装可选 samples,MySQL 与 PostgreSQL
+仍可正常使用,并都包含必需的 `demo` 与 seed rows。
+
+`make up` 启动 MySQL、PostgreSQL 和 Adminer;`make up-no-ui` 只启动两种
+数据库。默认可通过 `http://127.0.0.1:8081` 访问 Adminer。
+
+启动模式、连接与凭据详见:[入门](zh/getting-started.md)。
+
+### 需要现成的练习数据吗?
+
+Samples 并非必需:`demo` 始终创建;MySQL 支持 Sakila 和 Chinook,PostgreSQL 支持 Pagila 和 Chinook。
+
+**首次启动,data 目录为空**
+
+```bash
+make init
+make samples-mysql
+make samples-postgres
+make up
+```
+
+请在首次初始化前准备 samples;官方 entrypoints 会将它们与 `demo` 一起载入。
+
+> **警告:** 重新初始化会删除所选数据库的数据。只有需要保留自己的数据时
+> 才需备份;没有重要改动的一次性学习环境不要求备份。
+
+
+📦 环境已启动过:添加或继续使用 samples
+
+**已在没有 samples 时初始化。** 普通的 `make up` 不会应用新增的 init/sample 文件。若需保留重要数据,请先备份,再选择对应方式:
+
+- MySQL:`make samples-mysql`,然后 `make reinit-mysql CONFIRM=1`。
+- PostgreSQL:`make samples-postgres`,然后 `make reinit-postgres CONFIRM=1`。
+- 两种数据库:`make samples-mysql`、`make samples-postgres`,然后 `make reinit-all CONFIRM=1`。
+
+> **警告:** `reinit-*` 会删除所选数据库的数据,且只有准确提供 `CONFIRM=1` 才会执行。
+
+**Samples 已安装。** 直接使用 `make up` 或所选的 `make up-*`:无需重复 download 或 reinit,数据库会保留在 bind-mounted storage 中。
+
+
+
+详见:[数据库与 samples](zh/databases.md)。
+
+## 启动模式
+
+| 命令 | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | 启动 | 启动 | 启动 |
+| `make up-no-ui` | 启动 | 启动 | 停止 |
+| `make up-mysql` | 启动 | 不启动 | 不启动 |
+| `make up-postgres` | 不启动 | 启动 | 不启动 |
+
+单数据库命令不会停止另一个已运行的数据库;Adminer 单独管理。完整
+targets 列表见 [`Makefile`](../../Makefile)。
+
+## 连接与可用数据库
+
+在 Compose 网络内,Adminer 使用 `mysql` 和 `postgres`。宿主机客户端
+使用 `127.0.0.1` 以及配置的 `MYSQL_PORT` 或 `POSTGRES_PORT`。日常练习
+使用 `DB_USER` 和 `DB_PASSWORD`。
+
+| 数据库 | 始终可用 | 可选 samples 初始化后 |
+|---|---|---|
+| MySQL | `demo` | `sakila`、`chinook` |
+| PostgreSQL | `demo` | `pagila`、`chinook` |
+
+可选数据库只有在实际初始化后才存在。详见:
+[启动与连接](zh/getting-started.md) ·
+[数据库与 samples](zh/databases.md)。
+
+## 凭据概览
+
+| 用途 | 用户 | 密码 |
+|---|---|---|
+| 共用学习用户 | `DB_USER` | `DB_PASSWORD` |
+| MySQL 管理员 | `root` | `MYSQL_ROOT_PASSWORD` |
+| PostgreSQL superuser | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` 与 `DB_USER` 必须是不同角色。练习时使用学习用户;
+对外发布服务前请替换示例密码。
+
+## 数据库与关键检查
+
+两种 `demo` 都包含等价的 `demo_users` 表和五行数据。以下静态检查
+无需启动数据库:
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+启动后,`make check` 检查 `demo` 与 `DB_USER` 访问;
+`make test-sql-imports` 测试公开的可信导入 targets。顺序与限制详见:
+[检查与运维](zh/operations.md)。
+
+## 安全与生命周期
+
+- `BIND_ADDRESS=127.0.0.1` 仅在 loopback 上发布服务。
+- `BIND_ADDRESS=0.0.0.0` 会暴露所有网络接口;请先配置 firewall、
+ 强凭据与可信网络。
+- 官方 entrypoints 仅在 data 为空时执行 init。
+- `make mysql-import` 与 `make postgres-import` 只接受可信 SQL。它们不是
+ sandbox:可能部分执行,且不保证完整自动 rollback。
+ 重要导入前,请检查 SQL 文件并创建合适的 backup。
+- 内置 `make dump` 与 `make restore` 只覆盖 MySQL `demo`;没有内置
+ PostgreSQL backup target。
+- 所有 `clean-*` 与 `reinit-*` 都是 destructive 操作,并且必须准确提供
+ `CONFIRM=1`。
+
+安全操作顺序:[检查与运维](zh/operations.md)。出现问题时先收集诊断:
+[诊断与故障排除](zh/troubleshooting.md)。
+
+## 练习数据许可证
+
+可选数据集保留各 upstream 项目的许可证与 notices。来源、固定
+revision、完整性信息与许可证文本见
+[`THIRD_PARTY_NOTICES.md`](../../THIRD_PARTY_NOTICES.md)。
+
+
+
+
+
+
+ yaleksandr89.github.io
+
diff --git a/docs/langs/de/databases.md b/docs/langs/de/databases.md
new file mode 100644
index 0000000..9f57b60
--- /dev/null
+++ b/docs/langs/de/databases.md
@@ -0,0 +1,125 @@
+# Datenbanken und Samples
+
+[← Zurück zur README](../README_de.md)
+
+## Sprache
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/databases.md) | [English](../en/databases.md) | [Español](../es/databases.md) | [中文](../zh/databases.md) | [Français](../fr/databases.md) | **Ausgewählt** |
+
+## Abschnitt
+
+| Erste Schritte | Datenbanken und Samples | Prüfungen und Betrieb | Diagnose und Fehlerbehebung |
+| --- | --- | --- | --- |
+| [Erste Schritte](getting-started.md) | **Ausgewählt** | [Prüfungen und Betrieb](operations.md) | [Diagnose und Fehlerbehebung](troubleshooting.md) |
+
+
+## Obligatorische `demo`-Datenbanken
+
+Beide DBMS initialisieren `demo`:
+
+- MySQL: `demo.demo_users`
+- PostgreSQL: `demo.public.demo_users`
+
+Die Tabellen besitzen entsprechende Felder `id`, `name`, `email` und
+`created_at` sowie Alice, Bob, Carol, Dave und Eve. Zusätzliche Zeilen sind
+zulässig. `make check-env` verlangt `MYSQL_DATABASE=demo` und
+`POSTGRES_DATABASE=demo`.
+
+
+## Optionale Samples
+
+| DBMS | Optionale Datenbanken | Vorbereitung |
+|---|---|---|
+| MySQL | `chinook`, `sakila` | `make samples-mysql` |
+| PostgreSQL | `pagila`, `chinook` | `make samples-postgres` |
+
+
+## Samples vorbereiten
+
+Die Vorbereitung benötigt `curl` und `git`; MySQL-Samples erfordern zusätzlich `unzip` und `sha256sum`.
+
+Die Vorbereitung lädt festgelegte upstream-Dateien herunter und prüft sie,
+startet aber keine Container und importiert nichts in bereits initialisierte
+Datenbanken. Temporäre Downloads bleiben lokal, werden nicht committed und
+liegen unter `MYSQL_SAMPLES_DIR` beziehungsweise `POSTGRES_SAMPLES_DIR`.
+Herkunft, Integritätswerte und Lizenzen stehen in
+[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md).
+
+Wann die Vorbereitung erfolgen muss und wie ein vorhandenes DBMS neu
+initialisiert wird, steht unter
+[Initialisierung und Lebenszyklus](#section-initialization).
+
+Ein vollständig fehlendes Sample wird übersprungen und verhindert `demo`
+nicht. Unvollständige Samples oder unerwartete Datenbanken werden ohne
+automatische Reparatur oder Löschung abgelehnt.
+
+
+## Speicherstruktur
+
+```text
+data/
+├── mysql/
+└── postgres/
+
+initdb/
+├── mysql/
+└── postgres/
+```
+
+| DBMS | Data | Init | Optionale Samples |
+|---|---|---|---|
+| MySQL | `MYSQL_DATA_DIR` (`./data/mysql`) | `MYSQL_INITDB_DIR` (`./initdb/mysql`) | `MYSQL_SAMPLES_DIR` (`./samples/mysql`) |
+| PostgreSQL | `POSTGRES_DATA_DIR` (`./data/postgres`) | `POSTGRES_INITDB_DIR` (`./initdb/postgres`) | `POSTGRES_SAMPLES_DIR` (`./samples/postgres`) |
+
+Data- und Sample-Pfade sind über `.docker.env` konfigurierbar und werden als
+managed paths validiert. Die Regeln für init-Verzeichnisse stehen unter
+[Initialisierung und Lebenszyklus](#section-initialization). Bearbeiten Sie
+Dateien in `data/` nicht manuell; sie können numerischen Container-UID/GID
+gehören.
+
+
+## Initialisierung und Lebenszyklus
+
+> **Wichtig:** Offizielle MySQL- und PostgreSQL-Entrypoints führen init-Dateien
+> nur bei leerem data directory aus. Nach der Initialisierung hinzugefügte
+> Dateien ändern keine bestehende Datenbank. `make down` erhält die Daten; eine
+> bestätigte Neuinitialisierung löscht dagegen die aktuellen Daten des gewählten
+> DBMS und erstellt die Datenbanken aus den aktuellen Init-Skripten neu. Ein
+> Backup ist nur nötig, wenn eigene Daten erhalten bleiben sollen; ein einmaliges
+> Lab ohne wertvolle Änderungen braucht keines.
+
+Bereiten Sie Samples für die erste Initialisierung vor dem ersten Start vor:
+
+```bash
+make samples-mysql
+make up-mysql
+
+make samples-postgres
+make up-postgres
+```
+
+Sichern Sie bei einem initialisierten DBMS eigene Daten bei Bedarf und verwenden
+Sie dann nur die passende bestätigte Neuinitialisierung:
+
+```bash
+make samples-mysql
+make reinit-mysql CONFIRM=1
+
+make samples-postgres
+make reinit-postgres CONFIRM=1
+```
+
+
+## Lernzugriff und Ownership
+
+MySQL erstellt `DB_USER` und gewährt Zugriff auf alle beim init
+gefundenen Nicht-Systemdatenbanken. PostgreSQL erstellt einen getrennten
+`DB_USER` ohne superuser/createdb/createrole und macht ihn zum Eigentümer von
+`demo`, dem `public`-Schema und geladenen Sample-Objekten. Administrative
+credentials bleiben getrennt: `MYSQL_ROOT_PASSWORD`, `POSTGRES_SUPERUSER`
+und `POSTGRES_SUPERUSER_PASSWORD`. Bearbeiten Sie container-owned Dateien in
+`data/` nicht manuell.
+
+[Zurück zur README](../README_de.md)
diff --git a/docs/langs/de/getting-started.md b/docs/langs/de/getting-started.md
new file mode 100644
index 0000000..1adbbf0
--- /dev/null
+++ b/docs/langs/de/getting-started.md
@@ -0,0 +1,167 @@
+# Erste Schritte
+
+[← Zurück zur README](../README_de.md)
+
+## Sprache
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/getting-started.md) | [English](../en/getting-started.md) | [Español](../es/getting-started.md) | [中文](../zh/getting-started.md) | [Français](../fr/getting-started.md) | **Ausgewählt** |
+
+## Abschnitt
+
+| Erste Schritte | Datenbanken und Samples | Prüfungen und Betrieb | Diagnose und Fehlerbehebung |
+| --- | --- | --- | --- |
+| **Ausgewählt** | [Datenbanken und Samples](databases.md) | [Prüfungen und Betrieb](operations.md) | [Diagnose und Fehlerbehebung](troubleshooting.md) |
+
+
+## Voraussetzungen
+
+- Docker Engine oder Docker Desktop mit Docker Compose v2.
+- GNU Make, Bash und grundlegende Unix-Kommandozeilenwerkzeuge.
+- Empfohlene Umgebungen: Linux; macOS mit Docker Desktop; oder Windows mit
+ Docker Desktop und WSL2.
+
+Führen Sie Befehle im Repository-Stamm aus. Der Standardbranch des Projekts ist
+`master`.
+
+
+## Schnellstart
+
+`make init` erstellt eine lokale `.docker.env` aus
+[`.docker.env.example`](../../../.docker.env.example), prüft die verwalteten
+Speicherpfade und legt die Arbeitsverzeichnisse an. Starten Sie danach das
+vollständige Lab:
+
+```bash
+make init
+make up
+```
+
+`make up` startet MySQL, PostgreSQL und Adminer. Mit der Standardkonfiguration
+ist Adminer unter `http://127.0.0.1:8081` erreichbar.
+
+Mit `make up-no-ui` starten Sie beide DBMS ohne Adminer. Die obligatorische
+`demo`-Datenbank wird bei der ersten Initialisierung immer erstellt;
+Beispieldatensätze sind optional. Um sie bei der ersten Initialisierung zu
+laden, bereiten Sie sie vor dem ersten `make up` vor. Bei bereits
+initialisierten Datenverzeichnissen erstellen Sie ein Backup vor einer
+bestätigten Neuinitialisierung nur, wenn eigene Daten erhalten bleiben sollen. Das genaue Verfahren steht
+unter [Initialisierung und Lebenszyklus](databases.md#section-initialization).
+
+```bash
+make status
+make logs
+make down
+```
+
+`make down` entfernt Container und Netzwerk, behält aber bind-mounted Daten.
+
+
+## Startmodi
+
+| Befehl | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Startet | Startet | Startet |
+| `make up-no-ui` | Startet | Startet | Stoppt |
+| `make up-mysql` | Startet | Startet nicht | Startet nicht |
+| `make up-postgres` | Startet nicht | Startet | Startet nicht |
+
+Einzel-DBMS-Befehle stoppen das andere laufende DBMS nicht; Adminer wird separat verwaltet.
+
+
+📋 Vollständige Tabelle der Startmodi
+
+| Befehl | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Startet | Startet | Startet |
+| `make up-no-ui` | Startet oder lässt laufen | Startet oder lässt laufen | Stoppt, falls aktiv |
+| `make up-mysql` | Startet | Startet nicht automatisch | Startet nicht automatisch |
+| `make up-mysql-ui` | Startet | Startet nicht automatisch | Startet |
+| `make up-postgres` | Startet nicht automatisch | Startet | Startet nicht automatisch |
+| `make up-postgres-ui` | Startet nicht automatisch | Startet | Startet |
+| `make up-ui` | Ändert nichts | Ändert nichts | Startet |
+| `make down-ui` | Ändert nichts | Ändert nichts | Stoppt |
+
+Einzel-DBMS-Befehle stoppen ein bereits laufendes anderes DBMS nicht. Adminer
+lässt sich separat starten und stoppen und ist nicht nur an MySQL gebunden.
+
+
+
+
+## Verbindungen
+
+### Adminer
+
+Im Compose-Netzwerk bietet Adminer zwei vordefinierte Server an:
+
+```text
+MySQL (mysql)
+PostgreSQL (postgres)
+```
+
+Wählen Sie einen Server und melden Sie sich mit `DB_USER`, `DB_PASSWORD` und
+einer Datenbank wie `demo` an. `mysql` und `postgres` sind interne
+Compose-Dienstnamen, keine Hostnamen für Desktop-Clients.
+
+### Clients auf dem Host
+
+| DBMS | Standardhost | Portvariable | Benutzer | Standarddatenbank |
+|---|---|---|---|---|
+| MySQL | `127.0.0.1` | `MYSQL_PORT` | `DB_USER` | `demo` |
+| PostgreSQL | `127.0.0.1` | `POSTGRES_PORT` | `DB_USER` | `demo` |
+
+DataGrip, DBeaver, PhpStorm und Host-CLI-Werkzeuge nutzen veröffentlichte
+Adresse und Port. Nach einer Änderung von `BIND_ADDRESS` verwenden Sie bei
+Bedarf die erreichbare Adresse dieser Schnittstelle.
+
+### CLI in den Containern
+
+Passwörter werden über die Containerumgebung übergeben und nicht in die
+Shell-History geschrieben:
+
+```bash
+make mysql # MySQL-Administrator, Datenbank demo
+make mysql-user # DB_USER, Datenbank demo
+make postgres # PostgreSQL-Superuser, Datenbank demo
+make postgres-user # DB_USER, Datenbank demo
+```
+
+
+## Zugangsdaten
+
+`.docker.env` wird aus [`.docker.env.example`](../../../.docker.env.example) erstellt und von Git ignoriert.
+Bewahren Sie Passwörter dort auf; codieren Sie sie nicht in versioniertem
+Compose, SQL oder Client-Konfigurationen.
+
+| Zweck | Benutzer | Passwort |
+|---|---|---|
+| Gemeinsamer Lernbenutzer | `DB_USER` | `DB_PASSWORD` |
+| MySQL-Administrator | `root` | `MYSQL_ROOT_PASSWORD` |
+| PostgreSQL-Administrator/Superuser | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` und `DB_USER` müssen verschiedene Rollen sein. Verwenden
+Sie den Lernbenutzer für normale Übungen. Ersetzen Sie Beispielpasswörter,
+bevor Dienste geteilt oder außerhalb des lokalen Rechners veröffentlicht werden.
+
+
+## Ports und `BIND_ADDRESS`
+
+| Dienst | Portvariable | Beispielwert |
+|---|---|---|
+| MySQL | `MYSQL_PORT` | `3306` |
+| PostgreSQL | `POSTGRES_PORT` | `5432` |
+| Adminer | `ADMINER_PORT` | `8081` |
+
+Standardmäßig werden alle drei Dienste nur an loopback gebunden:
+
+```env
+BIND_ADDRESS=127.0.0.1
+```
+
+`127.0.0.1` ist der lokale Standard. `BIND_ADDRESS=0.0.0.0` veröffentlicht die
+Ports auf allen Netzwerkschnittstellen. Für LAN oder VPN ist die Adresse einer
+bestimmten Schnittstelle vorzuziehen. Ändern Sie die Bindung bewusst und
+beachten Sie Firewall, Passwortstärke und Vertrauenswürdigkeit des Netzes.
+
+[Zurück zur README](../README_de.md)
diff --git a/docs/langs/de/operations.md b/docs/langs/de/operations.md
new file mode 100644
index 0000000..6591a69
--- /dev/null
+++ b/docs/langs/de/operations.md
@@ -0,0 +1,163 @@
+# Prüfungen und Betrieb
+
+[← Zurück zur README](../README_de.md)
+
+## Sprache
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/operations.md) | [English](../en/operations.md) | [Español](../es/operations.md) | [中文](../zh/operations.md) | [Français](../fr/operations.md) | **Ausgewählt** |
+
+## Abschnitt
+
+| Erste Schritte | Datenbanken und Samples | Prüfungen und Betrieb | Diagnose und Fehlerbehebung |
+| --- | --- | --- | --- |
+| [Erste Schritte](getting-started.md) | [Datenbanken und Samples](databases.md) | **Ausgewählt** | [Diagnose und Fehlerbehebung](troubleshooting.md) |
+
+
+## Öffentliche Make targets
+
+Öffentliche Targets und ihre Implementierung stehen im [`Makefile`](../../../Makefile).
+
+Wichtige Targets sind `make init`, `make up`, `make down`, `make check`,
+`make test-storage-paths`, `make test-sql-imports`, `make mysql-import`
+und `make postgres-import`. Vertrauenswürdiges SQL ist keine isolierte Umgebung (`sandbox`), kann
+partiell ausgeführt werden und erhält keine Garantie für automatic rollback;
+erstellen Sie vor wichtigen Imports ein Backup. Integrierte backup targets
+decken nur MySQL `demo` ab, nicht PostgreSQL. `clean-*` und `reinit-*`
+sind destruktiv und erfordern exakt `CONFIRM=1`.
+
+
+📋 Vollständige Referenz der öffentlichen Make targets
+
+`make help` zeigt die kompakte Liste.
+
+| Befehl | Zweck |
+|---|---|
+| `make init` | `.docker.env` erstellen, managed paths prüfen, Verzeichnisse anlegen |
+| `make check-env` | Env-Werte, Rollen, Datenbanknamen und Pfade prüfen |
+| `make pull` | Die drei festgelegten Images laden |
+| `make config` | Erweiterte Compose-Konfiguration validieren |
+| `make up`, `make up-no-ui` | Beide DBMS mit oder ohne Adminer starten |
+| `make up-mysql`, `make up-mysql-ui` | MySQL, optional mit Adminer, starten |
+| `make up-postgres`, `make up-postgres-ui` | PostgreSQL, optional mit Adminer, starten |
+| `make up-ui`, `make down-ui` | Nur Adminer starten oder stoppen |
+| `make down` | Lab stoppen, ohne bind-mounted Daten zu löschen |
+| `make status`, `make logs` | Status anzeigen oder alle Logs verfolgen |
+| `make log SERVICE=postgres` | Einen Dienst verfolgen; auch `make log postgres` |
+| `make in SERVICE=postgres` | Dienst-Shell öffnen; auch `make in postgres` |
+| `make mysql`, `make mysql-user` | MySQL als Admin oder `DB_USER` öffnen |
+| `make postgres`, `make postgres-user` | PostgreSQL als Superuser oder `DB_USER` öffnen |
+| `make samples-mysql`, `make samples-postgres` | Geprüfte Samples vorbereiten |
+| `make check-mysql-access`, `make check-postgres-access` | Ein laufendes DBMS prüfen |
+| `make check` | Compose und `DB_USER`-Zugriff auf beide DBMS prüfen |
+| `make test-storage-paths` | Managed paths ohne Docker runtime testen |
+| `make test-sql-imports` | Smoke-Test beider öffentlicher SQL-Imports |
+| `make mysql-import FILE=... DATABASE=...` | Plain SQL als `DB_USER` in MySQL importieren |
+| `make postgres-import FILE=... DATABASE=...` | Plain SQL als `DB_USER` in PostgreSQL importieren |
+| `make dump`, `make restore` | MySQL-`demo` sichern oder wiederherstellen |
+| `make clean-{mysql,postgres,all} CONFIRM=1` | Ausgewählte data directories löschen |
+| `make reinit-{mysql,postgres,all} CONFIRM=1` | Datenbanken löschen, neu erstellen und prüfen |
+
+
+
+
+## Prüfungen
+
+### Statische und lokale Prüfungen
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+`make check-env` erstellt bei Bedarf `.docker.env` und validiert Werte und
+Pfade. `make config` prüft das erweiterte Compose-Modell.
+`make test-storage-paths` benötigt keine Docker runtime und testet Pfade
+außerhalb des Projekts, symlink-Komponenten, überlappende oder verschachtelte
+Pfade sowie reserved directories.
+
+### Runtime-Prüfungen
+
+```bash
+make up-no-ui
+make check
+make test-sql-imports
+make down
+```
+
+`make check` prüft `demo` und den tatsächlichen `DB_USER`-Zugriff in beiden
+DBMS sowie installierte Samples. `make test-sql-imports` benötigt beide
+laufenden DBMS, ruft `mysql-import` und `postgres-import` auf, erstellt
+eindeutig benannte temporäre Tabellen in `demo`, prüft marker rows als
+`DB_USER` und löscht nur diese Tabellen. Dies ist ein Smoke-Test des trusted
+import workflow, keine isolierte Umgebung (`sandbox`) und kein Sicherheitsnachweis für fremdes SQL.
+
+
+## Sicherheit der storage paths
+
+`make check-env` führt
+[`scripts/validate-storage-paths.sh`](../../../scripts/validate-storage-paths.sh)
+aus. Data
+paths müssen strikt unter `data/`, sample paths unter `samples/` liegen;
+Symlinks sowie gleiche, verschachtelte, überlappende oder reservierte Pfade
+werden abgelehnt. `make test-storage-paths` testet dies ohne Docker runtime.
+
+
+## Import vertrauenswürdiger SQL-Dateien
+
+```bash
+make mysql-import FILE=path/to/file.sql DATABASE=demo
+make postgres-import FILE=path/to/file.sql DATABASE=demo
+```
+
+Für beide targets gilt:
+
+- `FILE` und `DATABASE` sind erforderlich.
+- Die lokale plain-SQL-Datei muss existieren, lesbar und nicht leer sein.
+- Die Datenbank muss existieren; Systemdatenbanken sind verboten.
+- Der Import läuft als `DB_USER`, erstellt keine Datenbank und vergibt keine grants.
+- `DATABASE` wählt die erste Verbindung, erzeugt aber keine isolierte Umgebung (`sandbox`).
+- Qualified names, Client-/Session-Befehle und tatsächliche grants können
+ andere erreichbare Objekte betreffen.
+- Partielle Ausführung ist möglich; automatischer Rollback wird nicht zugesagt.
+- Erstellen Sie vor wichtigen Imports ein Backup.
+- gzip, Archive und PostgreSQL-custom-format backups werden nicht verarbeitet.
+
+
+## Backup
+
+Die eingebauten Targets decken nur die konfigurierte MySQL-Datenbank `demo` ab:
+
+```bash
+make dump
+make restore
+```
+
+`make dump` schreibt `backup/demo.sql`; `make restore` liest die Datei und
+wendet die MySQL-Lern-grants erneut an. Verwenden Sie für PostgreSQL ein eigenes
+Backup-Verfahren.
+
+
+## Bereinigung und Neuinitialisierung
+
+> **Warnung:** Alle `clean-*`- und `reinit-*`-Targets sind destruktiv und
+> verlangen die exakte Bestätigung `CONFIRM=1`.
+
+```bash
+make clean-mysql CONFIRM=1
+make clean-postgres CONFIRM=1
+make clean-all CONFIRM=1
+
+make reinit-mysql CONFIRM=1
+make reinit-postgres CONFIRM=1
+make reinit-all CONFIRM=1
+```
+
+Einzelne Targets löschen nur das gewählte DBMS-data directory; `all` löscht
+beide. Konfiguration, init, Samples und Backups bleiben erhalten. Reinit
+startet und prüft anschließend die gewählten DBMS; `reinit-all` startet MySQL,
+PostgreSQL und Adminer und führt anschließend die gemeinsame Zugriffsprüfung aus.
+
+[Zurück zur README](../README_de.md)
diff --git a/docs/langs/de/troubleshooting.md b/docs/langs/de/troubleshooting.md
new file mode 100644
index 0000000..cf5a78d
--- /dev/null
+++ b/docs/langs/de/troubleshooting.md
@@ -0,0 +1,63 @@
+# Diagnose und Fehlerbehebung
+
+[← Zurück zur README](../README_de.md)
+
+## Sprache
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/troubleshooting.md) | [English](../en/troubleshooting.md) | [Español](../es/troubleshooting.md) | [中文](../zh/troubleshooting.md) | [Français](../fr/troubleshooting.md) | **Ausgewählt** |
+
+## Abschnitt
+
+| Erste Schritte | Datenbanken und Samples | Prüfungen und Betrieb | Diagnose und Fehlerbehebung |
+| --- | --- | --- | --- |
+| [Erste Schritte](getting-started.md) | [Datenbanken und Samples](databases.md) | [Prüfungen und Betrieb](operations.md) | **Ausgewählt** |
+
+Sammeln Sie zuerst Diagnosedaten und korrigieren Sie dann die konkrete
+Ursache. Falls eine Neuinitialisierung nötig bleibt, sichern Sie eigene Daten
+nur, wenn sie erhalten bleiben sollen, und verwenden `reinit-... CONFIRM=1` nur als bewussten letzten Schritt.
+
+Verbindliche Details zu Lebenszyklus und Betrieb: [Datenbanken](databases.md#section-initialization) · [Betrieb](operations.md#section-clean-reinitialize).
+
+
+## Konfiguration oder Pfadvalidierung schlägt fehl
+
+Führen Sie `make check-env` aus. Data paths müssen strikt unter `data/` und
+Samples unter `samples/` liegen; symlinks, Überschneidungen und reservierte
+Verzeichnisse sind unzulässig. Korrigieren Sie `.docker.env` und führen Sie
+`make config` aus.
+
+
+## Ein Dienst wird nicht bereit
+
+```bash
+make status
+make log SERVICE=mysql
+make log SERVICE=postgres
+```
+
+Prüfen Sie Docker, Port, `.docker.env` und Logs, bevor Sie Daten ändern.
+
+
+## Init-Änderungen oder Samples erscheinen nicht
+
+Bei bereits initialisierten Daten ist das erwartet. Prüfen Sie Pfade und
+Vorbereitung; wenn wichtige Daten erhalten bleiben sollen, sichern Sie sie und
+verwenden `reinit-... CONFIRM=1` nur als bewussten letzten Schritt.
+
+
+## Sample ist unvollständig oder hat einen unerwarteten Besitzer
+
+Der Loader überschreibt oder repariert unerwartete Datenbanken nicht. Führen
+Sie `make samples-mysql` oder `make samples-postgres` erneut aus und prüfen Sie
+den Fehler; sichern Sie Daten vor einer Neuinitialisierung.
+
+
+## Ein Client kann keine Verbindung herstellen
+
+Host-Clients verwenden veröffentlichte Adresse und `MYSQL_PORT` oder
+`POSTGRES_PORT`; Adminer verwendet `mysql` oder `postgres` im Compose-Netz.
+Prüfen Sie `BIND_ADDRESS`, Firewall, Datenbankauswahl und `DB_USER`-Zugangsdaten.
+
+[Zurück zur README](../README_de.md)
diff --git a/docs/langs/en/databases.md b/docs/langs/en/databases.md
new file mode 100644
index 0000000..ac59ecd
--- /dev/null
+++ b/docs/langs/en/databases.md
@@ -0,0 +1,133 @@
+# Databases and samples
+
+[← Back to README](../README_en.md)
+
+## Language
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/databases.md) | **Selected** | [Español](../es/databases.md) | [中文](../zh/databases.md) | [Français](../fr/databases.md) | [Deutsch](../de/databases.md) |
+
+## Section
+
+| Getting started | Databases and samples | Validation and operations | Troubleshooting |
+| --- | --- | --- | --- |
+| [Getting started](getting-started.md) | **Selected** | [Validation and operations](operations.md) | [Troubleshooting](troubleshooting.md) |
+
+
+## Required `demo` databases
+
+Both DBMSs initialize a required database named `demo`:
+
+- MySQL: `demo.demo_users`
+- PostgreSQL: `demo.public.demo_users`
+
+The tables have equivalent `id`, `name`, `email`, and `created_at` fields and
+contain the same five required example users: Alice, Bob, Carol, Dave, and Eve.
+The access checks allow additional user-created rows.
+
+The default database names are enforced by `make check-env` through
+`MYSQL_DATABASE=demo` and `POSTGRES_DATABASE=demo`.
+
+
+## Optional samples
+
+| DBMS | Optional databases | Preparation command |
+|---|---|---|
+| MySQL | `chinook`, `sakila` | `make samples-mysql` |
+| PostgreSQL | `pagila`, `chinook` | `make samples-postgres` |
+
+
+## Sample preparation
+
+Preparation requires `curl` and `git`; MySQL samples additionally require `unzip` and `sha256sum`.
+
+The preparation commands download and verify pinned upstream files but do not
+start containers or import into an initialized database. Downloads are local,
+are excluded from Git, and are stored under `MYSQL_SAMPLES_DIR` or
+`POSTGRES_SAMPLES_DIR`. Their provenance, integrity pins, and license terms are
+documented in
+[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md).
+
+See [Initialization lifecycle](#section-initialization) for when preparation
+must occur and how an initialized DBMS can be reinitialized.
+
+A completely absent optional sample is skipped and does not prevent the
+required `demo` database from being created. A partial sample set or an
+unexpected existing sample database is rejected instead of being repaired or
+deleted automatically.
+
+
+## Storage layout
+
+The default bind-mounted storage is separated by DBMS:
+
+```text
+data/
+├── mysql/
+└── postgres/
+
+initdb/
+├── mysql/
+└── postgres/
+```
+
+The related `.docker.env` settings are separate as well:
+
+| DBMS | Data | Init | Optional samples |
+|---|---|---|---|
+| MySQL | `MYSQL_DATA_DIR` (`./data/mysql`) | `MYSQL_INITDB_DIR` (`./initdb/mysql`) | `MYSQL_SAMPLES_DIR` (`./samples/mysql`) |
+| PostgreSQL | `POSTGRES_DATA_DIR` (`./data/postgres`) | `POSTGRES_INITDB_DIR` (`./initdb/postgres`) | `POSTGRES_SAMPLES_DIR` (`./samples/postgres`) |
+
+Data and sample locations can be changed through `.docker.env`, subject to
+managed storage-path validation. See
+[Initialization lifecycle](#section-initialization) for the rules governing
+the init directories.
+
+Do not edit database files inside `data/` manually. Container-owned files may
+use numeric UID/GID values that differ from the host user.
+
+
+## Initialization lifecycle
+
+> **Important:** Official MySQL and PostgreSQL entrypoints run init files only
+> for an empty data directory. Adding files after initialization does not
+> change an existing database. `make down` preserves data, while confirmed
+> reinitialization deletes the selected DBMS's current data and recreates its
+> databases from the current init scripts. Back up custom data only when you
+> need to preserve it; a one-off lab with no valuable changes needs no backup.
+
+For first initialization with sample datasets, prepare them before the first
+start:
+
+```bash
+make samples-mysql
+make up-mysql
+
+make samples-postgres
+make up-postgres
+```
+
+For an initialized DBMS, preserve custom data if needed and then use only its
+matching confirmed reinitialization:
+
+```bash
+make samples-mysql
+make reinit-mysql CONFIRM=1
+
+make samples-postgres
+make reinit-postgres CONFIRM=1
+```
+
+
+## Training access and ownership
+
+MySQL creates `DB_USER` and grants it access to every non-system
+database found during init. PostgreSQL creates a separate, non-superuser
+`DB_USER` without createdb/createrole privileges and makes it owner of
+`demo`, the `public` schema, and loaded sample objects. Administrative
+credentials remain separate: `MYSQL_ROOT_PASSWORD`, `POSTGRES_SUPERUSER`,
+and `POSTGRES_SUPERUSER_PASSWORD`. Do not edit container-owned files in
+`data/` manually.
+
+[Back to README](../README_en.md)
diff --git a/docs/langs/en/getting-started.md b/docs/langs/en/getting-started.md
new file mode 100644
index 0000000..9e4d6ae
--- /dev/null
+++ b/docs/langs/en/getting-started.md
@@ -0,0 +1,176 @@
+# Getting started
+
+[← Back to README](../README_en.md)
+
+## Language
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/getting-started.md) | **Selected** | [Español](../es/getting-started.md) | [中文](../zh/getting-started.md) | [Français](../fr/getting-started.md) | [Deutsch](../de/getting-started.md) |
+
+## Section
+
+| Getting started | Databases and samples | Validation and operations | Troubleshooting |
+| --- | --- | --- | --- |
+| **Selected** | [Databases and samples](databases.md) | [Validation and operations](operations.md) | [Troubleshooting](troubleshooting.md) |
+
+
+## Requirements
+
+- Docker Engine or Docker Desktop with Docker Compose v2.
+- GNU Make, Bash, and basic Unix CLI utilities.
+- Recommended environments: Linux; macOS with Docker Desktop; or Windows with
+ Docker Desktop and WSL2.
+
+Run project commands from the repository root. The default project branch is
+`master`.
+
+
+## Quick start
+
+`make init` creates a local `.docker.env` from
+[`.docker.env.example`](../../../.docker.env.example), validates the managed
+storage paths, and creates the working directories. Then start the full lab:
+
+```bash
+make init
+make up
+```
+
+`make up` starts MySQL, PostgreSQL, and Adminer. With the default configuration,
+open Adminer at `http://127.0.0.1:8081`.
+
+Use `make up-no-ui` to start both DBMSs without Adminer. The required `demo`
+database is always created during first initialization; sample datasets are
+optional. To include them in the first initialization, prepare them before the
+first `make up`. For initialized data directories, create a backup before
+confirmed reinitialization only when you need to preserve custom data. See
+[Initialization lifecycle](databases.md#section-initialization) for the exact
+procedure.
+
+Useful follow-up commands:
+
+```bash
+make status
+make logs
+make down
+```
+
+`make down` removes the containers and network but preserves the bind-mounted
+database data.
+
+
+## Startup modes
+
+| Command | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Starts | Starts | Starts |
+| `make up-no-ui` | Starts | Starts | Stops |
+| `make up-mysql` | Starts | Does not start | Does not start |
+| `make up-postgres` | Does not start | Starts | Does not start |
+
+Single-DBMS commands do not stop the other running database; Adminer is managed separately.
+
+
+📋 Full startup-mode table
+
+| Command | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Starts | Starts | Starts |
+| `make up-no-ui` | Starts or keeps running | Starts or keeps running | Stops if running |
+| `make up-mysql` | Starts | Does not start automatically | Does not start automatically |
+| `make up-mysql-ui` | Starts | Does not start automatically | Starts |
+| `make up-postgres` | Does not start automatically | Starts | Does not start automatically |
+| `make up-postgres-ui` | Does not start automatically | Starts | Starts |
+| `make up-ui` | Does not change | Does not change | Starts |
+| `make down-ui` | Does not change | Does not change | Stops |
+
+The single-service commands do not stop another database that is already
+running. Adminer can be started and stopped separately and is not tied to
+MySQL alone.
+
+
+
+
+## Connections
+
+### Adminer
+
+Adminer provides two predefined server choices inside the Compose network:
+
+```text
+MySQL (mysql)
+PostgreSQL (postgres)
+```
+
+Select a server, then sign in with `DB_USER`, `DB_PASSWORD`, and a database
+name such as `demo`. The service names `mysql` and `postgres` work inside the
+Compose network; they are not host names for desktop clients.
+
+### Host clients
+
+DataGrip, DBeaver, PhpStorm, and host CLI tools connect through the published
+address and port:
+
+| DBMS | Host with default settings | Port variable | User | Default database |
+|---|---|---|---|---|
+| MySQL | `127.0.0.1` | `MYSQL_PORT` | `DB_USER` | `demo` |
+| PostgreSQL | `127.0.0.1` | `POSTGRES_PORT` | `DB_USER` | `demo` |
+
+If you change `BIND_ADDRESS`, use that reachable interface address instead of
+`127.0.0.1` where appropriate.
+
+### Container CLI
+
+The Make targets pass passwords through the container environment instead of
+placing them in shell history:
+
+```bash
+make mysql # MySQL administrator, demo database
+make mysql-user # DB_USER, demo database
+make postgres # PostgreSQL superuser, demo database
+make postgres-user # DB_USER, demo database
+```
+
+
+## Credentials
+
+Copying [`.docker.env.example`](../../../.docker.env.example) creates `.docker.env`; the latter is ignored by
+Git. Keep passwords there and do not hardcode them in Compose, SQL, or client
+configuration committed to the repository.
+
+| Purpose | User setting | Password setting |
+|---|---|---|
+| Shared training user for both DBMSs | `DB_USER` | `DB_PASSWORD` |
+| MySQL administrator | `root` | `MYSQL_ROOT_PASSWORD` |
+| PostgreSQL administrator/superuser | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` and `DB_USER` must be different roles. Use the shared
+training user for normal exercises; do not use root or superuser credentials
+for routine work. Replace the example passwords before sharing access or
+publishing any service beyond the local machine.
+
+
+## Ports and `BIND_ADDRESS`
+
+The host-side ports are configured in `.docker.env`:
+
+| Service | Port variable | Example default |
+|---|---|---|
+| MySQL | `MYSQL_PORT` | `3306` |
+| PostgreSQL | `POSTGRES_PORT` | `5432` |
+| Adminer | `ADMINER_PORT` | `8081` |
+
+All three services publish only on loopback by default:
+
+```env
+BIND_ADDRESS=127.0.0.1
+```
+
+This is the safe default for a local lab. Setting `BIND_ADDRESS=0.0.0.0`
+publishes the configured ports on every network interface. For VPN or LAN
+access, prefer the address of the specific interface. Change the binding only
+deliberately, after considering firewall rules, password strength, and trust
+in every connected network.
+
+[Back to README](../README_en.md)
diff --git a/docs/langs/en/operations.md b/docs/langs/en/operations.md
new file mode 100644
index 0000000..a23fa09
--- /dev/null
+++ b/docs/langs/en/operations.md
@@ -0,0 +1,184 @@
+# Validation and operations
+
+[← Back to README](../README_en.md)
+
+## Language
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/operations.md) | **Selected** | [Español](../es/operations.md) | [中文](../zh/operations.md) | [Français](../fr/operations.md) | [Deutsch](../de/operations.md) |
+
+## Section
+
+| Getting started | Databases and samples | Validation and operations | Troubleshooting |
+| --- | --- | --- | --- |
+| [Getting started](getting-started.md) | [Databases and samples](databases.md) | **Selected** | [Troubleshooting](troubleshooting.md) |
+
+
+## Make targets
+
+Public targets and their implementation are in the [`Makefile`](../../../Makefile).
+
+Key targets include `make init`, `make up`, `make down`, `make check`,
+`make test-storage-paths`, `make test-sql-imports`, `make mysql-import`,
+and `make postgres-import`. Trusted SQL is not a sandbox, may execute
+partially, and has no guaranteed automatic rollback; create a backup before an
+important import. Built-in backup targets cover MySQL `demo` only, not
+PostgreSQL. `clean-*` and `reinit-*` are destructive and require exact
+`CONFIRM=1`.
+
+
+📋 Complete public Make target reference
+
+Run `make help` for the concise command list.
+
+| Command | Purpose |
+|---|---|
+| `make init` | Create `.docker.env`, validate managed paths, and create required working directories |
+| `make check-env` | Check required environment values, role separation, database names, and managed paths |
+| `make pull` | Pull the three pinned container images |
+| `make config` | Validate the expanded Compose configuration |
+| `make up`, `make up-no-ui` | Start both DBMSs with or without Adminer |
+| `make up-mysql`, `make up-mysql-ui` | Start MySQL, optionally with Adminer |
+| `make up-postgres`, `make up-postgres-ui` | Start PostgreSQL, optionally with Adminer |
+| `make up-ui`, `make down-ui` | Start or stop only Adminer |
+| `make down` | Stop the lab without deleting bind-mounted data |
+| `make status` | Show service status |
+| `make logs` | Follow logs for all services |
+| `make log SERVICE=postgres` | Follow one service log; `make log postgres` is also supported |
+| `make in SERVICE=postgres` | Open a service shell; `make in postgres` is also supported |
+| `make mysql`, `make mysql-user` | Open MySQL as administrator or `DB_USER` |
+| `make postgres`, `make postgres-user` | Open PostgreSQL as superuser or `DB_USER` |
+| `make samples-mysql`, `make samples-postgres` | Prepare verified optional samples |
+| `make check-mysql-access`, `make check-postgres-access` | Verify training-user access to one running DBMS |
+| `make check` | Validate Compose and check `DB_USER` access to both running DBMSs |
+| `make test-storage-paths` | Test managed storage-path protection without Docker runtime |
+| `make test-sql-imports` | Smoke-test both public trusted SQL import targets |
+| `make mysql-import FILE=... DATABASE=...` | Import trusted plain SQL into an existing MySQL database as `DB_USER` |
+| `make postgres-import FILE=... DATABASE=...` | Import trusted plain SQL into an existing PostgreSQL database as `DB_USER` |
+| `make dump`, `make restore` | Dump or restore the configured MySQL `demo` database |
+| `make clean-{mysql,postgres,all} CONFIRM=1` | Delete selected managed data directories |
+| `make reinit-{mysql,postgres,all} CONFIRM=1` | Delete, recreate, and check selected databases |
+
+
+
+
+## Validation
+
+### Static and local checks
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+`make check-env` creates `.docker.env` from the example if it is absent, then
+validates required settings and managed paths. `make config` validates the
+expanded Compose model.
+
+`make test-storage-paths` does not require the Docker runtime. It exercises the
+guards for paths outside the project, symbolic-link components, overlapping or
+nested managed paths, and reserved directories. The same validator protects
+the configured MySQL/PostgreSQL data and sample locations during normal setup.
+
+### Runtime checks
+
+Start both databases without Adminer, check training access, exercise imports,
+and stop the services:
+
+```bash
+make up-no-ui
+make check
+make test-sql-imports
+make down
+```
+
+`make check` verifies the required `demo` data and actual `DB_USER` access in
+both DBMSs. It also checks supported optional samples when they are installed.
+
+`make test-sql-imports` requires both DBMSs to be running. It invokes the
+public `mysql-import` and `postgres-import` targets, creates uniquely named
+temporary smoke tables in `demo`, verifies marker rows as `DB_USER`, and
+removes only those tables. It tests the trusted SQL import workflow; it does
+not prove that untrusted SQL is safe or sandboxed.
+
+
+## Storage-path safety
+
+`make check-env` runs
+[`scripts/validate-storage-paths.sh`](../../../scripts/validate-storage-paths.sh).
+Data paths
+must be strict descendants of `data/`, and sample paths of `samples/`.
+Symlink components, equal, nested, overlapping, and reserved paths are
+rejected. `make test-storage-paths` exercises these rules without Docker
+runtime.
+
+
+## Trusted SQL imports
+
+Import only trusted local plain SQL files:
+
+```bash
+make mysql-import FILE=path/to/file.sql DATABASE=demo
+make postgres-import FILE=path/to/file.sql DATABASE=demo
+```
+
+For both targets:
+
+- `FILE` and `DATABASE` are required.
+- The file must be a readable, non-empty regular file.
+- The database name must start with a lowercase ASCII letter and contain only
+ lowercase ASCII letters, digits, or `_`; system databases are rejected.
+- The database must already exist and accept a connection from `DB_USER`.
+- Import runs as `DB_USER`, never as MySQL root or PostgreSQL superuser.
+- The target neither creates a database nor grants access.
+- Archives, gzip streams, and PostgreSQL custom-format backups are not
+ supported by these targets.
+
+`DATABASE` selects the initial connection database; it does not create a
+sandbox. Qualified names, session or client commands, and the actual grants of
+`DB_USER` may permit access to other objects. SQL may modify or delete anything
+that role can access.
+
+An import may complete partially. Neither target promises an automatic full
+rollback after an error. Review the file and create an appropriate backup
+before an important import.
+
+
+## Backup
+
+The built-in backup targets cover only the configured MySQL `demo` database:
+
+```bash
+make dump
+make restore
+```
+
+`make dump` writes `backup/demo.sql` with the default configuration.
+`make restore` reads that file and reapplies MySQL training grants. Use a
+separate PostgreSQL backup procedure when PostgreSQL data must be preserved.
+
+
+## Clean and reinitialize
+
+> **Warning:** Every `clean-*` and `reinit-*` command below is destructive and
+> requires the exact opt-in `CONFIRM=1`.
+
+```bash
+make clean-mysql CONFIRM=1
+make clean-postgres CONFIRM=1
+make clean-all CONFIRM=1
+
+make reinit-mysql CONFIRM=1
+make reinit-postgres CONFIRM=1
+make reinit-all CONFIRM=1
+```
+
+The single-DBMS commands delete only that DBMS data directory. The `all`
+variants delete both database data directories. Configuration, init files,
+optional sample downloads, and backups are preserved. Reinitialization then
+starts and checks the selected DBMSs; `reinit-all` starts MySQL, PostgreSQL,
+and Adminer, then performs the shared access check.
+
+[Back to README](../README_en.md)
diff --git a/docs/langs/en/troubleshooting.md b/docs/langs/en/troubleshooting.md
new file mode 100644
index 0000000..3769cf9
--- /dev/null
+++ b/docs/langs/en/troubleshooting.md
@@ -0,0 +1,72 @@
+# Troubleshooting
+
+[← Back to README](../README_en.md)
+
+## Language
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/troubleshooting.md) | **Selected** | [Español](../es/troubleshooting.md) | [中文](../zh/troubleshooting.md) | [Français](../fr/troubleshooting.md) | [Deutsch](../de/troubleshooting.md) |
+
+## Section
+
+| Getting started | Databases and samples | Validation and operations | Troubleshooting |
+| --- | --- | --- | --- |
+| [Getting started](getting-started.md) | [Databases and samples](databases.md) | [Validation and operations](operations.md) | **Selected** |
+
+Collect diagnostics first, then correct the specific cause. If a
+reinitialization is still required, back up custom data only when you need to
+keep it and use confirmed `reinit-... CONFIRM=1` only as a deliberate last resort.
+
+Canonical lifecycle and operational details: [databases](databases.md#section-initialization) · [operations](operations.md#section-clean-reinitialize).
+
+
+## Configuration or storage-path validation fails
+
+Run `make check-env` and read the rejected variable and path in the error.
+Managed data paths must be strictly inside the project `data/` tree; sample
+paths must be inside `samples/`. They cannot contain symbolic-link components,
+overlap one another, or use reserved project directories. Correct
+`.docker.env`, then rerun `make check-env` and `make config`.
+
+
+## A service does not become ready
+
+Check the service state and logs before changing data:
+
+```bash
+make status
+make log SERVICE=mysql
+make log SERVICE=postgres
+```
+
+Confirm that Docker is running, the configured host port is available, and
+`.docker.env` contains all required values. Fix the specific configuration or
+port conflict and start the service again.
+
+
+## Init changes or optional samples do not appear
+
+This is expected when the data directory is already initialized. Verify the
+configured data and sample paths and confirm that the preparation command
+succeeded. If existing data matters, back it up. Use the matching
+`reinit-... CONFIRM=1` command only as a deliberate last step; it deletes that
+DBMS's data.
+
+
+## An optional sample is incomplete or has unexpected ownership
+
+The loader intentionally refuses to overwrite or repair an unexpected
+database. Re-run the appropriate `make samples-mysql` or
+`make samples-postgres` preparation command and inspect the error. Preserve any
+needed data before considering a confirmed reinitialization.
+
+
+## A client cannot connect
+
+Host clients use the published host address and `MYSQL_PORT` or
+`POSTGRES_PORT`, not the Compose service name. Adminer uses `mysql` or
+`postgres` inside the Compose network. Confirm `BIND_ADDRESS`, firewall rules,
+the selected database, and the non-administrative `DB_USER` credentials.
+
+[Back to README](../README_en.md)
diff --git a/docs/langs/es/databases.md b/docs/langs/es/databases.md
new file mode 100644
index 0000000..54216ae
--- /dev/null
+++ b/docs/langs/es/databases.md
@@ -0,0 +1,119 @@
+# Bases y samples
+
+[← Volver al README](../README_es.md)
+
+## Idioma
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/databases.md) | [English](../en/databases.md) | **Seleccionado** | [中文](../zh/databases.md) | [Français](../fr/databases.md) | [Deutsch](../de/databases.md) |
+
+## Sección
+
+| Primeros pasos | Bases y samples | Comprobaciones y operaciones | Diagnóstico |
+| --- | --- | --- | --- |
+| [Primeros pasos](getting-started.md) | **Seleccionado** | [Comprobaciones y operaciones](operations.md) | [Diagnóstico](troubleshooting.md) |
+
+
+## Bases `demo` obligatorias
+
+Ambos SGBD inicializan `demo`:
+
+- MySQL: `demo.demo_users`
+- PostgreSQL: `demo.public.demo_users`
+
+Las tablas tienen campos equivalentes `id`, `name`, `email` y `created_at`, y
+los mismos cinco usuarios: Alice, Bob, Carol, Dave y Eve. Las comprobaciones
+admiten filas adicionales. `make check-env` exige `MYSQL_DATABASE=demo` y
+`POSTGRES_DATABASE=demo`.
+
+
+## Samples opcionales
+
+| SGBD | Bases opcionales | Preparación |
+|---|---|---|
+| MySQL | `chinook`, `sakila` | `make samples-mysql` |
+| PostgreSQL | `pagila`, `chinook` | `make samples-postgres` |
+
+
+## Preparación de samples
+
+La preparación requiere `curl` y `git`; los samples MySQL también necesitan `unzip` y `sha256sum`.
+
+La preparación descarga y verifica archivos upstream fijados, pero no inicia
+contenedores ni importa en una base ya inicializada. Las descargas temporales
+son locales, no se versionan y quedan bajo `MYSQL_SAMPLES_DIR` o
+`POSTGRES_SAMPLES_DIR`. Procedencia, hashes y licencias están en
+[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md).
+
+Consulte [Inicialización y ciclo de vida](#section-initialization) para saber
+cuándo preparar los samples y cómo reinicializar un SGBD existente.
+
+Un sample ausente se omite y no impide crear `demo`. Un conjunto parcial o una
+base inesperada se rechazan sin reparación ni borrado automático.
+
+
+## Estructura de almacenamiento
+
+```text
+data/
+├── mysql/
+└── postgres/
+
+initdb/
+├── mysql/
+└── postgres/
+```
+
+| SGBD | Data | Init | Samples opcionales |
+|---|---|---|---|
+| MySQL | `MYSQL_DATA_DIR` (`./data/mysql`) | `MYSQL_INITDB_DIR` (`./initdb/mysql`) | `MYSQL_SAMPLES_DIR` (`./samples/mysql`) |
+| PostgreSQL | `POSTGRES_DATA_DIR` (`./data/postgres`) | `POSTGRES_INITDB_DIR` (`./initdb/postgres`) | `POSTGRES_SAMPLES_DIR` (`./samples/postgres`) |
+
+Las rutas data y samples son configurables en `.docker.env` y están sujetas a
+validación. Las reglas de los directorios init se describen en
+[Inicialización y ciclo de vida](#section-initialization). No edite manualmente
+los archivos de `data/`; pueden pertenecer a UID/GID numéricos del contenedor.
+
+
+## Inicialización y ciclo de vida
+
+> **Importante:** Los entrypoints oficiales de MySQL y PostgreSQL ejecutan init
+> solo si el data directory está vacío. Añadir archivos tras la inicialización
+> no cambia una base existente. `make down` conserva los datos, mientras que
+> una reinicialización confirmada elimina los datos actuales del SGBD elegido y
+> recrea las bases con los scripts init actuales. Haga backup solo si debe
+> conservar datos propios; un laboratorio puntual sin cambios valiosos no lo necesita.
+
+Para una primera inicialización con samples, prepárelos antes del primer inicio:
+
+```bash
+make samples-mysql
+make up-mysql
+
+make samples-postgres
+make up-postgres
+```
+
+Para un SGBD inicializado, conserve los datos propios si hace falta y use
+únicamente su reinicialización confirmada correspondiente:
+
+```bash
+make samples-mysql
+make reinit-mysql CONFIRM=1
+
+make samples-postgres
+make reinit-postgres CONFIRM=1
+```
+
+
+## Acceso didáctico y ownership
+
+MySQL crea `DB_USER` y le concede acceso a todas las bases no
+sistémicas encontradas durante init. PostgreSQL crea un `DB_USER` separado,
+sin superuser/createdb/createrole, y lo hace propietario de `demo`, de
+`public` y de los objetos sample. Las credenciales administrativas siguen
+separadas: `MYSQL_ROOT_PASSWORD`, `POSTGRES_SUPERUSER` y
+`POSTGRES_SUPERUSER_PASSWORD`. No edite manualmente archivos de `data/`.
+
+[Volver al README](../README_es.md)
diff --git a/docs/langs/es/getting-started.md b/docs/langs/es/getting-started.md
new file mode 100644
index 0000000..d02e557
--- /dev/null
+++ b/docs/langs/es/getting-started.md
@@ -0,0 +1,166 @@
+# Primeros pasos
+
+[← Volver al README](../README_es.md)
+
+## Idioma
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/getting-started.md) | [English](../en/getting-started.md) | **Seleccionado** | [中文](../zh/getting-started.md) | [Français](../fr/getting-started.md) | [Deutsch](../de/getting-started.md) |
+
+## Sección
+
+| Primeros pasos | Bases y samples | Comprobaciones y operaciones | Diagnóstico |
+| --- | --- | --- | --- |
+| **Seleccionado** | [Bases y samples](databases.md) | [Comprobaciones y operaciones](operations.md) | [Diagnóstico](troubleshooting.md) |
+
+
+## Requisitos
+
+- Docker Engine o Docker Desktop con Docker Compose v2.
+- GNU Make, Bash y utilidades básicas de línea de comandos Unix.
+- Entornos recomendados: Linux; macOS con Docker Desktop; o Windows con
+ Docker Desktop y WSL2.
+
+Ejecute los comandos desde la raíz del repositorio. La rama predeterminada del
+proyecto es `master`.
+
+
+## Inicio rápido
+
+`make init` crea un `.docker.env` local desde
+[`.docker.env.example`](../../../.docker.env.example), valida las rutas de
+almacenamiento gestionadas y crea los directorios de trabajo. Después, inicie
+el laboratorio completo:
+
+```bash
+make init
+make up
+```
+
+`make up` inicia MySQL, PostgreSQL y Adminer. Con la configuración por defecto,
+Adminer está en `http://127.0.0.1:8081`.
+
+Use `make up-no-ui` para iniciar ambos SGBD sin Adminer. La base obligatoria
+`demo` siempre se crea durante la primera inicialización; los datasets de
+ejemplo son opcionales. Para incluirlos en la primera inicialización,
+prepárelos antes del primer `make up`. Si necesita conservar datos propios de
+directorios ya inicializados, cree un backup antes de la reinicialización
+confirmada. Consulte el procedimiento exacto en
+[Inicialización y ciclo de vida](databases.md#section-initialization).
+
+```bash
+make status
+make logs
+make down
+```
+
+`make down` elimina los contenedores y la red, pero conserva los datos
+bind-mounted.
+
+
+## Modos de inicio
+
+| Comando | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Inicia | Inicia | Inicia |
+| `make up-no-ui` | Inicia | Inicia | Detiene |
+| `make up-mysql` | Inicia | No inicia | No inicia |
+| `make up-postgres` | No inicia | Inicia | No inicia |
+
+Los comandos de un SGBD no detienen el otro ya activo; Adminer se gestiona aparte.
+
+
+📋 Tabla completa de modos de inicio
+
+| Comando | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Inicia | Inicia | Inicia |
+| `make up-no-ui` | Inicia o mantiene activo | Inicia o mantiene activo | Detiene si estaba activo |
+| `make up-mysql` | Inicia | No inicia automáticamente | No inicia automáticamente |
+| `make up-mysql-ui` | Inicia | No inicia automáticamente | Inicia |
+| `make up-postgres` | No inicia automáticamente | Inicia | No inicia automáticamente |
+| `make up-postgres-ui` | No inicia automáticamente | Inicia | Inicia |
+| `make up-ui` | No cambia | No cambia | Inicia |
+| `make down-ui` | No cambia | No cambia | Detiene |
+
+Los comandos de un solo SGBD no detienen el otro si ya está activo. Adminer se
+puede iniciar y detener por separado y no está ligado únicamente a MySQL.
+
+
+
+
+## Conexiones
+
+### Adminer
+
+Dentro de la red de Compose, Adminer ofrece dos servidores predefinidos:
+
+```text
+MySQL (mysql)
+PostgreSQL (postgres)
+```
+
+Seleccione uno e introduzca `DB_USER`, `DB_PASSWORD` y una base como `demo`.
+`mysql` y `postgres` son nombres internos de servicio, no hosts para clientes
+de escritorio.
+
+### Clientes en el host
+
+| SGBD | Host predeterminado | Variable de puerto | Usuario | Base predeterminada |
+|---|---|---|---|---|
+| MySQL | `127.0.0.1` | `MYSQL_PORT` | `DB_USER` | `demo` |
+| PostgreSQL | `127.0.0.1` | `POSTGRES_PORT` | `DB_USER` | `demo` |
+
+DataGrip, DBeaver, PhpStorm y los CLI del host usan la dirección y el puerto
+publicados. Si cambia `BIND_ADDRESS`, use la dirección accesible de esa interfaz.
+
+### CLI dentro de los contenedores
+
+Las contraseñas pasan por el entorno del contenedor y no quedan en el historial:
+
+```bash
+make mysql # administrador MySQL, base demo
+make mysql-user # DB_USER, base demo
+make postgres # superusuario PostgreSQL, base demo
+make postgres-user # DB_USER, base demo
+```
+
+
+## Credenciales
+
+`.docker.env` se crea a partir de [`.docker.env.example`](../../../.docker.env.example) y está ignorado por
+Git. Guarde allí las contraseñas; no las codifique en Compose, SQL ni en
+configuración de cliente versionada.
+
+| Finalidad | Usuario | Contraseña |
+|---|---|---|
+| Usuario didáctico común | `DB_USER` | `DB_PASSWORD` |
+| Administrador MySQL | `root` | `MYSQL_ROOT_PASSWORD` |
+| Administrador/superusuario PostgreSQL | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` y `DB_USER` deben ser roles distintos. Use `DB_USER` para
+los ejercicios habituales. Cambie las contraseñas de ejemplo antes de
+compartir o publicar cualquier servicio.
+
+
+## Puertos y `BIND_ADDRESS`
+
+| Servicio | Variable | Valor de ejemplo |
+|---|---|---|
+| MySQL | `MYSQL_PORT` | `3306` |
+| PostgreSQL | `POSTGRES_PORT` | `5432` |
+| Adminer | `ADMINER_PORT` | `8081` |
+
+Por defecto, los tres servicios solo se publican en loopback:
+
+```env
+BIND_ADDRESS=127.0.0.1
+```
+
+`127.0.0.1` es el valor local predeterminado. `BIND_ADDRESS=0.0.0.0` publica
+los puertos en todas las interfaces. Para LAN o VPN, prefiera la dirección de
+una interfaz concreta. Cambie este valor conscientemente y revise firewall,
+fortaleza de contraseñas y confianza en la red.
+
+[Volver al README](../README_es.md)
diff --git a/docs/langs/es/operations.md b/docs/langs/es/operations.md
new file mode 100644
index 0000000..6d1f74a
--- /dev/null
+++ b/docs/langs/es/operations.md
@@ -0,0 +1,162 @@
+# Comprobaciones y operaciones
+
+[← Volver al README](../README_es.md)
+
+## Idioma
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/operations.md) | [English](../en/operations.md) | **Seleccionado** | [中文](../zh/operations.md) | [Français](../fr/operations.md) | [Deutsch](../de/operations.md) |
+
+## Sección
+
+| Primeros pasos | Bases y samples | Comprobaciones y operaciones | Diagnóstico |
+| --- | --- | --- | --- |
+| [Primeros pasos](getting-started.md) | [Bases y samples](databases.md) | **Seleccionado** | [Diagnóstico](troubleshooting.md) |
+
+
+## Targets públicos de Make
+
+Los targets públicos y su implementación están en el [`Makefile`](../../../Makefile).
+
+Targets clave: `make init`, `make up`, `make down`, `make check`,
+`make test-storage-paths`, `make test-sql-imports`, `make mysql-import` y
+`make postgres-import`. El SQL de confianza no es un sandbox, puede
+ejecutarse parcialmente y no ofrece rollback automático completo; haga backup
+antes de un import importante. Los backups integrados solo cubren MySQL
+`demo`, no PostgreSQL. `clean-*` y `reinit-*` son destructivos y exigen
+`CONFIRM=1` exacto.
+
+
+📋 Referencia completa de targets públicos de Make
+
+`make help` muestra la lista resumida.
+
+| Comando | Función |
+|---|---|
+| `make init` | Crear `.docker.env`, validar rutas y crear directorios |
+| `make check-env` | Validar variables, roles, bases y managed paths |
+| `make pull` | Descargar las tres imágenes fijadas |
+| `make config` | Validar la configuración Compose expandida |
+| `make up`, `make up-no-ui` | Iniciar ambos SGBD con o sin Adminer |
+| `make up-mysql`, `make up-mysql-ui` | Iniciar MySQL, opcionalmente con Adminer |
+| `make up-postgres`, `make up-postgres-ui` | Iniciar PostgreSQL, opcionalmente con Adminer |
+| `make up-ui`, `make down-ui` | Iniciar o detener solo Adminer |
+| `make down` | Detener sin borrar los datos bind-mounted |
+| `make status`, `make logs` | Mostrar estado o seguir todos los logs |
+| `make log SERVICE=postgres` | Seguir un servicio; también `make log postgres` |
+| `make in SERVICE=postgres` | Abrir un shell; también `make in postgres` |
+| `make mysql`, `make mysql-user` | Abrir MySQL como admin o `DB_USER` |
+| `make postgres`, `make postgres-user` | Abrir PostgreSQL como superusuario o `DB_USER` |
+| `make samples-mysql`, `make samples-postgres` | Preparar samples verificados |
+| `make check-mysql-access`, `make check-postgres-access` | Comprobar un SGBD activo |
+| `make check` | Validar Compose y acceso de `DB_USER` a ambos SGBD |
+| `make test-storage-paths` | Probar la protección de managed paths sin Docker runtime |
+| `make test-sql-imports` | Smoke-test de los dos imports SQL públicos |
+| `make mysql-import FILE=... DATABASE=...` | Importar plain SQL en MySQL como `DB_USER` |
+| `make postgres-import FILE=... DATABASE=...` | Importar plain SQL en PostgreSQL como `DB_USER` |
+| `make dump`, `make restore` | Exportar o restaurar `demo` de MySQL |
+| `make clean-{mysql,postgres,all} CONFIRM=1` | Borrar data directories seleccionados |
+| `make reinit-{mysql,postgres,all} CONFIRM=1` | Borrar, recrear y comprobar bases |
+
+
+
+
+## Comprobaciones
+
+### Estáticas y locales
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+`make check-env` crea `.docker.env` si falta y valida valores y rutas.
+`make config` valida el modelo Compose expandido. `make test-storage-paths` no
+requiere Docker runtime: prueba rutas fuera del proyecto, componentes symlink,
+rutas solapadas o anidadas y directorios reservados.
+
+### Runtime
+
+```bash
+make up-no-ui
+make check
+make test-sql-imports
+make down
+```
+
+`make check` verifica `demo` y el acceso real de `DB_USER` en ambos SGBD,
+además de samples instalados. `make test-sql-imports` requiere ambos SGBD,
+invoca `mysql-import` y `postgres-import`, crea tablas temporales con nombres
+únicos en `demo`, comprueba marker rows como `DB_USER` y elimina solo esas
+tablas. Es un smoke-test del flujo de trusted imports; no es un sandbox ni una
+prueba de seguridad para SQL no confiable.
+
+
+## Seguridad de storage paths
+
+`make check-env` ejecuta
+[`scripts/validate-storage-paths.sh`](../../../scripts/validate-storage-paths.sh).
+Los data
+paths deben estar estrictamente bajo `data/` y los sample paths bajo
+`samples/`; se rechazan symlinks, rutas iguales, anidadas, solapadas o
+reservadas. `make test-storage-paths` prueba estas reglas sin Docker runtime.
+
+
+## Importación de SQL de confianza
+
+```bash
+make mysql-import FILE=path/to/file.sql DATABASE=demo
+make postgres-import FILE=path/to/file.sql DATABASE=demo
+```
+
+Para ambos targets:
+
+- `FILE` y `DATABASE` son obligatorios.
+- El archivo debe existir, ser legible, no estar vacío y ser plain SQL local.
+- La base debe existir; las bases de sistema están prohibidas.
+- El import se ejecuta como `DB_USER`; no crea la base ni concede grants.
+- `DATABASE` elige la conexión inicial, pero no crea un sandbox.
+- Nombres cualificados, comandos client/session y los grants reales pueden
+ afectar otros objetos accesibles.
+- Puede producirse una ejecución parcial; no se promete rollback automático.
+- Haga backup antes de una importación importante.
+- gzip, archivos y backups PostgreSQL custom-format no están soportados.
+
+
+## Backup
+
+Los targets integrados cubren solo la base MySQL `demo` configurada:
+
+```bash
+make dump
+make restore
+```
+
+`make dump` escribe `backup/demo.sql`; `make restore` lo lee y reaplica los
+grants didácticos de MySQL. Use otro procedimiento para PostgreSQL.
+
+
+## Limpieza y reinicialización
+
+> **Advertencia:** todos los targets `clean-*` y `reinit-*` son destructivos y
+> exigen la confirmación exacta `CONFIRM=1`.
+
+```bash
+make clean-mysql CONFIRM=1
+make clean-postgres CONFIRM=1
+make clean-all CONFIRM=1
+
+make reinit-mysql CONFIRM=1
+make reinit-postgres CONFIRM=1
+make reinit-all CONFIRM=1
+```
+
+Los targets individuales borran solo los datos del SGBD elegido; los de tipo
+`all` borran ambos. Conservan configuración, init, samples y backups. La
+reinicialización vuelve a iniciar y comprobar los SGBD seleccionados;
+`reinit-all` inicia MySQL, PostgreSQL y Adminer, y después realiza la
+comprobación común de acceso.
+
+[Volver al README](../README_es.md)
diff --git a/docs/langs/es/troubleshooting.md b/docs/langs/es/troubleshooting.md
new file mode 100644
index 0000000..cc88be9
--- /dev/null
+++ b/docs/langs/es/troubleshooting.md
@@ -0,0 +1,62 @@
+# Diagnóstico
+
+[← Volver al README](../README_es.md)
+
+## Idioma
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/troubleshooting.md) | [English](../en/troubleshooting.md) | **Seleccionado** | [中文](../zh/troubleshooting.md) | [Français](../fr/troubleshooting.md) | [Deutsch](../de/troubleshooting.md) |
+
+## Sección
+
+| Primeros pasos | Bases y samples | Comprobaciones y operaciones | Diagnóstico |
+| --- | --- | --- | --- |
+| [Primeros pasos](getting-started.md) | [Bases y samples](databases.md) | [Comprobaciones y operaciones](operations.md) | **Seleccionado** |
+
+Primero reúna diagnósticos y después corrija la causa concreta. Si aún
+hace falta reinicializar, haga backup solo de los datos propios que necesite
+conservar y use `reinit-... CONFIRM=1` solo como última medida deliberada.
+
+Detalles canónicos del ciclo de vida y las operaciones: [bases](databases.md#section-initialization) · [operaciones](operations.md#section-clean-reinitialize).
+
+
+## Falla la configuración o la validación de rutas
+
+Ejecute `make check-env`. Los data paths deben estar estrictamente bajo
+`data/` y los samples bajo `samples/`; no pueden contener symlinks, solaparse
+ni usar directorios reservados. Corrija `.docker.env` y ejecute `make config`.
+
+
+## Un servicio no está listo
+
+```bash
+make status
+make log SERVICE=mysql
+make log SERVICE=postgres
+```
+
+Compruebe Docker, el puerto, `.docker.env` y los logs antes de tocar datos.
+
+
+## No aparecen cambios de init o samples
+
+Es normal si data ya estaba inicializado. Revise las rutas y la preparación;
+si necesita conservar datos importantes, haga backup y use
+`reinit-... CONFIRM=1` solo como último paso deliberado.
+
+
+## Un sample está incompleto o tiene propietario inesperado
+
+El loader no sobrescribe ni repara bases inesperadas. Repita
+`make samples-mysql` o `make samples-postgres`, inspeccione el error y preserve
+los datos antes de reinicializar.
+
+
+## Un cliente no conecta
+
+Los clientes del host usan la dirección publicada y `MYSQL_PORT` o
+`POSTGRES_PORT`; Adminer usa `mysql` o `postgres` dentro de Compose. Revise
+`BIND_ADDRESS`, firewall, base seleccionada y credenciales `DB_USER`.
+
+[Volver al README](../README_es.md)
diff --git a/docs/langs/fr/databases.md b/docs/langs/fr/databases.md
new file mode 100644
index 0000000..7f80f06
--- /dev/null
+++ b/docs/langs/fr/databases.md
@@ -0,0 +1,124 @@
+# Bases et samples
+
+[← Retour au README](../README_fr.md)
+
+## Langue
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/databases.md) | [English](../en/databases.md) | [Español](../es/databases.md) | [中文](../zh/databases.md) | **Sélectionné** | [Deutsch](../de/databases.md) |
+
+## Section
+
+| Prise en main | Bases et samples | Contrôles et opérations | Diagnostic et dépannage |
+| --- | --- | --- | --- |
+| [Prise en main](getting-started.md) | **Sélectionné** | [Contrôles et opérations](operations.md) | [Diagnostic et dépannage](troubleshooting.md) |
+
+
+## Bases `demo` obligatoires
+
+Les deux SGBD initialisent `demo` :
+
+- MySQL : `demo.demo_users`
+- PostgreSQL : `demo.public.demo_users`
+
+Les tables ont les champs équivalents `id`, `name`, `email` et `created_at`,
+avec Alice, Bob, Carol, Dave et Eve. Les contrôles acceptent des lignes
+supplémentaires. `make check-env` impose `MYSQL_DATABASE=demo` et
+`POSTGRES_DATABASE=demo`.
+
+
+## Samples optionnels
+
+| SGBD | Bases optionnelles | Préparation |
+|---|---|---|
+| MySQL | `chinook`, `sakila` | `make samples-mysql` |
+| PostgreSQL | `pagila`, `chinook` | `make samples-postgres` |
+
+
+## Préparation des samples
+
+La préparation demande `curl` et `git` ; les samples MySQL exigent aussi `unzip` et `sha256sum`.
+
+La préparation télécharge et vérifie des fichiers upstream épinglés, sans
+démarrer les conteneurs ni importer dans une base déjà initialisée. Les
+téléchargements temporaires restent locaux, ne sont pas commités et sont
+placés sous `MYSQL_SAMPLES_DIR` ou `POSTGRES_SAMPLES_DIR`. Provenance,
+intégrité et licences figurent dans
+[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md).
+
+Consultez [Initialisation et cycle de vie](#section-initialization) pour le
+moment de la préparation et la réinitialisation d’un SGBD existant.
+
+Un sample totalement absent est ignoré et n’empêche pas la création de `demo`.
+Un ensemble incomplet ou une base inattendue est refusé, sans réparation ni
+suppression automatique.
+
+
+## Organisation du stockage
+
+```text
+data/
+├── mysql/
+└── postgres/
+
+initdb/
+├── mysql/
+└── postgres/
+```
+
+| SGBD | Data | Init | Samples optionnels |
+|---|---|---|---|
+| MySQL | `MYSQL_DATA_DIR` (`./data/mysql`) | `MYSQL_INITDB_DIR` (`./initdb/mysql`) | `MYSQL_SAMPLES_DIR` (`./samples/mysql`) |
+| PostgreSQL | `POSTGRES_DATA_DIR` (`./data/postgres`) | `POSTGRES_INITDB_DIR` (`./initdb/postgres`) | `POSTGRES_SAMPLES_DIR` (`./samples/postgres`) |
+
+Les chemins data et samples se configurent dans `.docker.env` sous contrôle
+des managed paths. Les règles des répertoires init sont décrites dans
+[Initialisation et cycle de vie](#section-initialization). Ne modifiez pas
+manuellement les fichiers de `data/` : ils peuvent appartenir à des UID/GID
+numériques du conteneur.
+
+
+## Initialisation et cycle de vie
+
+> **Important :** Les entrypoints officiels MySQL et PostgreSQL n’exécutent
+> init que si le data directory est vide. Ajouter des fichiers après
+> l’initialisation ne modifie pas une base existante. `make down` conserve les
+> données, tandis qu’une réinitialisation confirmée supprime les données
+> actuelles du SGBD choisi et recrée les bases depuis les scripts init actuels.
+> Sauvegardez uniquement les données personnelles à conserver ; un lab ponctuel sans changement précieux ne l’exige pas.
+
+Pour une première initialisation avec samples, préparez-les avant le premier
+démarrage :
+
+```bash
+make samples-mysql
+make up-mysql
+
+make samples-postgres
+make up-postgres
+```
+
+Pour un SGBD déjà initialisé, conservez les données personnelles si nécessaire,
+puis utilisez uniquement sa réinitialisation confirmée correspondante :
+
+```bash
+make samples-mysql
+make reinit-mysql CONFIRM=1
+
+make samples-postgres
+make reinit-postgres CONFIRM=1
+```
+
+
+## Accès pédagogique et ownership
+
+MySQL crée `DB_USER` et lui accorde l’accès à toutes les bases non
+système trouvées pendant init. PostgreSQL crée un `DB_USER` distinct, sans
+superuser/createdb/createrole, et le rend propriétaire de `demo`, du schéma
+`public` et des objets sample. Les credentials administratifs restent
+séparés : `MYSQL_ROOT_PASSWORD`, `POSTGRES_SUPERUSER` et
+`POSTGRES_SUPERUSER_PASSWORD`. Ne modifiez pas manuellement les fichiers de
+`data/` appartenant aux conteneurs.
+
+[Retour au README](../README_fr.md)
diff --git a/docs/langs/fr/getting-started.md b/docs/langs/fr/getting-started.md
new file mode 100644
index 0000000..b3b5c10
--- /dev/null
+++ b/docs/langs/fr/getting-started.md
@@ -0,0 +1,167 @@
+# Prise en main
+
+[← Retour au README](../README_fr.md)
+
+## Langue
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/getting-started.md) | [English](../en/getting-started.md) | [Español](../es/getting-started.md) | [中文](../zh/getting-started.md) | **Sélectionné** | [Deutsch](../de/getting-started.md) |
+
+## Section
+
+| Prise en main | Bases et samples | Contrôles et opérations | Diagnostic et dépannage |
+| --- | --- | --- | --- |
+| **Sélectionné** | [Bases et samples](databases.md) | [Contrôles et opérations](operations.md) | [Diagnostic et dépannage](troubleshooting.md) |
+
+
+## Prérequis
+
+- Docker Engine ou Docker Desktop avec Docker Compose v2.
+- GNU Make, Bash et les utilitaires Unix de base en ligne de commande.
+- Environnements recommandés : Linux ; macOS avec Docker Desktop ; ou Windows
+ avec Docker Desktop et WSL2.
+
+Exécutez les commandes depuis la racine du dépôt. La branche par défaut du
+projet est `master`.
+
+
+## Démarrage rapide
+
+`make init` crée un `.docker.env` local depuis
+[`.docker.env.example`](../../../.docker.env.example), valide les chemins de
+stockage gérés et crée les répertoires de travail. Démarrez ensuite le
+laboratoire complet :
+
+```bash
+make init
+make up
+```
+
+`make up` démarre MySQL, PostgreSQL et Adminer. Avec la configuration par
+défaut, Adminer est disponible sur `http://127.0.0.1:8081`.
+
+Utilisez `make up-no-ui` pour démarrer les deux SGBD sans Adminer. La base
+obligatoire `demo` est toujours créée à la première initialisation ; les jeux
+de données d’exemple sont optionnels. Pour les inclure à la première
+initialisation, préparez-les avant le premier `make up`. Si les répertoires de
+données sont déjà initialisés, sauvegardez-les avant une réinitialisation
+confirmée uniquement si vous devez conserver des données personnelles. Consultez la procédure exacte dans
+[Initialisation et cycle de vie](databases.md#section-initialization).
+
+```bash
+make status
+make logs
+make down
+```
+
+`make down` supprime les conteneurs et le réseau, mais conserve les données
+bind-mounted.
+
+
+## Modes de démarrage
+
+| Commande | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Démarre | Démarre | Démarre |
+| `make up-no-ui` | Démarre | Démarre | Arrête |
+| `make up-mysql` | Démarre | Ne démarre pas | Ne démarre pas |
+| `make up-postgres` | Ne démarre pas | Démarre | Ne démarre pas |
+
+Les commandes d’un SGBD n’arrêtent pas l’autre déjà actif ; Adminer se gère séparément.
+
+
+📋 Tableau complet des modes de démarrage
+
+| Commande | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Démarre | Démarre | Démarre |
+| `make up-no-ui` | Démarre ou laisse actif | Démarre ou laisse actif | Arrête s’il est actif |
+| `make up-mysql` | Démarre | Ne démarre pas automatiquement | Ne démarre pas automatiquement |
+| `make up-mysql-ui` | Démarre | Ne démarre pas automatiquement | Démarre |
+| `make up-postgres` | Ne démarre pas automatiquement | Démarre | Ne démarre pas automatiquement |
+| `make up-postgres-ui` | Ne démarre pas automatiquement | Démarre | Démarre |
+| `make up-ui` | Ne change rien | Ne change rien | Démarre |
+| `make down-ui` | Ne change rien | Ne change rien | Arrête |
+
+Les commandes dédiées à un SGBD n’arrêtent pas l’autre s’il fonctionne déjà.
+Adminer se démarre et s’arrête séparément et n’est pas lié uniquement à MySQL.
+
+
+
+
+## Connexions
+
+### Adminer
+
+Dans le réseau Compose, Adminer propose deux serveurs prédéfinis :
+
+```text
+MySQL (mysql)
+PostgreSQL (postgres)
+```
+
+Choisissez un serveur, puis saisissez `DB_USER`, `DB_PASSWORD` et une base telle
+que `demo`. `mysql` et `postgres` sont des noms internes au réseau Compose, pas
+des noms d’hôte pour les clients desktop.
+
+### Clients sur l’hôte
+
+| SGBD | Hôte par défaut | Variable de port | Utilisateur | Base par défaut |
+|---|---|---|---|---|
+| MySQL | `127.0.0.1` | `MYSQL_PORT` | `DB_USER` | `demo` |
+| PostgreSQL | `127.0.0.1` | `POSTGRES_PORT` | `DB_USER` | `demo` |
+
+DataGrip, DBeaver, PhpStorm et les CLI de l’hôte utilisent l’adresse et le port
+publiés. Si `BIND_ADDRESS` change, utilisez l’adresse joignable de l’interface.
+
+### CLI dans les conteneurs
+
+Les mots de passe passent par l’environnement du conteneur et non par
+l’historique du shell :
+
+```bash
+make mysql # administrateur MySQL, base demo
+make mysql-user # DB_USER, base demo
+make postgres # superutilisateur PostgreSQL, base demo
+make postgres-user # DB_USER, base demo
+```
+
+
+## Identifiants
+
+`.docker.env` est créé depuis [`.docker.env.example`](../../../.docker.env.example) et ignoré par Git. Gardez
+les mots de passe dans ce fichier ; ne les codez pas en dur dans Compose, SQL
+ou une configuration client versionnée.
+
+| Usage | Utilisateur | Mot de passe |
+|---|---|---|
+| Utilisateur pédagogique commun | `DB_USER` | `DB_PASSWORD` |
+| Administrateur MySQL | `root` | `MYSQL_ROOT_PASSWORD` |
+| Administrateur/superutilisateur PostgreSQL | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` et `DB_USER` doivent être deux rôles différents. Utilisez
+le compte pédagogique pour les exercices courants. Remplacez les mots de passe
+d’exemple avant tout partage ou toute publication de service.
+
+
+## Ports et `BIND_ADDRESS`
+
+| Service | Variable | Valeur d’exemple |
+|---|---|---|
+| MySQL | `MYSQL_PORT` | `3306` |
+| PostgreSQL | `POSTGRES_PORT` | `5432` |
+| Adminer | `ADMINER_PORT` | `8081` |
+
+Par défaut, les trois services ne sont publiés que sur loopback :
+
+```env
+BIND_ADDRESS=127.0.0.1
+```
+
+`127.0.0.1` est la valeur locale par défaut. `BIND_ADDRESS=0.0.0.0` publie les
+ports sur toutes les interfaces. Pour un LAN ou VPN, préférez l’adresse d’une
+interface précise. Ce changement doit être volontaire et tenir compte du
+firewall, de la robustesse des mots de passe et de la confiance accordée au réseau.
+
+[Retour au README](../README_fr.md)
diff --git a/docs/langs/fr/operations.md b/docs/langs/fr/operations.md
new file mode 100644
index 0000000..1c7bc49
--- /dev/null
+++ b/docs/langs/fr/operations.md
@@ -0,0 +1,162 @@
+# Contrôles et opérations
+
+[← Retour au README](../README_fr.md)
+
+## Langue
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/operations.md) | [English](../en/operations.md) | [Español](../es/operations.md) | [中文](../zh/operations.md) | **Sélectionné** | [Deutsch](../de/operations.md) |
+
+## Section
+
+| Prise en main | Bases et samples | Contrôles et opérations | Diagnostic et dépannage |
+| --- | --- | --- | --- |
+| [Prise en main](getting-started.md) | [Bases et samples](databases.md) | **Sélectionné** | [Diagnostic et dépannage](troubleshooting.md) |
+
+
+## Targets Make publics
+
+Les targets publics et leur implémentation figurent dans le [`Makefile`](../../../Makefile).
+
+Targets essentiels : `make init`, `make up`, `make down`, `make check`,
+`make test-storage-paths`, `make test-sql-imports`, `make mysql-import` et
+`make postgres-import`. Le SQL de confiance n’est pas un sandbox, peut être
+exécuté partiellement et n’offre aucun rollback automatique garanti ; faites
+un backup avant tout import important. Les backups intégrés couvrent seulement
+MySQL `demo`, pas PostgreSQL. `clean-*` et `reinit-*` sont destructifs et
+exigent `CONFIRM=1` exact.
+
+
+📋 Référence complète des targets Make publics
+
+`make help` affiche la liste concise.
+
+| Commande | Rôle |
+|---|---|
+| `make init` | Créer `.docker.env`, valider les chemins et créer les répertoires |
+| `make check-env` | Vérifier env, rôles, noms de bases et managed paths |
+| `make pull` | Télécharger les trois images épinglées |
+| `make config` | Valider la configuration Compose développée |
+| `make up`, `make up-no-ui` | Démarrer les deux SGBD avec ou sans Adminer |
+| `make up-mysql`, `make up-mysql-ui` | Démarrer MySQL, avec Adminer en option |
+| `make up-postgres`, `make up-postgres-ui` | Démarrer PostgreSQL, avec Adminer en option |
+| `make up-ui`, `make down-ui` | Démarrer ou arrêter seulement Adminer |
+| `make down` | Arrêter sans supprimer les données bind-mounted |
+| `make status`, `make logs` | Afficher l’état ou suivre tous les logs |
+| `make log SERVICE=postgres` | Suivre un service ; `make log postgres` fonctionne aussi |
+| `make in SERVICE=postgres` | Ouvrir un shell ; `make in postgres` fonctionne aussi |
+| `make mysql`, `make mysql-user` | Ouvrir MySQL comme admin ou `DB_USER` |
+| `make postgres`, `make postgres-user` | Ouvrir PostgreSQL comme superutilisateur ou `DB_USER` |
+| `make samples-mysql`, `make samples-postgres` | Préparer les samples vérifiés |
+| `make check-mysql-access`, `make check-postgres-access` | Contrôler un SGBD actif |
+| `make check` | Valider Compose et l’accès `DB_USER` aux deux SGBD |
+| `make test-storage-paths` | Tester les managed paths sans Docker runtime |
+| `make test-sql-imports` | Smoke-test des deux imports SQL publics |
+| `make mysql-import FILE=... DATABASE=...` | Importer du plain SQL dans MySQL comme `DB_USER` |
+| `make postgres-import FILE=... DATABASE=...` | Importer du plain SQL dans PostgreSQL comme `DB_USER` |
+| `make dump`, `make restore` | Sauvegarder ou restaurer `demo` dans MySQL |
+| `make clean-{mysql,postgres,all} CONFIRM=1` | Supprimer les data directories choisis |
+| `make reinit-{mysql,postgres,all} CONFIRM=1` | Supprimer, recréer et contrôler les bases |
+
+
+
+
+## Contrôles
+
+### Contrôles statiques et locaux
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+`make check-env` crée `.docker.env` s’il manque, puis valide valeurs et chemins.
+`make config` valide le modèle Compose développé. `make test-storage-paths` ne
+demande pas de Docker runtime : il teste les sorties du projet, composants
+symlink, chemins qui se chevauchent ou s’imbriquent et répertoires réservés.
+
+### Contrôles runtime
+
+```bash
+make up-no-ui
+make check
+make test-sql-imports
+make down
+```
+
+`make check` valide `demo` et l’accès réel de `DB_USER` aux deux SGBD, ainsi que
+les samples installés. `make test-sql-imports` exige que les deux SGBD soient
+actifs ; il appelle `mysql-import` et `postgres-import`, crée des tables
+temporaires aux noms uniques dans `demo`, vérifie les marker rows comme
+`DB_USER` puis ne supprime que ces tables. C’est un smoke-test du flux trusted
+import, ni un sandbox ni une preuve de sécurité pour du SQL non fiable.
+
+
+## Sécurité des storage paths
+
+`make check-env` exécute
+[`scripts/validate-storage-paths.sh`](../../../scripts/validate-storage-paths.sh).
+Les data
+paths doivent rester strictement sous `data/`, les sample paths sous
+`samples/` ; symlinks, chemins égaux, imbriqués, chevauchants ou réservés
+sont refusés. `make test-storage-paths` teste ces règles sans Docker runtime.
+
+
+## Imports SQL de confiance
+
+```bash
+make mysql-import FILE=path/to/file.sql DATABASE=demo
+make postgres-import FILE=path/to/file.sql DATABASE=demo
+```
+
+Pour les deux targets :
+
+- `FILE` et `DATABASE` sont obligatoires.
+- Le fichier local plain SQL doit exister, être lisible et non vide.
+- La base doit déjà exister ; les bases système sont interdites.
+- L’import s’exécute comme `DB_USER`, sans créer de base ni accorder de grants.
+- `DATABASE` choisit la connexion initiale, mais ne crée pas de sandbox.
+- Les qualified names, commandes client/session et grants effectifs peuvent
+ atteindre d’autres objets accessibles.
+- Une exécution partielle est possible ; aucun rollback automatique n’est promis.
+- Effectuez une sauvegarde avant un import important.
+- gzip, archives et backups PostgreSQL custom-format ne sont pas pris en charge.
+
+
+## Sauvegarde
+
+Les targets intégrés couvrent uniquement la base MySQL `demo` configurée :
+
+```bash
+make dump
+make restore
+```
+
+`make dump` écrit `backup/demo.sql` ; `make restore` le lit et réapplique les
+grants pédagogiques MySQL. Utilisez une procédure distincte pour PostgreSQL.
+
+
+## Nettoyage et réinitialisation
+
+> **Attention :** tous les targets `clean-*` et `reinit-*` sont destructifs et
+> exigent la confirmation exacte `CONFIRM=1`.
+
+```bash
+make clean-mysql CONFIRM=1
+make clean-postgres CONFIRM=1
+make clean-all CONFIRM=1
+
+make reinit-mysql CONFIRM=1
+make reinit-postgres CONFIRM=1
+make reinit-all CONFIRM=1
+```
+
+Les commandes unitaires ne suppriment que les données du SGBD choisi ; `all`
+supprime celles des deux. Configuration, init, samples et backups restent en
+place. La réinitialisation redémarre et contrôle les SGBD choisis ;
+`reinit-all` démarre MySQL, PostgreSQL et Adminer, puis effectue le contrôle
+d’accès commun.
+
+[Retour au README](../README_fr.md)
diff --git a/docs/langs/fr/troubleshooting.md b/docs/langs/fr/troubleshooting.md
new file mode 100644
index 0000000..b503448
--- /dev/null
+++ b/docs/langs/fr/troubleshooting.md
@@ -0,0 +1,62 @@
+# Diagnostic et dépannage
+
+[← Retour au README](../README_fr.md)
+
+## Langue
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/troubleshooting.md) | [English](../en/troubleshooting.md) | [Español](../es/troubleshooting.md) | [中文](../zh/troubleshooting.md) | **Sélectionné** | [Deutsch](../de/troubleshooting.md) |
+
+## Section
+
+| Prise en main | Bases et samples | Contrôles et opérations | Diagnostic et dépannage |
+| --- | --- | --- | --- |
+| [Prise en main](getting-started.md) | [Bases et samples](databases.md) | [Contrôles et opérations](operations.md) | **Sélectionné** |
+
+Collectez d’abord le diagnostic, puis corrigez la cause précise. Si une
+réinitialisation reste nécessaire, sauvegardez seulement les données
+personnelles à conserver et n’utilisez `reinit-... CONFIRM=1` qu’en dernier recours volontaire.
+
+Détails canoniques du cycle de vie et des opérations : [bases](databases.md#section-initialization) · [opérations](operations.md#section-clean-reinitialize).
+
+
+## Échec de configuration ou de validation des chemins
+
+Lancez `make check-env`. Les data paths doivent rester strictement sous
+`data/` et les samples sous `samples/` ; aucun symlink, chevauchement ou
+répertoire réservé n’est admis. Corrigez `.docker.env` puis lancez `make config`.
+
+
+## Un service n’est pas prêt
+
+```bash
+make status
+make log SERVICE=mysql
+make log SERVICE=postgres
+```
+
+Vérifiez Docker, le port, `.docker.env` et les logs avant de toucher aux données.
+
+
+## Les changements init ou les samples n’apparaissent pas
+
+C’est normal si data est déjà initialisé. Vérifiez chemins et préparation ; si
+des données importantes doivent être conservées, sauvegardez-les et n’utilisez
+`reinit-... CONFIRM=1` qu’en dernier recours volontaire.
+
+
+## Sample incomplet ou propriétaire inattendu
+
+Le loader ne remplace ni ne répare une base inattendue. Relancez
+`make samples-mysql` ou `make samples-postgres` et inspectez l’erreur ;
+préservez les données avant toute réinitialisation.
+
+
+## Un client ne se connecte pas
+
+Les clients hôtes utilisent l’adresse publiée et `MYSQL_PORT` ou
+`POSTGRES_PORT` ; Adminer utilise `mysql` ou `postgres` dans Compose. Vérifiez
+`BIND_ADDRESS`, firewall, base sélectionnée et identifiants `DB_USER`.
+
+[Retour au README](../README_fr.md)
diff --git a/docs/langs/ru/databases.md b/docs/langs/ru/databases.md
new file mode 100644
index 0000000..9dc5c3c
--- /dev/null
+++ b/docs/langs/ru/databases.md
@@ -0,0 +1,134 @@
+# Базы и учебные данные
+
+[← Вернуться к README](../../../README.md)
+
+## Язык
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| **Выбран** | [English](../en/databases.md) | [Español](../es/databases.md) | [中文](../zh/databases.md) | [Français](../fr/databases.md) | [Deutsch](../de/databases.md) |
+
+## Раздел
+
+| Начало работы | Базы и учебные данные | Проверки и эксплуатация | Диагностика |
+| --- | --- | --- | --- |
+| [Начало работы](getting-started.md) | **Выбран** | [Проверки и эксплуатация](operations.md) | [Диагностика](troubleshooting.md) |
+
+
+## Обязательные базы `demo`
+
+Обе СУБД инициализируют обязательную базу с именем `demo`:
+
+- MySQL: `demo.demo_users`
+- PostgreSQL: `demo.public.demo_users`
+
+Таблицы содержат эквивалентные поля `id`, `name`, `email`, `created_at` и
+одинаковые пять обязательных пользователей: Alice, Bob, Carol, Dave и Eve.
+Проверки допускают дополнительные строки, созданные пользователем.
+
+Имена баз по умолчанию задаются как `MYSQL_DATABASE=demo` и
+`POSTGRES_DATABASE=demo`; `make check-env` требует именно эти значения.
+
+
+## Необязательные учебные базы
+
+| СУБД | Необязательные базы | Команда подготовки |
+|---|---|---|
+| MySQL | `chinook`, `sakila` | `make samples-mysql` |
+| PostgreSQL | `pagila`, `chinook` | `make samples-postgres` |
+
+
+## Подготовка учебных баз
+
+Для подготовки нужны `curl` и `git`; учебные базы MySQL дополнительно требуют
+`unzip` и `sha256sum`.
+
+Команды подготовки загружают и проверяют закреплённые файлы исходных проектов, но не
+запускают контейнеры и не импортируют данные в уже инициализированную СУБД.
+Загрузки остаются локальными, исключаются из Git и сохраняются в
+`MYSQL_SAMPLES_DIR` или `POSTGRES_SAMPLES_DIR`. Происхождение, контрольные
+значения целостности и лицензии описаны в
+[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md).
+
+Порядок подготовки относительно первого запуска и повторной инициализации
+описан в разделе [«Инициализация и жизненный цикл»](#section-initialization).
+
+Полностью отсутствующая необязательная учебная база пропускается и не мешает
+создать обязательную `demo`. Частичный набор файлов учебных баз или неожиданно
+существующая учебная база отклоняются без автоматического исправления или
+удаления.
+
+
+## Структура хранения
+
+Каталоги хоста для хранения данных по умолчанию разделены по СУБД:
+
+```text
+data/
+├── mysql/
+└── postgres/
+
+initdb/
+├── mysql/
+└── postgres/
+```
+
+Связанные настройки `.docker.env` также разделены:
+
+| СУБД | Данные | Инициализация | Необязательные учебные базы |
+|---|---|---|---|
+| MySQL | `MYSQL_DATA_DIR` (`./data/mysql`) | `MYSQL_INITDB_DIR` (`./initdb/mysql`) | `MYSQL_SAMPLES_DIR` (`./samples/mysql`) |
+| PostgreSQL | `POSTGRES_DATA_DIR` (`./data/postgres`) | `POSTGRES_INITDB_DIR` (`./initdb/postgres`) | `POSTGRES_SAMPLES_DIR` (`./samples/postgres`) |
+
+Пути данных и учебных баз можно изменить через `.docker.env` с учётом проверки
+контролируемых путей хранения. Правила применения каталогов инициализации
+описаны в разделе [«Инициализация и жизненный цикл»](#section-initialization).
+
+Не редактируйте файлы СУБД внутри `data/` вручную. Файлы, принадлежащие
+контейнерам, могут иметь числовые UID/GID, отличающиеся от пользователя хоста.
+
+
+## Инициализация и жизненный цикл
+
+> **Важно:** Официальные точки входа MySQL и PostgreSQL выполняют файлы
+> инициализации только при пустом каталоге данных. Добавление файлов после
+> инициализации не изменяет существующую базу. `make down` сохраняет данные,
+> а подтверждённая переинициализация удаляет текущие данные выбранной СУБД и
+> создаёт базы заново из актуальных init-скриптов. Резервная копия нужна, если
+> требуется сохранить собственные данные; для одноразового учебного стенда без
+> ценных изменений она не обязательна.
+
+Для первой инициализации с учебными базами подготовьте их до первого запуска:
+
+```bash
+make samples-mysql
+make up-mysql
+
+make samples-postgres
+make up-postgres
+```
+
+Для уже инициализированной СУБД при необходимости сначала сохраните
+собственные данные, затем используйте только соответствующую подтверждённую
+переинициализацию:
+
+```bash
+make samples-mysql
+make reinit-mysql CONFIRM=1
+
+make samples-postgres
+make reinit-postgres CONFIRM=1
+```
+
+
+## Доступ учебного пользователя и владение объектами
+
+MySQL создаёт `DB_USER` и выдаёт ему права на все пользовательские
+базы, обнаруженные во время инициализации. PostgreSQL создаёт отдельную роль
+`DB_USER` без прав `superuser`, `createdb` и `createrole` и назначает её владельцем
+`demo`, схемы `public` и загруженных объектов учебных баз. Административные
+учётные данные остаются отдельными: `MYSQL_ROOT_PASSWORD`,
+`POSTGRES_SUPERUSER` и `POSTGRES_SUPERUSER_PASSWORD`. Не редактируйте
+файлы в `data/`, принадлежащие контейнерам, вручную.
+
+[Вернуться к README](../../../README.md)
diff --git a/docs/langs/ru/getting-started.md b/docs/langs/ru/getting-started.md
new file mode 100644
index 0000000..2fee3a8
--- /dev/null
+++ b/docs/langs/ru/getting-started.md
@@ -0,0 +1,177 @@
+# Начало работы
+
+[← Вернуться к README](../../../README.md)
+
+## Язык
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| **Выбран** | [English](../en/getting-started.md) | [Español](../es/getting-started.md) | [中文](../zh/getting-started.md) | [Français](../fr/getting-started.md) | [Deutsch](../de/getting-started.md) |
+
+## Раздел
+
+| Начало работы | Базы и учебные данные | Проверки и эксплуатация | Диагностика |
+| --- | --- | --- | --- |
+| **Выбран** | [Базы и учебные данные](databases.md) | [Проверки и эксплуатация](operations.md) | [Диагностика](troubleshooting.md) |
+
+
+## Требования
+
+- Docker Engine или Docker Desktop с Docker Compose v2.
+- GNU Make, Bash и базовые утилиты командной строки Unix.
+- Рекомендуемая среда: Linux; macOS с Docker Desktop; Windows с Docker Desktop
+ и WSL2.
+
+Выполняйте команды из корня репозитория. Ветка проекта по умолчанию — `master`.
+
+
+## Быстрый старт
+
+`make init` создаёт локальный `.docker.env` из
+[`.docker.env.example`](../../../.docker.env.example), проверяет контролируемые
+пути хранения и создаёт рабочие каталоги. Затем запустите полный стенд:
+
+```bash
+make init
+make up
+```
+
+`make up` запускает MySQL, PostgreSQL и Adminer. С конфигурацией по умолчанию
+Adminer доступен по адресу `http://127.0.0.1:8081`.
+
+Чтобы запустить обе СУБД без Adminer, используйте `make up-no-ui`. При первой
+инициализации всегда создаётся обязательная база `demo`; учебные базы
+необязательны. Если они нужны при первой инициализации, подготовьте их до
+первого `make up`. Если нужно сохранить собственные данные из уже
+инициализированных каталогов, перед подтверждённой переинициализацией создайте
+резервную копию. Точный порядок действий приведён в разделе
+[«Инициализация и жизненный цикл»](databases.md#section-initialization).
+
+Полезные следующие команды:
+
+```bash
+make status
+make logs
+make down
+```
+
+`make down` удаляет контейнеры и сеть, но сохраняет данные СУБД в каталогах
+хоста, подключённых в контейнеры.
+
+
+## Режимы запуска
+
+| Команда | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Запускает | Запускает | Запускает |
+| `make up-no-ui` | Запускает | Запускает | Останавливает |
+| `make up-mysql` | Запускает | Не запускает | Не запускает |
+| `make up-postgres` | Не запускает | Запускает | Не запускает |
+
+Команды одной СУБД не останавливают уже работающую другую; Adminer управляется отдельно.
+
+
+📋 Полная таблица режимов запуска
+
+| Команда | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | Запускает | Запускает | Запускает |
+| `make up-no-ui` | Запускает или оставляет активным | Запускает или оставляет активным | Останавливает, если запущен |
+| `make up-mysql` | Запускает | Не запускает автоматически | Не запускает автоматически |
+| `make up-mysql-ui` | Запускает | Не запускает автоматически | Запускает |
+| `make up-postgres` | Не запускает автоматически | Запускает | Не запускает автоматически |
+| `make up-postgres-ui` | Не запускает автоматически | Запускает | Запускает |
+| `make up-ui` | Не меняет состояние | Не меняет состояние | Запускает |
+| `make down-ui` | Не меняет состояние | Не меняет состояние | Останавливает |
+
+Команды одиночного запуска не останавливают другую уже работающую СУБД.
+Adminer можно запускать и останавливать отдельно; он не привязан только к
+MySQL.
+
+
+
+
+## Подключения
+
+### Adminer
+
+Внутри Compose-сети Adminer предлагает два заранее заданных сервера:
+
+```text
+MySQL (mysql)
+PostgreSQL (postgres)
+```
+
+Выберите сервер, затем укажите `DB_USER`, `DB_PASSWORD` и имя базы, например
+`demo`. Имена сервисов `mysql` и `postgres` работают внутри Compose-сети; для
+клиентов на компьютере пользователя это не адреса серверов.
+
+### Клиенты на хосте
+
+DataGrip, DBeaver, PhpStorm и клиенты командной строки на компьютере
+пользователя подключаются через опубликованный адрес и порт:
+
+| СУБД | Хост по умолчанию | Переменная порта | Пользователь | База по умолчанию |
+|---|---|---|---|---|
+| MySQL | `127.0.0.1` | `MYSQL_PORT` | `DB_USER` | `demo` |
+| PostgreSQL | `127.0.0.1` | `POSTGRES_PORT` | `DB_USER` | `demo` |
+
+Если вы изменили `BIND_ADDRESS`, при необходимости используйте вместо
+`127.0.0.1` доступный адрес этого интерфейса.
+
+### CLI в контейнерах
+
+Цели Make передают пароли через окружение контейнера и не помещают их в
+историю командной оболочки:
+
+```bash
+make mysql # администратор MySQL, база demo
+make mysql-user # DB_USER, база demo
+make postgres # суперпользователь PostgreSQL, база demo
+make postgres-user # DB_USER, база demo
+```
+
+
+## Учётные данные
+
+При копировании [`.docker.env.example`](../../../.docker.env.example) создаётся `.docker.env`; этот файл
+исключён из Git. Храните пароли в нём и не записывайте их напрямую в Compose,
+SQL или отслеживаемую конфигурацию клиентов.
+
+| Назначение | Настройка пользователя | Настройка пароля |
+|---|---|---|
+| Общий учебный пользователь двух СУБД | `DB_USER` | `DB_PASSWORD` |
+| Администратор MySQL | `root` | `MYSQL_ROOT_PASSWORD` |
+| Администратор/суперпользователь PostgreSQL | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` и `DB_USER` должны быть разными ролями. Для обычных
+упражнений используйте общего учебного пользователя, а не `root` или
+суперпользователя.
+Замените пароли из примера до предоставления общего доступа или публикации
+любого сервиса за пределами локального компьютера.
+
+
+## Порты и `BIND_ADDRESS`
+
+Порты хоста настраиваются в `.docker.env`:
+
+| Сервис | Переменная порта | Значение примера |
+|---|---|---|
+| MySQL | `MYSQL_PORT` | `3306` |
+| PostgreSQL | `POSTGRES_PORT` | `5432` |
+| Adminer | `ADMINER_PORT` | `8081` |
+
+По умолчанию все три сервиса публикуются только на интерфейсе обратной петли
+(loopback):
+
+```env
+BIND_ADDRESS=127.0.0.1
+```
+
+Это безопасное значение по умолчанию для локального стенда. Значение
+`BIND_ADDRESS=0.0.0.0` публикует настроенные порты на всех сетевых интерфейсах.
+Для доступа через VPN или LAN предпочтительнее адрес конкретного интерфейса.
+Меняйте привязку осознанно, учитывая правила межсетевого экрана, надёжность паролей и
+доверие ко всем подключённым сетям.
+
+[Вернуться к README](../../../README.md)
diff --git a/docs/langs/ru/operations.md b/docs/langs/ru/operations.md
new file mode 100644
index 0000000..eca1501
--- /dev/null
+++ b/docs/langs/ru/operations.md
@@ -0,0 +1,188 @@
+# Проверки и эксплуатация
+
+[← Вернуться к README](../../../README.md)
+
+## Язык
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| **Выбран** | [English](../en/operations.md) | [Español](../es/operations.md) | [中文](../zh/operations.md) | [Français](../fr/operations.md) | [Deutsch](../de/operations.md) |
+
+## Раздел
+
+| Начало работы | Базы и учебные данные | Проверки и эксплуатация | Диагностика |
+| --- | --- | --- | --- |
+| [Начало работы](getting-started.md) | [Базы и учебные данные](databases.md) | **Выбран** | [Диагностика](troubleshooting.md) |
+
+
+## Команды `Makefile`
+
+Публичные цели и их фактическая реализация находятся в
+[`Makefile`](../../../Makefile).
+
+Ключевые цели: `make init`, `make up`, `make down`, `make check`,
+`make test-storage-paths`, `make test-sql-imports`, `make mysql-import` и
+`make postgres-import`. Импорт доверенного SQL не является изолированной
+средой (`sandbox`), может выполниться частично и не гарантирует автоматический
+откат; перед важным импортом нужна резервная копия. Встроенные цели резервного
+копирования покрывают только MySQL `demo`, но не PostgreSQL. Цели `clean-*` и
+`reinit-*` удаляют данные и требуют точного `CONFIRM=1`.
+
+
+📋 Полный справочник публичных целей Make
+
+Краткий список команд выводит `make help`.
+
+| Команда | Назначение |
+|---|---|
+| `make init` | Создать `.docker.env`, проверить контролируемые пути хранения и создать рабочие каталоги |
+| `make check-env` | Проверить обязательные значения окружения, разделение ролей, имена баз и контролируемые пути хранения |
+| `make pull` | Загрузить три закреплённых образа контейнеров |
+| `make config` | Проверить развёрнутую Compose-конфигурацию |
+| `make up`, `make up-no-ui` | Запустить обе СУБД с Adminer или без него |
+| `make up-mysql`, `make up-mysql-ui` | Запустить MySQL, при необходимости с Adminer |
+| `make up-postgres`, `make up-postgres-ui` | Запустить PostgreSQL, при необходимости с Adminer |
+| `make up-ui`, `make down-ui` | Запустить или остановить только Adminer |
+| `make down` | Остановить стенд без удаления данных в каталогах хоста, подключённых в контейнеры |
+| `make status` | Показать состояние сервисов |
+| `make logs` | Следить за логами всех сервисов |
+| `make log SERVICE=postgres` | Следить за логом одного сервиса; также работает `make log postgres` |
+| `make in SERVICE=postgres` | Открыть командную оболочку сервиса; также работает `make in postgres` |
+| `make mysql`, `make mysql-user` | Открыть MySQL как администратор или `DB_USER` |
+| `make postgres`, `make postgres-user` | Открыть PostgreSQL как суперпользователь или `DB_USER` |
+| `make samples-mysql`, `make samples-postgres` | Подготовить проверенные необязательные учебные базы |
+| `make check-mysql-access`, `make check-postgres-access` | Проверить доступ учебного пользователя к одной запущенной СУБД |
+| `make check` | Проверить Compose и доступ `DB_USER` к двум запущенным СУБД |
+| `make test-storage-paths` | Проверить защиту контролируемых путей хранения без запуска Docker |
+| `make test-sql-imports` | Быстро проверить две публичные цели импорта доверенных SQL-файлов |
+| `make mysql-import FILE=... DATABASE=...` | Импортировать доверенный файл в формате `plain SQL` в существующую базу MySQL от `DB_USER` |
+| `make postgres-import FILE=... DATABASE=...` | Импортировать доверенный файл в формате `plain SQL` в существующую базу PostgreSQL от `DB_USER` |
+| `make dump`, `make restore` | Создать или восстановить dump настроенной MySQL-базы `demo` |
+| `make clean-{mysql,postgres,all} CONFIRM=1` | Удалить выбранные контролируемые каталоги данных |
+| `make reinit-{mysql,postgres,all} CONFIRM=1` | Удалить, пересоздать и проверить выбранные базы |
+
+
+
+
+## Проверки
+
+### Статические и локальные проверки
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+`make check-env` создаёт `.docker.env` из примера, если файла ещё нет, затем
+проверяет обязательные настройки и контролируемые пути хранения. `make config` проверяет
+развёрнутую Compose-модель.
+
+Для `make test-storage-paths` запуск Docker не требуется. Тест проверяет защиту
+от путей за пределами проекта, компонентов символических ссылок,
+пересекающихся или вложенных контролируемых путей и зарезервированных
+каталогов. Та же проверка защищает настроенные каталоги данных и учебных баз
+MySQL/PostgreSQL при обычной инициализации.
+
+### Проверки после запуска
+
+Запустите обе СУБД без Adminer, проверьте учебный доступ и импорт, затем
+остановите сервисы:
+
+```bash
+make up-no-ui
+make check
+make test-sql-imports
+make down
+```
+
+`make check` проверяет обязательные данные `demo` и фактический доступ
+`DB_USER` в обеих СУБД. Если установлены поддерживаемые учебные базы, они
+также проверяются.
+
+Для `make test-sql-imports` обе СУБД должны быть запущены. Тест вызывает
+публичные цели `mysql-import` и `postgres-import`, создаёт уникальные временные
+проверочные таблицы в `demo`, проверяет контрольные записи от имени `DB_USER` и
+удаляет только эти таблицы. Это проверка процесса импорта доверенного SQL, а не
+доказательство безопасности или изолированная среда для недоверенного SQL.
+
+
+## Безопасность путей хранения
+
+`make check-env` запускает
+[`scripts/validate-storage-paths.sh`](../../../scripts/validate-storage-paths.sh).
+Пути данных должны быть строго внутри `data/`, пути учебных баз — внутри
+`samples/`; компоненты символических ссылок, совпадающие, вложенные, пересекающиеся и
+зарезервированные пути отклоняются. `make test-storage-paths` проверяет эти
+ограничения без запуска Docker.
+
+
+## Импорт доверенных SQL-файлов
+
+Импортируйте только доверенные локальные файлы в формате `plain SQL`:
+
+```bash
+make mysql-import FILE=path/to/file.sql DATABASE=demo
+make postgres-import FILE=path/to/file.sql DATABASE=demo
+```
+
+Для обеих целей:
+
+- `FILE` и `DATABASE` обязательны.
+- Файл должен быть существующим, читаемым и непустым обычным файлом.
+- Имя базы должно начинаться со строчной ASCII-буквы и содержать только
+ строчные ASCII-буквы, цифры или `_`; системные базы запрещены.
+- База должна существовать и принимать подключение от `DB_USER`.
+- Импорт выполняется от `DB_USER`, а не от `root` MySQL или суперпользователя
+ PostgreSQL.
+- Цель не создаёт базу и не выдаёт права.
+- Цели не обрабатывают архивы, потоки `gzip` и резервные копии PostgreSQL в
+ формате `custom format`.
+
+`DATABASE` выбирает начальную базу подключения, но не создаёт изолированную
+среду. Полные имена объектов, команды сеанса или клиента и фактические права
+роли `DB_USER` могут разрешить доступ к другим объектам. SQL способен
+изменить или удалить всё, к чему у этой роли есть доступ.
+
+Импорт может выполниться частично. Ни одна цель не обещает автоматический
+полный откат после ошибки. Перед важным импортом изучите файл и создайте
+подходящую резервную копию.
+
+
+## Резервное копирование
+
+Встроенные цели резервного копирования работают только с настроенной базой
+MySQL `demo`:
+
+```bash
+make dump
+make restore
+```
+
+С конфигурацией по умолчанию `make dump` записывает `backup/demo.sql`.
+`make restore` читает этот файл и повторно применяет учебные права MySQL. Для
+сохранения PostgreSQL используйте отдельную процедуру резервного копирования.
+
+
+## Очистка и переинициализация
+
+> **Внимание:** все перечисленные ниже команды `clean-*` и `reinit-*` удаляют
+> данные и требуют точного подтверждения `CONFIRM=1`.
+
+```bash
+make clean-mysql CONFIRM=1
+make clean-postgres CONFIRM=1
+make clean-all CONFIRM=1
+
+make reinit-mysql CONFIRM=1
+make reinit-postgres CONFIRM=1
+make reinit-all CONFIRM=1
+```
+
+Одиночные команды удаляют только каталог данных выбранной СУБД. Варианты
+`all` удаляют каталоги данных обеих СУБД. Конфигурация, файлы инициализации,
+загруженные учебные базы и резервные копии сохраняются. Затем
+переинициализация запускает и проверяет выбранные СУБД; `reinit-all` запускает
+MySQL, PostgreSQL и Adminer, затем выполняет общую проверку доступа.
+
+[Вернуться к README](../../../README.md)
diff --git a/docs/langs/ru/troubleshooting.md b/docs/langs/ru/troubleshooting.md
new file mode 100644
index 0000000..9c744cc
--- /dev/null
+++ b/docs/langs/ru/troubleshooting.md
@@ -0,0 +1,75 @@
+# Диагностика
+
+[← Вернуться к README](../../../README.md)
+
+## Язык
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| **Выбран** | [English](../en/troubleshooting.md) | [Español](../es/troubleshooting.md) | [中文](../zh/troubleshooting.md) | [Français](../fr/troubleshooting.md) | [Deutsch](../de/troubleshooting.md) |
+
+## Раздел
+
+| Начало работы | Базы и учебные данные | Проверки и эксплуатация | Диагностика |
+| --- | --- | --- | --- |
+| [Начало работы](getting-started.md) | [Базы и учебные данные](databases.md) | [Проверки и эксплуатация](operations.md) | **Выбран** |
+
+Сначала соберите диагностику, затем исправьте конкретную причину. Если
+изменение всё же требует переинициализации, сохраните собственные данные, если
+их нужно оставить, и только после этого используйте подтверждённый
+`reinit-... CONFIRM=1` как осознанную последнюю меру.
+
+Канонические описания жизненного цикла и операций: [базы](databases.md#section-initialization) · [операции](operations.md#section-clean-reinitialize).
+
+
+## Ошибка конфигурации или проверки путей хранения
+
+Выполните `make check-env` и найдите в ошибке отклонённую переменную и путь.
+Контролируемые пути данных должны находиться строго внутри каталога проекта
+`data/`, а пути учебных баз — внутри `samples/`. Они не могут содержать
+компоненты символических ссылок,
+пересекаться между собой или использовать зарезервированные каталоги проекта.
+Исправьте `.docker.env`, затем повторите `make check-env` и `make config`.
+
+
+## Сервис не переходит в состояние готовности
+
+До изменения данных проверьте состояние и логи:
+
+```bash
+make status
+make log SERVICE=mysql
+make log SERVICE=postgres
+```
+
+Убедитесь, что Docker запущен, настроенный порт компьютера свободен, а
+`.docker.env` содержит обязательные значения. Исправьте конкретную настройку
+или конфликт порта и снова запустите сервис.
+
+
+## Изменения инициализации или учебных баз не появились
+
+Для уже инициализированного каталога данных это ожидаемое поведение. Проверьте
+настроенные пути данных и учебных баз и успешность команды подготовки. Если
+существующие данные важны, сохраните их. Используйте соответствующую команду
+`reinit-... CONFIRM=1` только как осознанную последнюю меру: она удаляет данные
+этой СУБД.
+
+
+## Учебная база неполна или имеет неожиданного владельца
+
+Загрузчик намеренно не перезаписывает и не исправляет неожиданную базу. Повторите
+подходящую подготовку `make samples-mysql` или `make samples-postgres` и
+изучите ошибку. Сохраните нужные данные, прежде чем рассматривать
+переинициализацию с подтверждением.
+
+
+## Клиент не подключается
+
+Клиенты на компьютере пользователя используют опубликованный адрес и
+`MYSQL_PORT` или
+`POSTGRES_PORT`, а не имя Compose-сервиса. Adminer использует `mysql` или
+`postgres` внутри Compose-сети. Проверьте `BIND_ADDRESS`, межсетевой экран,
+выбранную базу и учётные данные непривилегированного пользователя `DB_USER`.
+
+[Вернуться к README](../../../README.md)
diff --git a/docs/langs/zh/databases.md b/docs/langs/zh/databases.md
new file mode 100644
index 0000000..d892c48
--- /dev/null
+++ b/docs/langs/zh/databases.md
@@ -0,0 +1,114 @@
+# 数据库与 samples
+
+[← 返回 README](../README_zh.md)
+
+## 语言
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/databases.md) | [English](../en/databases.md) | [Español](../es/databases.md) | **已选择** | [Français](../fr/databases.md) | [Deutsch](../de/databases.md) |
+
+## 章节
+
+| 入门 | 数据库与 samples | 检查与运维 | 诊断与故障排除 |
+| --- | --- | --- | --- |
+| [入门](getting-started.md) | **已选择** | [检查与运维](operations.md) | [诊断与故障排除](troubleshooting.md) |
+
+
+## 必需的 `demo` 数据库
+
+两种数据库都会初始化 `demo`:
+
+- MySQL:`demo.demo_users`
+- PostgreSQL:`demo.public.demo_users`
+
+表包含等价字段 `id`、`name`、`email` 和 `created_at`,以及相同的五名
+用户 Alice、Bob、Carol、Dave 和 Eve;检查允许用户添加额外行。
+`make check-env` 要求 `MYSQL_DATABASE=demo` 与 `POSTGRES_DATABASE=demo`。
+
+
+## 可选 samples
+
+| 数据库 | 可选数据库 | 准备命令 |
+|---|---|---|
+| MySQL | `chinook`、`sakila` | `make samples-mysql` |
+| PostgreSQL | `pagila`、`chinook` | `make samples-postgres` |
+
+
+## 准备 samples
+
+准备过程需要 `curl` 和 `git`;MySQL samples 还需要 `unzip` 与 `sha256sum`。
+
+准备命令会下载并校验固定的 upstream 文件,但不会启动容器,也不会向
+已初始化数据库导入数据。临时下载仅保存在本地,不提交到 Git,最终位于
+`MYSQL_SAMPLES_DIR` 或 `POSTGRES_SAMPLES_DIR`。来源、完整性固定值和许可
+见 [`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md)。
+
+准备时机以及重新初始化现有数据库的方法见
+[初始化生命周期](#section-initialization)。
+
+完全缺失的 sample 会被跳过,不影响创建 `demo`。不完整的 sample 集合或
+意外存在的 sample 数据库会被拒绝,不会自动修复或删除。
+
+
+## 存储目录结构
+
+```text
+data/
+├── mysql/
+└── postgres/
+
+initdb/
+├── mysql/
+└── postgres/
+```
+
+| 数据库 | Data | Init | 可选 samples |
+|---|---|---|---|
+| MySQL | `MYSQL_DATA_DIR` (`./data/mysql`) | `MYSQL_INITDB_DIR` (`./initdb/mysql`) | `MYSQL_SAMPLES_DIR` (`./samples/mysql`) |
+| PostgreSQL | `POSTGRES_DATA_DIR` (`./data/postgres`) | `POSTGRES_INITDB_DIR` (`./initdb/postgres`) | `POSTGRES_SAMPLES_DIR` (`./samples/postgres`) |
+
+data 与 sample 路径可在 `.docker.env` 中修改,但必须通过 managed path
+验证。init 目录的应用规则见[初始化生命周期](#section-initialization)。
+不要手工编辑 `data/` 中的数据库文件,它们可能属于容器使用的数字 UID/GID。
+
+
+## 初始化生命周期
+
+> **重要:** MySQL 与 PostgreSQL 官方 entrypoints 仅对空 data 目录执行
+> init 文件。初始化后添加文件不会改变已有数据库。`make down` 会保留
+> 数据,而确认后的重新初始化会删除所选数据库当前的数据,并根据最新 init
+> 脚本重新创建数据库。只有需要保留自己的数据时才需备份;没有重要改动的
+> 一次性学习环境不要求备份。
+
+如需在首次初始化时包含 samples,请在第一次启动前准备:
+
+```bash
+make samples-mysql
+make up-mysql
+
+make samples-postgres
+make up-postgres
+```
+
+对于已经初始化的数据库,如有需要请先保留自己的数据,然后只执行对应的确认重新初始化:
+
+```bash
+make samples-mysql
+make reinit-mysql CONFIRM=1
+
+make samples-postgres
+make reinit-postgres CONFIRM=1
+```
+
+
+## 学习用户访问与 ownership
+
+MySQL 在 init 时创建 `DB_USER`,并授予它所有非系统数据库的
+访问权。PostgreSQL 创建独立的非 superuser `DB_USER`,不授予
+createdb/createrole,并让它拥有 `demo`、`public` schema 和已加载
+sample 对象。管理员 credentials 保持独立:`MYSQL_ROOT_PASSWORD`、
+`POSTGRES_SUPERUSER` 与 `POSTGRES_SUPERUSER_PASSWORD`。不要手工编辑
+`data/` 中由容器拥有的文件。
+
+[返回 README](../README_zh.md)
diff --git a/docs/langs/zh/getting-started.md b/docs/langs/zh/getting-started.md
new file mode 100644
index 0000000..83db386
--- /dev/null
+++ b/docs/langs/zh/getting-started.md
@@ -0,0 +1,157 @@
+# 入门
+
+[← 返回 README](../README_zh.md)
+
+## 语言
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/getting-started.md) | [English](../en/getting-started.md) | [Español](../es/getting-started.md) | **已选择** | [Français](../fr/getting-started.md) | [Deutsch](../de/getting-started.md) |
+
+## 章节
+
+| 入门 | 数据库与 samples | 检查与运维 | 诊断与故障排除 |
+| --- | --- | --- | --- |
+| **已选择** | [数据库与 samples](databases.md) | [检查与运维](operations.md) | [诊断与故障排除](troubleshooting.md) |
+
+
+## 要求
+
+- Docker Engine 或 Docker Desktop,并使用 Docker Compose v2。
+- GNU Make、Bash 和基本 Unix 命令行工具。
+- 推荐环境:Linux;使用 Docker Desktop 的 macOS;或使用 Docker Desktop
+ 与 WSL2 的 Windows。
+
+请在仓库根目录执行命令。项目默认分支为 `master`。
+
+
+## 快速开始
+
+`make init` 会根据 [`.docker.env.example`](../../../.docker.env.example)
+创建本地 `.docker.env`,验证受控存储路径并创建工作目录。然后启动完整环境:
+
+```bash
+make init
+make up
+```
+
+`make up` 会启动 MySQL、PostgreSQL 和 Adminer。默认配置下,Adminer
+地址为 `http://127.0.0.1:8081`。
+
+使用 `make up-no-ui` 可启动两种数据库而不启动 Adminer。首次初始化时
+始终创建必需的 `demo` 数据库;示例数据集是可选的。如需在首次初始化时
+加载它们,请在第一次 `make up` 前完成准备。对于已经初始化的数据目录,
+仅在需要保留自己的数据时才在确认重新初始化前备份。准确步骤见
+[初始化生命周期](databases.md#section-initialization)。
+
+```bash
+make status
+make logs
+make down
+```
+
+`make down` 删除容器和网络,但保留 bind-mounted 数据。
+
+
+## 启动模式
+
+| 命令 | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | 启动 | 启动 | 启动 |
+| `make up-no-ui` | 启动 | 启动 | 停止 |
+| `make up-mysql` | 启动 | 不启动 | 不启动 |
+| `make up-postgres` | 不启动 | 启动 | 不启动 |
+
+单数据库命令不会停止另一种已运行的数据库;Adminer 可单独管理。
+
+
+📋 完整启动模式表
+
+| 命令 | MySQL | PostgreSQL | Adminer |
+|---|---|---|---|
+| `make up` | 启动 | 启动 | 启动 |
+| `make up-no-ui` | 启动或保持运行 | 启动或保持运行 | 若运行则停止 |
+| `make up-mysql` | 启动 | 不自动启动 | 不自动启动 |
+| `make up-mysql-ui` | 启动 | 不自动启动 | 启动 |
+| `make up-postgres` | 不自动启动 | 启动 | 不自动启动 |
+| `make up-postgres-ui` | 不自动启动 | 启动 | 启动 |
+| `make up-ui` | 不改变 | 不改变 | 启动 |
+| `make down-ui` | 不改变 | 不改变 | 停止 |
+
+单数据库命令不会停止已经运行的另一种数据库。Adminer 可以单独启停,
+并非只服务于 MySQL。
+
+
+
+
+## 连接
+
+### Adminer
+
+在 Compose 网络内,Adminer 提供两个预设服务器:
+
+```text
+MySQL (mysql)
+PostgreSQL (postgres)
+```
+
+选择服务器后,使用 `DB_USER`、`DB_PASSWORD` 和数据库名(如 `demo`)
+登录。`mysql` 与 `postgres` 是 Compose 内部服务名,不是桌面客户端的
+主机名。
+
+### 主机客户端
+
+| 数据库 | 默认主机 | 端口变量 | 用户 | 默认数据库 |
+|---|---|---|---|---|
+| MySQL | `127.0.0.1` | `MYSQL_PORT` | `DB_USER` | `demo` |
+| PostgreSQL | `127.0.0.1` | `POSTGRES_PORT` | `DB_USER` | `demo` |
+
+DataGrip、DBeaver、PhpStorm 和主机 CLI 使用发布的地址与端口。如果修改
+`BIND_ADDRESS`,请按需改用对应接口可达的地址。
+
+### 容器内 CLI
+
+Make targets 通过容器环境传递密码,不会把密码写入 shell history:
+
+```bash
+make mysql # MySQL 管理员,demo 数据库
+make mysql-user # DB_USER,demo 数据库
+make postgres # PostgreSQL 超级用户,demo 数据库
+make postgres-user # DB_USER,demo 数据库
+```
+
+
+## 凭据
+
+`.docker.env` 从 [`.docker.env.example`](../../../.docker.env.example) 创建,并被 Git 忽略。密码应保存在
+该文件中,不要硬编码进受版本控制的 Compose、SQL 或客户端配置。
+
+| 用途 | 用户设置 | 密码设置 |
+|---|---|---|
+| 两种数据库共用的学习用户 | `DB_USER` | `DB_PASSWORD` |
+| MySQL 管理员 | `root` | `MYSQL_ROOT_PASSWORD` |
+| PostgreSQL 管理员/超级用户 | `POSTGRES_SUPERUSER` | `POSTGRES_SUPERUSER_PASSWORD` |
+
+`POSTGRES_SUPERUSER` 与 `DB_USER` 必须是不同角色。日常练习应使用学习
+用户。对外共享或发布服务前,请替换示例密码。
+
+
+## 端口与 `BIND_ADDRESS`
+
+| 服务 | 端口变量 | 示例默认值 |
+|---|---|---|
+| MySQL | `MYSQL_PORT` | `3306` |
+| PostgreSQL | `POSTGRES_PORT` | `5432` |
+| Adminer | `ADMINER_PORT` | `8081` |
+
+默认情况下,三个服务只绑定 loopback:
+
+```env
+BIND_ADDRESS=127.0.0.1
+```
+
+`127.0.0.1` 是本地默认值。`BIND_ADDRESS=0.0.0.0` 会在所有网络接口上
+发布端口。LAN 或 VPN 访问应优先使用具体接口的地址。修改时必须明确
+评估 firewall、密码强度和网络可信度。
+
+[返回 README](../README_zh.md)
diff --git a/docs/langs/zh/operations.md b/docs/langs/zh/operations.md
new file mode 100644
index 0000000..b4438df
--- /dev/null
+++ b/docs/langs/zh/operations.md
@@ -0,0 +1,158 @@
+# 检查与运维
+
+[← 返回 README](../README_zh.md)
+
+## 语言
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/operations.md) | [English](../en/operations.md) | [Español](../es/operations.md) | **已选择** | [Français](../fr/operations.md) | [Deutsch](../de/operations.md) |
+
+## 章节
+
+| 入门 | 数据库与 samples | 检查与运维 | 诊断与故障排除 |
+| --- | --- | --- | --- |
+| [入门](getting-started.md) | [数据库与 samples](databases.md) | **已选择** | [诊断与故障排除](troubleshooting.md) |
+
+
+## 公共 Make targets
+
+公共 targets 及其实现位于 [`Makefile`](../../../Makefile)。
+
+关键 targets 包括 `make init`、`make up`、`make down`、
+`make check`、`make test-storage-paths`、`make test-sql-imports`、
+`make mysql-import` 和 `make postgres-import`。Trusted SQL 不是
+sandbox,可能 partial execution,且不保证 automatic rollback;重要导入
+前先做 backup。内置 backup targets 仅覆盖 MySQL `demo`,不覆盖
+PostgreSQL。`clean-*` 与 `reinit-*` 是 destructive 操作,并要求精确
+`CONFIRM=1`。
+
+
+📋 完整公共 Make targets 参考
+
+`make help` 显示简要命令列表。
+
+| 命令 | 用途 |
+|---|---|
+| `make init` | 创建 `.docker.env`、验证 managed paths、创建目录 |
+| `make check-env` | 检查 env、角色分离、数据库名与 managed paths |
+| `make pull` | 拉取三个固定版本的镜像 |
+| `make config` | 验证展开后的 Compose 配置 |
+| `make up`、`make up-no-ui` | 启动两个数据库,启用或不启用 Adminer |
+| `make up-mysql`、`make up-mysql-ui` | 启动 MySQL,可选 Adminer |
+| `make up-postgres`、`make up-postgres-ui` | 启动 PostgreSQL,可选 Adminer |
+| `make up-ui`、`make down-ui` | 仅启动或停止 Adminer |
+| `make down` | 停止环境但不删除 bind-mounted 数据 |
+| `make status`、`make logs` | 查看状态或跟踪所有日志 |
+| `make log SERVICE=postgres` | 跟踪单个服务;也支持 `make log postgres` |
+| `make in SERVICE=postgres` | 打开服务 shell;也支持 `make in postgres` |
+| `make mysql`、`make mysql-user` | 以管理员或 `DB_USER` 打开 MySQL |
+| `make postgres`、`make postgres-user` | 以超级用户或 `DB_USER` 打开 PostgreSQL |
+| `make samples-mysql`、`make samples-postgres` | 准备已校验的 samples |
+| `make check-mysql-access`、`make check-postgres-access` | 检查一个运行中的数据库 |
+| `make check` | 验证 Compose 及 `DB_USER` 对两种数据库的访问 |
+| `make test-storage-paths` | 无需 Docker runtime 测试 managed path 保护 |
+| `make test-sql-imports` | smoke-test 两个公共 SQL import targets |
+| `make mysql-import FILE=... DATABASE=...` | 以 `DB_USER` 向 MySQL 导入 plain SQL |
+| `make postgres-import FILE=... DATABASE=...` | 以 `DB_USER` 向 PostgreSQL 导入 plain SQL |
+| `make dump`、`make restore` | 备份或恢复 MySQL `demo` |
+| `make clean-{mysql,postgres,all} CONFIRM=1` | 删除选定的 data 目录 |
+| `make reinit-{mysql,postgres,all} CONFIRM=1` | 删除、重建并检查数据库 |
+
+
+
+
+## 检查
+
+### 静态与本地检查
+
+```bash
+make check-env
+make config
+make test-storage-paths
+```
+
+`make check-env` 在缺失时创建 `.docker.env`,然后验证配置与 managed
+paths。`make config` 验证展开后的 Compose 模型。`make test-storage-paths`
+不需要 Docker runtime;它测试项目外路径、symlink 组件、路径重叠/嵌套
+以及 reserved directories。
+
+### Runtime 检查
+
+```bash
+make up-no-ui
+make check
+make test-sql-imports
+make down
+```
+
+`make check` 验证必需的 `demo` 数据和 `DB_USER` 的实际访问,也检查已安装
+的可选 samples。`make test-sql-imports` 要求两种数据库都已启动;它调用
+公共 `mysql-import` 与 `postgres-import`,在 `demo` 中创建唯一命名的临时
+表,以 `DB_USER` 验证 marker rows,并且只删除这些表。这是 trusted import
+工作流的 smoke-test,不是 sandbox,也不能证明不可信 SQL 的安全性。
+
+
+## Storage path 安全
+
+`make check-env` 会运行
+[`scripts/validate-storage-paths.sh`](../../../scripts/validate-storage-paths.sh)。Data
+paths 必须严格位于 `data/` 内,sample paths 必须严格位于
+`samples/` 内;symlink、相同、嵌套、重叠和 reserved paths 都会被拒绝。
+`make test-storage-paths` 无需 Docker runtime 即可测试这些规则。
+
+
+## Trusted SQL 导入
+
+```bash
+make mysql-import FILE=path/to/file.sql DATABASE=demo
+make postgres-import FILE=path/to/file.sql DATABASE=demo
+```
+
+两个 targets 都遵循以下规则:
+
+- `FILE` 与 `DATABASE` 必填。
+- 文件必须是存在、可读、非空的本地 plain SQL。
+- 数据库必须已存在;禁止系统数据库。
+- import 以 `DB_USER` 执行,不创建数据库,也不授予 grants。
+- `DATABASE` 只选择初始连接,不创建 sandbox。
+- qualified names、client/session commands 及真实 grants 可能影响其他
+ 可访问对象。
+- 可能发生 partial execution;不承诺自动 rollback。
+- 重要导入前必须备份。
+- 不处理 gzip、archives 或 PostgreSQL custom-format backups。
+
+
+## 备份
+
+内置 targets 只覆盖配置的 MySQL `demo` 数据库:
+
+```bash
+make dump
+make restore
+```
+
+`make dump` 写入 `backup/demo.sql`;`make restore` 读取该文件并重新应用
+MySQL 学习 grants。PostgreSQL 数据应使用独立备份流程。
+
+
+## 清理与重新初始化
+
+> **警告:** 所有 `clean-*` 和 `reinit-*` 命令都是破坏性的,并且要求
+> 精确确认 `CONFIRM=1`。
+
+```bash
+make clean-mysql CONFIRM=1
+make clean-postgres CONFIRM=1
+make clean-all CONFIRM=1
+
+make reinit-mysql CONFIRM=1
+make reinit-postgres CONFIRM=1
+make reinit-all CONFIRM=1
+```
+
+单数据库命令只删除对应 data 目录;`all` 会删除两种数据库的数据。配置、
+init、samples 和 backups 会保留。reinit 随后启动并检查所选数据库;
+`reinit-all` 会启动 MySQL、PostgreSQL 和 Adminer,然后执行共享访问检查。
+
+[返回 README](../README_zh.md)
diff --git a/docs/langs/zh/troubleshooting.md b/docs/langs/zh/troubleshooting.md
new file mode 100644
index 0000000..c26f901
--- /dev/null
+++ b/docs/langs/zh/troubleshooting.md
@@ -0,0 +1,59 @@
+# 诊断与故障排除
+
+[← 返回 README](../README_zh.md)
+
+## 语言
+
+| Русский | English | Español | 中文 | Français | Deutsch |
+| --- | --- | --- | --- | --- | --- |
+| [Русский](../ru/troubleshooting.md) | [English](../en/troubleshooting.md) | [Español](../es/troubleshooting.md) | **已选择** | [Français](../fr/troubleshooting.md) | [Deutsch](../de/troubleshooting.md) |
+
+## 章节
+
+| 入门 | 数据库与 samples | 检查与运维 | 诊断与故障排除 |
+| --- | --- | --- | --- |
+| [入门](getting-started.md) | [数据库与 samples](databases.md) | [检查与运维](operations.md) | **已选择** |
+
+先收集诊断信息,再针对原因修正。若仍需重新初始化,仅在需要保留自己的
+数据时才备份,并仅将确认后的 `reinit-... CONFIRM=1` 作为有意的最后手段。
+
+生命周期与运维的规范说明:[数据库](databases.md#section-initialization) · [运维](operations.md#section-clean-reinitialize)。
+
+
+## 配置或 storage path 验证失败
+
+运行 `make check-env`。data paths 必须严格位于 `data/` 内,samples 必须
+位于 `samples/` 内;不得包含 symlink、互相重叠或使用 reserved
+directories。修正 `.docker.env` 后运行 `make config`。
+
+
+## 服务未就绪
+
+```bash
+make status
+make log SERVICE=mysql
+make log SERVICE=postgres
+```
+
+在修改数据前,检查 Docker、端口、`.docker.env` 与日志。
+
+
+## init 修改或 samples 未出现
+
+如果 data 已初始化,这是预期行为。检查路径与准备命令;若需保留重要数据,
+请备份,并仅在明确需要时最后使用 `reinit-... CONFIRM=1`。
+
+
+## sample 不完整或所有者异常
+
+loader 不会覆盖或修复意外数据库。重新运行 `make samples-mysql` 或
+`make samples-postgres` 并检查错误;重新初始化前先保留所需数据。
+
+
+## 客户端无法连接
+
+主机客户端使用发布地址及 `MYSQL_PORT` 或 `POSTGRES_PORT`;Adminer 在
+Compose 网络中使用 `mysql` 或 `postgres`。检查 `BIND_ADDRESS`、
+firewall、数据库选择和非管理员 `DB_USER` 凭据。
+
+[返回 README](../README_zh.md)
diff --git a/initdb/mysql/030_training_database.sql.example b/initdb/mysql/030_training_database.sql.example
index 29b8f2f..398c496 100644
--- a/initdb/mysql/030_training_database.sql.example
+++ b/initdb/mysql/030_training_database.sql.example
@@ -22,51 +22,50 @@
-- При чистой инициализации порядок будет таким:
--
-- 001_demo.sql
--- 050_load_optional_samples.sh
-- 030_shop.sql
+-- 050_load_optional_samples.sh
-- 090_grant_training_access.sh
-- 099_check_training_access.sh
--
--- Grant-скрипт обнаружит новую базу shop автоматически.
+-- Grant-скрипт обнаружит новую пользовательскую базу shop и выдаст
+-- учебному пользователю доступ к ней во время чистой инициализации.
--
-- ============================================================================
-- Вариант 2. Добавить базу в уже работающий MySQL без удаления данных
-- ============================================================================
--
--- Подготовьте файл:
+-- Административно создайте новую базу отдельно, затем примените grants:
--
--- cp initdb/mysql/030_training_database.sql.example initdb/mysql/030_shop.sql
+-- make mysql-grants
--
--- Отредактируйте его и выполните:
+-- Подготовьте доверенный plain SQL-файл со схемой и данными для уже созданной
+-- базы shop. В нём не должно быть CREATE DATABASE и USE.
--
--- make mysql-import FILE=initdb/mysql/030_shop.sql
+-- Выполните импорт:
--
--- Команда:
+-- make mysql-import FILE=/path/to/shop.sql DATABASE=shop
--
--- 1. импортирует SQL от имени root;
--- 2. повторно выдаст DB_USER права на все учебные базы;
--- 3. проверит доступ учебного пользователя.
+-- mysql-import требует FILE и DATABASE, выполняет импорт от имени DB_USER,
+-- не создаёт базу и не выдаёт grants, а после импорта запускает проверку
+-- доступа учебного пользователя.
--
-- ============================================================================
-- Вариант 3. Добавить сторонний SQL dump
-- ============================================================================
--
--- Если dump уже содержит:
+-- Сторонний dump нужно привести к форме schema/data-only без database-level
+-- directives (CREATE DATABASE и USE), а административное создание или
+-- восстановление базы выполнить отдельно от mysql-import.
--
--- CREATE DATABASE ...
--- USE ...
+-- mysql-import предназначен для доверенного plain SQL внутри уже
+-- существующей базы; он не является sandbox для недоверенного SQL.
--
--- его можно импортировать напрямую:
+-- Например, если dump содержит:
--
--- make mysql-import FILE=/path/to/database.sql
---
--- Если dump содержит только CREATE TABLE / INSERT, добавьте в начало:
---
--- CREATE DATABASE IF NOT EXISTS `shop`
--- CHARACTER SET utf8mb4
--- COLLATE utf8mb4_0900_ai_ci;
+-- CREATE DATABASE ...
+-- USE ...
--
--- USE `shop`;
+-- удалите эти директивы перед импортом.
--
-- ============================================================================
-- Вариант 4. Разделить схему и данные на несколько файлов
@@ -78,7 +77,8 @@
-- initdb/mysql/031_shop_data.sql
--
-- Главное требование: оба файла должны выполняться раньше
--- 090_grant_training_access.sh.
+-- 050_load_optional_samples.sh, 090_grant_training_access.sh и
+-- 099_check_training_access.sh.
--
-- ============================================================================
-- Пример содержимого учебной базы
diff --git a/scripts/test-sql-imports.sh b/scripts/test-sql-imports.sh
new file mode 100755
index 0000000..e016eb4
--- /dev/null
+++ b/scripts/test-sql-imports.sh
@@ -0,0 +1,151 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+project_dir="$(cd -- "${script_dir}/.." && pwd)"
+cd -- "${project_dir}"
+
+fail() {
+ printf 'ERROR: %s\n' "$*" >&2
+ exit 1
+}
+
+[[ -f .docker.env ]] || fail '.docker.env is required; run make init first'
+
+set -a
+source .docker.env
+set +a
+
+for variable_name in COMPOSE_PROJECT_NAME DB_USER DB_PASSWORD; do
+ [[ -n "${!variable_name:-}" ]] || fail "${variable_name} is required in .docker.env"
+done
+
+compose=(docker compose --env-file .docker.env -p "${COMPOSE_PROJECT_NAME}")
+temp_dir="$(mktemp -d)"
+mysql_table=''
+postgres_table=''
+mysql_cleanup_needed=false
+postgres_cleanup_needed=false
+
+assert_safe_identifier() {
+ local identifier="$1"
+
+ [[ "${identifier}" =~ ^[a-z][a-z0-9_]{0,62}$ ]] || \
+ fail "generated unsafe SQL identifier: ${identifier}"
+}
+
+mysql_query() {
+ local query="$1"
+
+ "${compose[@]}" exec -T \
+ -e IMPORT_DATABASE=demo \
+ -e IMPORT_QUERY="${query}" \
+ mysql sh -c 'MYSQL_PWD="$DB_PASSWORD" exec mysql --host=127.0.0.1 --user="$DB_USER" --batch --skip-column-names "$IMPORT_DATABASE" --execute "$IMPORT_QUERY"'
+}
+
+postgres_query() {
+ local query="$1"
+
+ "${compose[@]}" exec -T \
+ -e IMPORT_DATABASE=demo \
+ -e IMPORT_QUERY="${query}" \
+ postgres sh -c 'PGPASSWORD="$DB_PASSWORD" exec psql --host=127.0.0.1 --username="$DB_USER" --dbname="$IMPORT_DATABASE" --no-psqlrc --set=ON_ERROR_STOP=1 --tuples-only --no-align --command="$IMPORT_QUERY"'
+}
+
+drop_smoke_tables() {
+ local cleanup_failed=0
+
+ if [[ "${mysql_cleanup_needed}" == true ]]; then
+ if mysql_query "DROP TABLE IF EXISTS \`${mysql_table}\`;" >/dev/null; then
+ mysql_cleanup_needed=false
+ else
+ printf 'ERROR: failed to remove MySQL smoke table\n' >&2
+ cleanup_failed=1
+ fi
+ fi
+ if [[ "${postgres_cleanup_needed}" == true ]]; then
+ if postgres_query "DROP TABLE IF EXISTS public.${postgres_table};" >/dev/null; then
+ postgres_cleanup_needed=false
+ else
+ printf 'ERROR: failed to remove PostgreSQL smoke table\n' >&2
+ cleanup_failed=1
+ fi
+ fi
+
+ return "${cleanup_failed}"
+}
+
+cleanup() {
+ local exit_status=$?
+ local cleanup_failed=0
+ local final_status
+
+ trap - EXIT
+ set +e
+
+ if ! drop_smoke_tables; then
+ cleanup_failed=1
+ fi
+ if ! rm -rf -- "${temp_dir}"; then
+ printf 'ERROR: failed to remove smoke-test temporary directory\n' >&2
+ cleanup_failed=1
+ fi
+ if ((cleanup_failed)); then
+ printf 'ERROR: trusted SQL import smoke-test cleanup failed\n' >&2
+ fi
+
+ final_status="${exit_status}"
+ if ((exit_status == 0 && cleanup_failed)); then
+ final_status=1
+ fi
+
+ exit "${final_status}"
+}
+trap cleanup EXIT
+
+make --no-print-directory wait-mysql
+make --no-print-directory wait-postgres
+
+suffix="$(date +%s)_$$_${RANDOM}"
+mysql_table="sql_import_smoke_mysql_${suffix}"
+postgres_table="sql_import_smoke_postgres_${suffix}"
+mysql_marker="mysql_import_marker_${suffix}"
+postgres_marker="postgres_import_marker_${suffix}"
+
+assert_safe_identifier "${mysql_table}"
+assert_safe_identifier "${postgres_table}"
+
+[[ "$(mysql_query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '${mysql_table}';")" == 0 ]] || \
+ fail "MySQL smoke table already exists: ${mysql_table}"
+[[ "$(postgres_query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '${postgres_table}';")" == 0 ]] || \
+ fail "PostgreSQL smoke table already exists: ${postgres_table}"
+
+mysql_sql="${temp_dir}/mysql-import.sql"
+postgres_sql="${temp_dir}/postgres-import.sql"
+
+printf 'CREATE TABLE `%s` (marker VARCHAR(255) NOT NULL);\nINSERT INTO `%s` (marker) VALUES (\047%s\047);\n' \
+ "${mysql_table}" "${mysql_table}" "${mysql_marker}" > "${mysql_sql}"
+printf 'CREATE TABLE public.%s (marker TEXT NOT NULL);\nINSERT INTO public.%s (marker) VALUES (\047%s\047);\n' \
+ "${postgres_table}" "${postgres_table}" "${postgres_marker}" > "${postgres_sql}"
+
+mysql_cleanup_needed=true
+make mysql-import FILE="${mysql_sql}" DATABASE=demo
+[[ "$(mysql_query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '${mysql_table}';")" == 1 ]] || \
+ fail "MySQL smoke table was not created: ${mysql_table}"
+[[ "$(mysql_query "SELECT marker FROM \`${mysql_table}\`;")" == "${mysql_marker}" ]] || \
+ fail 'MySQL smoke marker did not match'
+
+postgres_cleanup_needed=true
+make postgres-import FILE="${postgres_sql}" DATABASE=demo
+[[ "$(postgres_query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '${postgres_table}';")" == 1 ]] || \
+ fail "PostgreSQL smoke table was not created: ${postgres_table}"
+[[ "$(postgres_query "SELECT marker FROM public.${postgres_table};")" == "${postgres_marker}" ]] || \
+ fail 'PostgreSQL smoke marker did not match'
+
+drop_smoke_tables
+[[ "$(mysql_query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '${mysql_table}';")" == 0 ]] || \
+ fail "MySQL smoke table still exists: ${mysql_table}"
+[[ "$(postgres_query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '${postgres_table}';")" == 0 ]] || \
+ fail "PostgreSQL smoke table still exists: ${postgres_table}"
+
+printf 'PASS: trusted SQL import smoke tests completed\n'
diff --git a/scripts/test-storage-paths.sh b/scripts/test-storage-paths.sh
new file mode 100755
index 0000000..cea5300
--- /dev/null
+++ b/scripts/test-storage-paths.sh
@@ -0,0 +1,149 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+validator="$script_dir/validate-storage-paths.sh"
+temp_dir="$(mktemp -d)"
+project_dir="$temp_dir/project"
+outside_dir="$temp_dir/outside"
+failures=0
+
+cleanup() {
+ rm -rf -- "$temp_dir"
+}
+trap cleanup EXIT
+
+mkdir -p \
+ "$project_dir/data/mysql" \
+ "$project_dir/data/postgres" \
+ "$project_dir/samples/mysql" \
+ "$project_dir/samples/postgres" \
+ "$outside_dir"
+
+run_validator() {
+ local mysql_data="${1-$project_dir/data/mysql}"
+ local postgres_data="${2-$project_dir/data/postgres}"
+ local mysql_samples="${3-$project_dir/samples/mysql}"
+ local postgres_samples="${4-$project_dir/samples/postgres}"
+
+ "$validator" \
+ --project-dir "$project_dir" \
+ --mysql-data "$mysql_data" \
+ --postgres-data "$postgres_data" \
+ --mysql-samples "$mysql_samples" \
+ --postgres-samples "$postgres_samples"
+}
+
+expect_accept() {
+ local case_name="$1"
+ shift
+
+ if ! run_validator "$@" >/dev/null 2>&1; then
+ printf 'FAIL: positive case %q was rejected\n' "$case_name" >&2
+ failures=$((failures + 1))
+ fi
+}
+
+expect_reject() {
+ local case_name="$1"
+ shift
+
+ if run_validator "$@" >/dev/null 2>&1; then
+ printf 'FAIL: negative case %q was accepted\n' "$case_name" >&2
+ failures=$((failures + 1))
+ fi
+}
+
+expect_accept 'default managed paths'
+expect_accept 'relative data and sample paths' \
+ 'data/mysql' \
+ 'data/postgres' \
+ 'samples/mysql' \
+ 'samples/postgres'
+expect_accept 'other names strictly inside their roots' \
+ "$project_dir/data/mysql-v2" \
+ "$project_dir/data/postgres-v2" \
+ "$project_dir/samples/mysql-extra" \
+ "$project_dir/samples/postgres-extra"
+
+fresh_clone_project_dir="$temp_dir/fresh-clone"
+mkdir -p "$fresh_clone_project_dir"
+if ! "$validator" \
+ --project-dir "$fresh_clone_project_dir" \
+ --mysql-data 'data/mysql' \
+ --postgres-data 'data/postgres' \
+ --mysql-samples 'samples/mysql' \
+ --postgres-samples 'samples/postgres' \
+ >/dev/null 2>&1; then
+ printf 'FAIL: positive case %q was rejected\n' 'fresh clone without managed directories' >&2
+ failures=$((failures + 1))
+fi
+if [[ -e "$fresh_clone_project_dir/data" || -e "$fresh_clone_project_dir/samples" ]]; then
+ printf 'FAIL: validator created managed directories in %q\n' "$fresh_clone_project_dir" >&2
+ failures=$((failures + 1))
+fi
+
+docs_project_dir="$temp_dir/docs/projects/sql-lab"
+mkdir -p \
+ "$docs_project_dir/data/mysql" \
+ "$docs_project_dir/data/postgres" \
+ "$docs_project_dir/samples/mysql" \
+ "$docs_project_dir/samples/postgres"
+if ! "$validator" \
+ --project-dir "$docs_project_dir" \
+ --mysql-data "$docs_project_dir/data/mysql" \
+ --postgres-data "$docs_project_dir/data/postgres" \
+ --mysql-samples "$docs_project_dir/samples/mysql" \
+ --postgres-samples "$docs_project_dir/samples/postgres" \
+ >/dev/null 2>&1; then
+ printf 'FAIL: positive case %q was rejected\n' 'project under docs/projects/sql-lab' >&2
+ failures=$((failures + 1))
+fi
+
+expect_reject '.git' "$project_dir/.git"
+expect_reject '.github' "$project_dir/data/mysql" "$project_dir/data/postgres" "$project_dir/.github"
+expect_reject 'project root' "$project_dir"
+expect_reject 'data root' "$project_dir/data"
+expect_reject 'samples root' "$project_dir/data/mysql" "$project_dir/data/postgres" "$project_dir/samples"
+expect_reject 'outside directory' "$outside_dir"
+expect_reject 'relative outside directory' '../outside'
+expect_reject 'empty path' ''
+expect_reject 'filesystem root' '/'
+
+for reserved_component in .git .github initdb conf adminer backup .tmp docs scripts; do
+ expect_reject "reserved component $reserved_component" \
+ "$project_dir/data/$reserved_component/mysql"
+done
+
+ln -s "$project_dir/data/mysql" "$project_dir/data/mysql-link"
+expect_reject 'symlink managed path' "$project_dir/data/mysql-link"
+
+ln -s "$outside_dir" "$project_dir/data/escape"
+expect_reject 'symlink component outside project' "$project_dir/data/escape/nested"
+
+expect_reject 'same data paths' "$project_dir/data/mysql" "$project_dir/data/mysql"
+expect_reject 'nested data paths' "$project_dir/data/mysql" "$project_dir/data/mysql/nested"
+expect_reject 'same sample paths' \
+ "$project_dir/data/mysql" \
+ "$project_dir/data/postgres" \
+ "$project_dir/samples/mysql" \
+ "$project_dir/samples/mysql"
+expect_reject 'nested sample paths' \
+ "$project_dir/data/mysql" \
+ "$project_dir/data/postgres" \
+ "$project_dir/samples/mysql" \
+ "$project_dir/samples/mysql/nested"
+expect_reject 'sample inside data' \
+ "$project_dir/data/mysql" \
+ "$project_dir/data/postgres" \
+ "$project_dir/data/mysql/sample"
+expect_reject 'data inside sample' \
+ "$project_dir/samples/mysql/data" \
+ "$project_dir/data/postgres"
+
+if ((failures > 0)); then
+ printf 'FAIL: %d storage-path test case(s) failed\n' "$failures" >&2
+ exit 1
+fi
+
+printf 'PASS: storage-path validator tests completed\n'
diff --git a/scripts/validate-storage-paths.sh b/scripts/validate-storage-paths.sh
new file mode 100755
index 0000000..260706a
--- /dev/null
+++ b/scripts/validate-storage-paths.sh
@@ -0,0 +1,173 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+usage() {
+ cat >&2 <<'USAGE'
+Usage: validate-storage-paths.sh \
+ --project-dir PATH \
+ --mysql-data PATH \
+ --postgres-data PATH \
+ --mysql-samples PATH \
+ --postgres-samples PATH
+USAGE
+}
+
+reject() {
+ local variable_name="$1"
+ local rejected_path="$2"
+ local reason="$3"
+
+ printf 'ERROR: %s: rejected path %q (%s)\n' \
+ "$variable_name" "$rejected_path" "$reason" >&2
+ exit 1
+}
+
+require_value() {
+ local variable_name="$1"
+ local value="$2"
+
+ [[ -n "$value" ]] || reject "$variable_name" "$value" 'path must not be empty'
+}
+
+is_strict_descendant() {
+ local parent="$1"
+ local child="$2"
+
+ [[ "$child" == "$parent/"* ]]
+}
+
+contains_reserved_component() {
+ local managed_path="$1"
+ local component
+ local -a components
+ local -a reserved_components=(.git .github initdb conf adminer backup .tmp docs scripts)
+
+ IFS='/' read -r -a components <<< "$managed_path"
+ for component in "${components[@]}"; do
+ case "$component" in
+ ''|.|..|data|samples) ;;
+ *)
+ local reserved
+ for reserved in "${reserved_components[@]}"; do
+ [[ "$component" == "$reserved" ]] && return 0
+ done
+ ;;
+ esac
+ done
+
+ return 1
+}
+
+has_symlink_component() {
+ local path="$1"
+ local component
+ local current=/
+ local -a components
+
+ IFS='/' read -r -a components <<< "$path"
+ for component in "${components[@]}"; do
+ case "$component" in
+ ''|.) continue ;;
+ ..) current="$current/.." ;;
+ *) current="$current/$component" ;;
+ esac
+
+ [[ -L "$current" ]] && return 0
+ done
+
+ return 1
+}
+
+project_dir=''
+mysql_data=''
+postgres_data=''
+mysql_samples=''
+postgres_samples=''
+
+while (($#)); do
+ case "$1" in
+ --project-dir|--mysql-data|--postgres-data|--mysql-samples|--postgres-samples)
+ (($# >= 2)) || { usage; exit 2; }
+ case "$1" in
+ --project-dir) project_dir="$2" ;;
+ --mysql-data) mysql_data="$2" ;;
+ --postgres-data) postgres_data="$2" ;;
+ --mysql-samples) mysql_samples="$2" ;;
+ --postgres-samples) postgres_samples="$2" ;;
+ esac
+ shift 2
+ ;;
+ --help|-h)
+ usage
+ exit 0
+ ;;
+ *)
+ printf 'ERROR: unknown argument: %s\n' "$1" >&2
+ usage
+ exit 2
+ ;;
+ esac
+done
+
+require_value PROJECT_DIR "$project_dir"
+require_value MYSQL_DATA_DIR "$mysql_data"
+require_value POSTGRES_DATA_DIR "$postgres_data"
+require_value MYSQL_SAMPLES_DIR "$mysql_samples"
+require_value POSTGRES_SAMPLES_DIR "$postgres_samples"
+
+project_dir_abs="$(realpath -m -- "$project_dir")"
+[[ -d "$project_dir_abs" ]] || reject PROJECT_DIR "$project_dir" 'project directory does not exist'
+
+data_root="$project_dir_abs/data"
+samples_root="$project_dir_abs/samples"
+
+variable_names=(MYSQL_DATA_DIR POSTGRES_DATA_DIR MYSQL_SAMPLES_DIR POSTGRES_SAMPLES_DIR)
+raw_paths=("$mysql_data" "$postgres_data" "$mysql_samples" "$postgres_samples")
+allowed_roots=("$data_root" "$data_root" "$samples_root" "$samples_root")
+resolved_paths=()
+
+for index in "${!variable_names[@]}"; do
+ variable_name="${variable_names[$index]}"
+ raw_path="${raw_paths[$index]}"
+ allowed_root="${allowed_roots[$index]}"
+
+ if [[ "$raw_path" == /* ]]; then
+ path_to_inspect="$raw_path"
+ else
+ path_to_inspect="$project_dir_abs/$raw_path"
+ fi
+
+ if has_symlink_component "$path_to_inspect"; then
+ reject "$variable_name" "$raw_path" 'managed path contains a symbolic-link component'
+ fi
+
+ resolved_path="$(realpath -m -- "$path_to_inspect")"
+ if ! is_strict_descendant "$allowed_root" "$resolved_path"; then
+ reject "$variable_name" "$raw_path" "must be strictly inside $allowed_root"
+ fi
+
+ managed_path="${resolved_path#"$project_dir_abs"/}"
+ if contains_reserved_component "$managed_path"; then
+ reject "$variable_name" "$raw_path" 'managed path contains a reserved component'
+ fi
+
+ resolved_paths[$index]="$resolved_path"
+done
+
+for ((left = 0; left < ${#resolved_paths[@]}; left++)); do
+ for ((right = left + 1; right < ${#resolved_paths[@]}; right++)); do
+ left_path="${resolved_paths[$left]}"
+ right_path="${resolved_paths[$right]}"
+
+ if [[ "$left_path" == "$right_path" ]]; then
+ reject "${variable_names[$left]}" "${raw_paths[$left]}" \
+ "must not match ${variable_names[$right]}"
+ fi
+
+ if is_strict_descendant "$left_path" "$right_path" || \
+ is_strict_descendant "$right_path" "$left_path"; then
+ reject "${variable_names[$left]}" "${raw_paths[$left]}" \
+ "must not be nested with ${variable_names[$right]}"
+ fi
+ done
+done