Число 1000 в выводе id ещё не гарантирует доступ к каталогу владельца 1000 на сервере. В тесте для статьи rootless Podman ответил Permission denied, хотя Docker записал файл. Внутри обоих контейнеров процесс имел UID 1000.

Два разных пользователя с номером 1000

Для теста использовали Alpine 3.24.2, Docker 29.8.2 и Podman 5.8.9. Docker запущен от root без userns‑remap, Podman запущен от author, UID 1000. Каталог /home/author/uploads: владелец 1000:1000, права 0700. У Alpine совпали image ID. Сеть контейнеров отключена, для Docker выбран драйвер vfs. Проверялись права записи, а скорость не измерялась.

Режим движка и карту пользователей показывают команды:

docker info
podman info --debug
podman unshare cat /proc/self/uid_map

Проверка записи под author:

podman run --rm --network none --user 1000:1000 \
  -v /home/author/uploads:/data \
 
docker.io/library/alpine:3.24 \
  sh -c "id; touch /data/podman-file"


Вывод unshare объяснил отказ: 0 1000 1 и 1 100 000 65536. Внутренний root отображался как хостовый UID 1000. Процесс с назначенным UID 1000 попадал в диапазон дополнительных идентификаторов и становился 100 999 на хосте. Каталог 0700 запрещал ему запись. Диапазоны заданы в /etc/subuid и /etc/subgid. Карта конкретного контейнера может отличаться от unshare.

С ‑userns=keep‑id запись прошла, созданный файл принадлежал 1000:1000 на хосте. Приложение должно допускать запуск под пользователем, отличным от указанного в USER образа. Вариант с:U проверен отдельно: папка и существующий файл сменили владельца с 1000:1000 на 100999:100999. :U рекурсивно меняет владельцев файлов на хосте. Для общей папки это затронет и соседние проекты.

На хосте с SELinux отказ может сохраниться и при подходящих Unix‑правах. У podman run для этого есть метки::Z для приватных данных,:z для совместного доступа контейнеров. Перемаркировывать так системные пути нельзя. Выключение SELinux заодно снимет другие его ограничения. В опыте с Alpine SELinux не проверялся.

Что означает отказ от демона

Команда docker run обращается к демону dockerd. Он управляет контейнерами, сетями и томами. Отсюда свойство rootful-сокета: приложение, получившее этот сокет, может управлять почти всем сервером. Монтировать такой сокет нужно столь же осторожно, как выдавать административные права.

Podman не требует постоянной работы общего демона при локальном запуске. Runtime и служебные процессы остаются. В rootless у каждого пользователя своё хранилище; можно запускать Podman и от root. Ядро у контейнеров и хоста остаётся общим. Для чужого недоверенного кода такая степень изоляции может быть недостаточной.

Убирать Docker ради одного rootless необязательно. В его rootless-режиме без root хоста работают оба: демон и контейнеры. В отличие от rootless, при userns-remap демон сохраняет привилегии: меняется только отображение UID контейнеров. Иногда достаточно сменить режим Docker.

При использовании Podman API снова появляется сокет: его клиент может выполнять код с привилегиями владельца сервиса, вплоть до root в режиме rootful. Нужны доверенные клиенты и проверенные файловые права. Удалённое соединение защищают через SSH или взаимную TLS-аутентификацию.

Образ тот же, окружение другое

Запись файла прошла. Но alias docker=podman касается только команд оболочки. Скрипту, использующему Docker API, alias не поможет. Сборку, плагины и сети придётся прогнать отдельно. Несовместимую зависимость нужно менять или сохранять для неё Docker.

У podman compose есть внешний провайдер Compose. Если установлены docker-compose и podman-compose, первым будет выбран docker-compose. Исполнителя задают через PODMAN_COMPOSE_PROVIDER. Нужно записать версию выбранного провайдера. Поддержку необходимых полей Compose проверяют с выбранным провайдером.

Во втором тесте PostgreSQL 16.15 перенесена из Docker в rootless Podman. Image ID совпадал, тома разные; драйвер Docker overlay2. Данные: таблица documents и отдельный каталог файлов. Для рабочего переноса нужны старые тома, конфигурационные файлы, секреты, версии, сеть, владельцы, метки SELinux и список постоянных данных. Бэкапы защищают от посторонних и восстанавливают отдельно. Digest образа исключает случайное обновление приложения; позже его меняют ради исправлений безопасности.

У базы и загрузок должна быть общая точка переноса

Дамп PostgreSQL согласован в пределах одной базы, файлы приложения в нём отсутствуют. В тесте запись выполнялась последовательно одним скриптом. При настоящем переносе сначала останавливают запись приложения и фоновых процессов, затем согласованно копируют базу данных и файлы. Живую папку PostgreSQL нельзя просто скопировать: нужен штатный бэкап или корректная физическая копия после остановки.

Контейнер new-db изолирован, app пуста, роль postgres; дополнительных расширений нет. Версии совместимы. Фрагмент запускается от root; Podman вызывается от author.

umask 077
docker exec -u postgres old-db pg_dump -Fc app > /tmp/app.dump &&
cat /tmp/app.dump | su - author -c \
  'XDG_RUNTIME_DIR=/run/user/1000 podman exec -i -u postgres new-db pg_restore --exit-on-error -d app'


Восстановилась одна строка; скопированный отдельно файл совпал с исходным. После ошибки pg_dump файл непригоден. После ошибки pg_restore в базе могут остаться частично загруженные данные. Перед повтором снова нужна пустая подготовленная база.

Когда сервер загрузится без открытой SSH-сессии

Для systemd-хоста подходит Quadlet: файл .container преобразуется в .service, rootless-файлы находятся в ~/.config/containers/systemd/. Quadlet доступен с Podman 4.4, ключи зависят от версии, актуальное руководство требует cgroup v2. К описанию контейнера можно добавить:

[Service]
Restart=on-failure
RestartSec=5

[Install]
WantedBy=
default.target

После правок выполняют systemctl ‑user daemon‑reload и запускают службу. Автозапуск задаёт [Install], сгенерированной службе enable не нужен. Linger позволяет пользовательскому сервису продолжать работу после выхода пользователя и при загрузке. Перезапуском управляет systemd; команда podman run ‑restart будет конкурировать с ним. Quadlet на Alpine не проверялся.

На репетиции используют восстановленный бэкап, тот же образ и отдельные порты. Проверяются запись файла, HTTP, перезагрузка с зависимыми службами, архитектура, healthcheck, лимиты и ротация логов. С прокси с 443 на 127.0.0.1:8080 порог низких портов менять не нужно. DNS, IPv6 и адрес клиента зависят от сетевого backend и версии.

Старый сервер не будет автоматически получать записи, сделанные после переключения.

В опыте после добавления второй строки и файла на новой стороне старая сохранила только первую строку и первый файл. Обратное восстановление в отдельную rollback_app вернуло обе строки; файлы совпали. HTTP-переключение не проверялось: после него нужно прочитать и записать данные через внешний адрес сервиса, проверить ошибки в логах. Запись и фоновые задания разрешены одной стороне; два PostgreSQL с общим каталогом данных не запускают. До открытия записи достаточно остановить новый сервис и вернуть трафик. После неё нужны обратный перенос либо согласованная копия с принятой потерей записей. Оба возврата требуют репетиции.