Как ведет себя Podman в файловой системе - Академия Selectel

Как ведет себя Podman в файловой системе

Тирекс
Тирекс Самый зубастый автор
4 марта 2026

Какие нюансы скрывает Podman, почему работа с ним отличается от работы с Docker и как избежать частых ошибок.

Изображение записи

Механизм работы с файловой системой в Podman очень схож с Docker. Но есть несколько нюансов. Так как Podman разрабатывался с упором на безопасность и rootless, порой возникают неочевидные ошибки при монтировании volumes. Сегодня рассмотрим эти нюансы и поглубже заглянем в кроличью нору.

База

Начнем с малого. Если запустить podman run -it alpine sh, то внутри контейнера будет своя файловая система, с которой можно работать и создавать файлы. Но если контейнер удалить, файловая система сотрется и все, что вы создавали, будет безвозвратно утрачено. Это база и очевидно, что вы о ней уже знаете.

Чтобы ваши наработки не пропали, монтируем папку с хоста к контейнеру:


      podman run -it -v $(pwd)/data:/app/data alpine sh

Пока папка data существует на хостовой машине, она будет доступна внутри контейнера. Если зайти в контейнер и создать файл touch /app/data/file.txt, то этот файл вы увидите и на хостовой машине. Такой вид монтирования принято называть Bind Mount.

Очевидно? Очевидно! Все так же, как в Docker. Существует еще и Named Volume.

Чтобы создать Named Volume, выполняем команду:


      podman volume create mydata

Теперь данные будут храниться не в контейнере или домашней папке пользователя, а в специальном storage podman.

Посмотреть созданные Named Volume можно командами:


      podman volume ls
podman volume inspect mydata

К контейнеру такие volumes подключается так:


      podman run -it -v mydata:/app/data alpine sh

Где хранятся Named Volume

По умолчанию в rootless-режиме Podman хранит пользовательские данные в ~/.local/share/containers/storage, а если root, то в /var/lib/containers/.

Вот так выглядит rootless:

Много кода.

А вот так чистый root:

Тоже много кода, но немного отличается.

Важно — в эти папки руками лезть не стоит. Podman управляется самостоятельно хранилищами и любое кривое вмешательство может все поломать. Это не просто папки, это storage-драйвер, обычно overlay. То есть присутствуют слои read-only, write layer, metadata, UID/GID mapping, lock-файлы. В общем, если используете Named Volume, управляйте им через Podman. Если хочется экспериментов, делайте Bind Mount.

Когда мы запускали команду podman volume inspect mydata вывод был таким:


      {
   "Name": "mydata",
   "Driver": "local",
   "Mountpoint": "/home/user/.local/share/containers/storage/volumes/mydata/_data",
   "CreatedAt": "...",
   "Labels": {},
   "Scope": "local",
   "Options": {},
   "MountCount": 0,
   "NeedsCopyUp": true,
   "NeedsChown": true,
   "LockNumber": 6
 }

Давайте разберем важные моменты:

  • MountCount: 0 — счетчик контейнеров, использующих данный volume
    Cейчас ни один из контейнеров не использует этот volume. Когда вы его подключите, 0 сменится на 1. При дебаге очень помогает, например чтобы бездумно не удалить ничего важного.
  • NeedsCopyUp: true — если в образе в точке монтирования есть файлы, Podman скопирует их в volume при первом запуске.

Например, в Dockerfile указано:


      RUN mkdir /data
RUN echo "default config" > /data/config.txt

И вы запускаете podman run -v mydata:/data image. Podman создаст volume, скопирует туда содержимое /data из образа, а потом подключит volume.

  • NeedsChown: true — параметр связан с rootless-режимом, root внутри контейнера — это не то же самое, что root на хосте. Podman при первом подключении volume, автоматически поменяет владельца файлов, чтобы контейнер мог писать. Если сделать false, то при записи данных получите ошибку permission denied.
  • Scope: local — volume живет на этом сервере, то есть расположен локально на хосте, он не сетевой и не кластерный.

Проведем эксперимент:


      podman run --rm -it -v mydata:/data alpine sh
echo hello > /data/test.txt
exit
ls ~/.local/share/containers/storage/volumes/mydata/_data

Запускаем контейнер, монтируем volume mydata, внутри контейнера создаем в нем файл, выходим, проверяем на хостовой машине, есть ли наш файл. Все есть. Во время эксперимента параметр станет таким: MountCount: 1.

SELinux

Окей, теперь рассмотрим нюансы, которые начинают вылазить при использовании SELinux. Обычно он включен из коробки на дистрибутивах Fedora, Alma, Rocky и RHEL.

Проверяем на хосте, что SELinux включен:


      sestatus
Судя по статусам, все хорошо.

Есть такое. Отлично. Продолжаем эксперимент:


      mkdir ~/selinux-test
echo hello > ~/selinux-test/file.txt

Смотрим контекст:


      ls -Z ~/selinux-test

Получаем unconfined_u:object_r:user_home_t:s0 file.txt.

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


      podman run --rm -it -v ~/selinux-test:/data alpine sh

В интерактивном режиме вводим:


      echo test > /data/test.txt

И… получаем ошибку: sh: can't create /data/test.txt: Permission denied. А что вообще произошло? Права есть, вы владелец, а оно того… не хочет. Куда копать?

Если так же сделать в Docker, то все будет нормально, файл создастся. Вот тут и кроются нюансы Podman: SELinux блокирует доступ. И чтобы нам это победить, нужен ключ Z. Выходим из контейнера и пересоздаем его, но уже с ключом:


      podman run --rm -it -v ~/selinux-test:/data:Z alpine sh

Снова пытаемся создать файл:


      echo works > /data/test.txt

Ура! Ошибка пропала, всё получилось. Смотрим на хосте:


      ls -Z ~/selinux-test

И видим уже другую картину:

Другая картина.

container_file_t теперь дает доступ к контейнеру.

Возможные ошибки

Следующий нюанс, снова про ключ Z. Если вы примонтировали папку в контейнер с этим ключом и попытаетесь примонтировать эту же папку в другой контейнер, то у вас ничего не получится. Ха!

Тут решение довольно простое: меняем на ключ z. И запоминаем:

  • :Z == приватный доступ (один контейнер);
  • :z == Shared доступ (несколько контейнеров).

По итогу у нас происходит такое: Podman делает relabel папки, присваивает container_file_t, добавляет MCS-метку, дает контейнеру право писать. Это не привычный chmod, это MAC (Mandatory Access Control).

Правило тут простое: если включен SELinux, при монтировании добавляйте Z либо z.

Дополнительно есть ключ U, он ремапит UID/GID. 


Нет, это не аналог параметра user: "1000:1000" от Docker, который меняет пользователь процесса внутри контейнера.

Ключ U используется при монтировании volume: Podman автоматически изменит владельца файлов на хосте под нужный UID/GID контейнера. То есть происходит chown на стороне хоста.

Когда вы запускаете rootless-контейнер, root внутри контейнера !== root на хосте. UID 0 внутри контейнера может соответствовать например UID 100000 на хосте. Масло масляное, но конкретную проблему при монтировании решает.

Итого: user = UID процесса внутри контейнера, а U = владельца файлов на хосте.

И по классике проводим эксперимент. Подготавливаем папку на хосте:


      mkdir demo
touch demo/file.txt
ls -ln demo

И видим:

Видим, что все получилось, есть наш пользователь.

1000 — это наш пользователь на хосте, все правильно и логично.

Теперь запускаем rooless контейнер:


      podman run --rm -v $(pwd)/demo:/data alpine sh -c "echo test >> /data/file.txt"

И получаем ошибку: sh: can't create /data/file.txt: Permission denied.

Дела! Почему так? Потому что внутри контейнера root = UID 0, а в rootless-режиме UID, к примеру, будет равен 10000, а файл принадлежит 1000. 10000 не равно 1000, отсюда и ошибка.

Смотрим мапинги:


      podman unshare cat /proc/self/uid_map
Мапинги.

UID 0 внутри контейнера соответствует UID 1000 на хосте, то есть контейнерный root = вы на хосте. Это особенность rootless-режима.

Вторая строка 1 524288 65536 — UID 1 внутри контейнера — соответствует UID 524288 на хосте.

И получается, что когда мы запускали команду из контейнера для создания файла, процесс работал под UID 1, соответственно его UID был равен 524288. Но на хосте UID был 1000. Поэтому мы получили ошибку. Ну а теперь используем ключ U:


      podman run --rm -v $(pwd)/demo:/data:ZU alpine sh -c "echo test >> /data/file.txt"

Теперь ошибки нет. Но не забываем предварительно воспользоваться ключом Z, чтобы контейнер получил container_file_t и право писать в файловую систему. Эти ключи не совмещаются, поэтому контекст выставляется так:


      chcon -R -t container_file_t demo

Выдаем рекурсивный контекст для всего каталога demo. В этом случае контейнер получит все нужные права для записи в каталог demo. Все это порой выглядит сумбурно, но таковы реалии выживания с SELinux.

Еще пара технических моментов

Когда контейнер стартует в SELinux, ему выдается уникальная MSC-метка (Multi-Category Security). Это механизм, который добавляет категории безопасности к объектам и процессам.


      system_u:system_r:container_t:s0:c123,c456

c123 / c456 — это MCS-категория. Если контейнеры имеют разные категории, то они не могут читать volume друг друга, даже если у папки будет установлено container_file_t. Так и работает реальная изоляция.

Поля контекста:

  • system_u == SELinux user,
  • object_r == роль,
  • container_file_t == тип (type enforcement),
  • s0:c123,c456 == уровень и категории.

Если запустить несколько контейнеров в Podman, контекст будет примерно таким:


      system_u:system_r:container_t:s0:c111,c222
system_u:system_r:container_t:s0:c333,c444
system_u:system_r:container_t:s0:c555,c666

Полная изоляция на уровне SELinux. Если всем контейнерам выдать тип container_file_t, они все равно не смогут читать файлы друг друга, потому что категории не совпадают. Категории — это что-то вроде «персонального штрих-кода».

Оверлейный маунт

А это уже низкоуровневая штука. Существует еще один ключ O, он позволяет сделать overlay поверх Bind Mount. И по итогу можно получить такое:

  • контейнер пишет в tmp overlay,
  • хостовая папка не меняется,
  • при удалении контейнера изменения пропадают.

Проводим эксперимент:


      mkdir data
echo hello > data/file.txt
ls data
В папке data у нас появился файл file.txt

Видим, что в папке data у нас появился файл file.txt, ожидаемо. Идем дальше.


      podman run --rm -it -v $(pwd)/data:/data:O alpine sh
echo test > /data/new.txt
cat /data/new.txt
Что-то ожидаемое.

Тоже вполне ожидаемо. Пока все выглядит хорошо. Теперь пишем exit, выходим из контейнера и запускаем ls data:

Что-то пошло не так, файла нет.

Опа! Файла new.txt нет и в помине, а в папке data мы видим тот самый file.txt. Магия!

Ключ O не меняет SELinux, не делает chown, не меняет контекст. Ключ O просто создает временный overlay, который работает прозрачно.

Чтобы убедиться, что контейнер использует overlay, внутри контейнера выполняем:


      podman run --rm -it -v $(pwd)/data:/data:O alpine sh
mount | grep data

И смотрим вывод:


      /home/user/.local/share/containers/storage/overlay-containers/1ae289eea96968a2cfabbd947f2dfbfd6f882c3fd425d4dbb38b7ad5fcf5dca0/userdata/overlay/2185336094/merge on /data type overlay (rw,context="system_u:object_r:container_file_t:s0:c385,c641",relatime,lowerdir=/home/user/data,upperdir=/home/user/.local/share/containers/storage/overlay-containers/1ae289eea96968a2cfabbd947f2dfbfd6f882c3fd425d4dbb38b7ad5fcf5dca0/
userdata/overlay/2185336094/upper,workdir=/home/user/.local/share/containers/storage/overlay-containers/1ae289eea96968a2cfabbd947f2dfbfd6f882c3fd425d4dbb38b7ad5fcf5dca0/userdata/overlay/2185336094/work,redirect_dir=nofollow,uuid=on,userxattr)

Нас интересует /data type overlay. Убедились, что оверлей активен. Раскапывать эту тему можно и дальше, нора очень глубокая и интересная. Но для рядового пользователя или инженера это избыточно. Достаточно знать азы и базу, все остальное — это уже гиковский путь и в реальной жизни применяется достаточно редко. Так что не забивайте себе голову.