SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-09-04

Rocky Linux 및 AlmaLinux에 Docker 설치하는 법

Rocky Linux와 AlmaLinux에 Docker Engine을 설치하는 올바른 방법을 안내합니다. Podman과의 충돌 해결, SELinux 바인드 마운트 권한 설정, 그리고 보안을 위해 피해야 할 편의 스크립트 사용법까지 실무적인 핵심 내용을 정리했습니다.

Rocky Linux 및 AlmaLinux에 Docker 설치하기

Rocky Linux나 AlmaLinux에 Docker를 설치하려면 Docker의 공식 dnf 저장소를 추가하고, compose 플러그인과 함께 엔진을 설치한 뒤 서비스를 활성화해야 합니다. 이 과정은 4개의 명령어로 구성되며, 두 배포판 모두 Red Hat Enterprise Linux(RHEL)를 기반으로 재구축되어 패키지 레이아웃을 공유하므로 동일하게 적용됩니다. CentOS Stream도 같은 방식으로 작동합니다. 아래의 모든 내용은 두 배포판에 공통으로 적용되므로, 아직 선택 중이라면 각 프로젝트가 제공하는 호환성 보장 수준과 구형 CPU 지원 여부가 결정 요인이 됩니다.

설치 과정은 짧으므로, 이 가이드의 대부분은 Enterprise Linux(EL)가 Ubuntu와 다르게 동작하는 부분을 다룹니다. 이미지에 따라 Podman이 이미 docker 명령어를 점유하고 있을 수 있습니다. SELinux는 바인드 마운트된 파일에 올바른 레이블이 지정되지 않으면 접근을 차단합니다. Firewalld는 Docker가 게시한 포트를 필터링하지 않으므로, firewall-cmd에서 열린 포트가 없다고 보고하더라도 컨테이너 포트는 인터넷에 노출될 수 있습니다.

get.docker.com에서 제공하는 Docker 편의 스크립트는 사용하지 마십시오. Docker 공식 문서에서도 프로덕션 환경에는 권장하지 않는다고 명시되어 있습니다. 이 스크립트는 사용자 동의 없이 저장소 설정을 재작성하며, 업그레이드를 위해 안전하게 다시 실행할 수 없습니다. 저장소를 직접 추가하면 dnf upgrade가 Docker를 시스템의 다른 패키지와 동일하게 관리합니다. 또한 이렇게 하면 dnf-automatic을 사용하여 보안 업데이트를 주기적으로 적용하는 경우 Docker도 관리 대상에 포함되므로, Docker를 자동으로 패치할지 아니면 유지보수 기간에 맞춰 수동으로 업데이트할지 일찍 결정해야 합니다. 어느 경우든 업그레이드가 수행되면 패키지된 바이너리는 교체되지만 기존 dockerd은 계속 실행되므로, needs-restarting 명령어를 사용하여 방금 교체한 코드를 여전히 실행 중인 서비스가 무엇인지 확인해야 합니다.

Podman이 이미 docker 명령에 응답하고 있습니까?

Rocky Linux와 AlmaLinux는 기본 저장소에 Podman을 포함하고 있으며, 많은 VPS 이미지에 기본적으로 설치되어 있습니다. 일부 이미지는 한 걸음 더 나아가 podman-docker를 설치하는데, 이는 /usr/bin/docker에 Podman을 호출하는 셸 스크립트를 배치합니다. 따라서 사용자가 입력하는 모든 docker 명령은 대신 Podman을 실행하게 되며, Docker용으로 작성된 가이드를 따라 할 때 예상치 못한 결과가 출력됩니다.

가장 먼저 확인할 수 있는 신호는 배너입니다. /usr/bin/docker 스크립트는 /etc/containers/nodocker 파일의 존재 여부를 확인하며, 해당 파일이 없을 경우 명령을 실행하기 전에 한 줄의 메시지를 출력합니다.

Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.

누군가 배너를 숨기기 위해 해당 파일을 생성했을 수 있으므로 이 신호에만 의존해서는 안 됩니다. 패키지 데이터베이스에 해당 바이너리를 소유한 패키지가 무엇인지 확인하십시오.

command -v docker
rpm -qf "$(command -v docker)"

podman-docker로 시작하는 결과가 나오면 Podman이 응답하고 있는 것입니다. docker-ce-cli으로 시작하는 결과가 나오면 실제 Docker입니다. 만약 rpm -qf이 해당 파일을 소유한 패키지가 없다고 보고한다면, 누군가 수동으로 설치한 것이므로 스크립트를 신뢰하기 전에 내용을 읽어보아야 합니다.

Podman은 동일한 OCI 이미지를 실행하며 합리적인 선택지입니다. Podman을 사용하려 한다면 여기서 멈추십시오. 두 도구 모두 리눅스 컨테이너 엔진이므로, 플랫폼 결정이 아직 내려지지 않았다면 FreeBSD jail은 레지스트리에서 가져온 계층형 이미지를 실행하는 대신 전체 사용자 공간을 격리한다는 점을 알아두는 것이 좋습니다. Docker Engine을 원한다면 먼저 충돌하는 패키지들을 제거하십시오. 다음은 Docker가 RHEL용으로 문서화한 목록입니다.

sudo dnf remove docker docker-client docker-client-latest docker-common \
  docker-latest docker-latest-logrotate docker-logrotate docker-engine podman runc

확인하기 전에 dnf가 함께 제거하려는 패키지 목록을 읽어보십시오. 새로 설치된 VPS 이미지에서는 목록이 짧습니다. 이미 사용 중인 서버라면 podman를 제거할 때 cockpit-podman이나 이에 의존하는 다른 도구까지 함께 삭제될 수 있습니다.

원칙적으로 Podman과 Docker를 함께 유지하는 것은 가능합니다. podman-docker만 제거하여 docker 이름을 비우고, containerd.io 패키지가 대체하는 runc을 제거하면 됩니다. 하지만 Docker의 문서는 Podman을 충돌하는 패키지로 간주하므로, 이러한 구성은 Docker가 지원하는 방식이 아닙니다. 설치 과정에서 여전히 충돌이 보고된다면 위에 나열된 전체 제거 목록을 사용하십시오.

dnf config-manager를 사용하여 Docker 저장소 추가하기

Docker는 Enterprise Linux용 RPM을 download.docker.com에 배포합니다. 저장소 파일은 CentOS 트리를 가리키며, Rocky Linux와 AlmaLinux는 이 트리를 참조합니다. 2020년 Red Hat이 CentOS를 Stream으로 전환한 이후 두 배포판이 어떻게 CentOS 계보에서 파생되었는지 알기 전까지는 Rocky 시스템을 CentOS 저장소에 연결하는 것이 잘못된 것처럼 보일 수 있습니다. 2026년 8월 확인 결과, Docker는 CentOS Stream 9 및 CentOS Stream 10에 대해 이 저장소를 공식 문서로 안내하고 있습니다.

sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

dnf 버전 5부터는 --add-repo 인수가 제거되었으므로, 최신 릴리스에서는 두 번째 명령이 실패합니다. 사용 중인 버전을 확인한 후 알맞은 형식을 선택하십시오.

dnf --version

5.x 버전이 출력된다면 하위 명령 형식을 사용하십시오.

sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/centos/docker-ce.repo

두 명령 모두 동일한 파일을 /etc/yum.repos.d/docker-ce.repo에 작성합니다. 잘못된 형식을 사용하면 조용히 오류가 발생하는 대신 알 수 없는 인수 오류가 발생하므로, 실수를 놓칠 염려는 없습니다.

해당 저장소 파일은 baseurl을 $releasever가 포함된 경로로 설정하며, dnf는 릴리스 패키지에서 이 변수를 확장합니다. Rocky Linux와 AlmaLinux는 이를 메이저 버전 번호로 설정하므로 EL 9에서는 9, EL 10에서는 10가 됩니다. 이것이 바로 Rocky 시스템에서 CentOS 저장소가 올바르게 작동하는 이유입니다. 설치하기 전에 확장이 올바른지 확인하십시오.

sudo dnf repoinfo docker-ce-stable

Repo-baseurl 줄을 읽어 보십시오. 끝이 /9/x86_64/stable 또는 /10/x86_64/stable으로 끝나야 합니다. 만약 릴리스 설정에서 $releasever이 9.6와 같은 포인트 버전으로 설정되어 있다면, dnf가 메타데이터를 가져올 때 해당 URL에 대해 Status code: 404 오류를 보고합니다. /etc/yum.repos.d/docker-ce.repo을 편집하여 $releasever를 메이저 버전 번호로 직접 수정하면 해결됩니다.

엔진 및 compose 플러그인 설치

sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

다섯 개의 패키지가 각각의 역할을 수행합니다. docker-ce은 데몬인 dockerd입니다. docker-ce-cli는 사용자가 입력하는 docker 명령어입니다. containerd.io은 데몬이 구동하는 컨테이너 런타임입니다. docker-buildx-plugin은 이미지를 빌드합니다. docker-compose-plugin는 docker compose을 하위 명령어로 제공합니다.

이 패키지들은 하이픈이 포함된 docker-compose 바이너리를 설치하지 않습니다. 이는 2023년 7월에 수명이 종료된 Compose v1입니다. 하이픈이 포함된 docker-compose를 호출하는 모든 항목은 공백이 포함된 docker compose으로 업데이트해야 합니다.

최초 설치 시 Docker의 서명 키를 가져오며 지문(fingerprint)을 표시합니다. 키는 방금 추가한 저장소 파일의 gpgkey=https://download.docker.com/linux/centos/gpg에서 가져오므로, 수락하기 전에 dnf가 출력하는 지문과 해당 URL의 지문을 비교하십시오.

자주 발생하는 오류가 하나 있습니다. dnf가 containerd.io에 container-selinux이 필요하지만 제공하는 패키지가 없다고 보고한다면, AppStream 저장소가 비활성화된 상태입니다. dnf repolist을 실행하여 appstream이 목록에 있는지 확인하십시오. EL 9 및 EL 10에서는 해당 저장소에서 container-selinux를 제공하기 때문입니다.

Docker 시작 및 실행 확인

sudo systemctl enable --now docker
sudo systemctl status docker --no-pager
sudo docker run --rm hello-world

Docker의 RPM 패키지는 설치 후 데몬을 중지된 상태로 비활성화해 둡니다. 이 단계가 Docker의 Ubuntu 페이지가 아닌 CentOS 페이지에 있는 이유는 Ubuntu의 deb 패키지는 설치 시 서비스를 자동으로 시작하기 때문입니다. enable 단계를 건너뛰면 Docker는 다음 재부팅 전까지만 실행되며, 재부팅 후에는 서비스가 내려가 모든 컨테이너도 함께 중단됩니다.

systemctl status 명령을 실행하면 Active: active (running)가 출력되어야 합니다. hello-world 컨테이너는 This message shows that your installation appears to be working correctly.를 출력하고 종료되어야 합니다. 만약 /var/run/docker.sock에서 권한 오류가 발생한다면 sudo을 누락한 것이며, 이는 아래의 docker 그룹 섹션에서 해결할 수 있습니다.

compose 플러그인은 별도의 패키지이므로 엔진이 정상 작동하더라도 누락될 수 있습니다. 따라서 별도로 확인해야 합니다.

docker compose version

정상적인 경우 Docker Compose version v2.x.x과 같은 결과가 출력됩니다. 재부팅 후 서비스를 자동으로 복구하는 것은 데몬 활성화와는 별개의 문제이며, 재시작 정책(restart policies)을 통해 Compose 서비스의 부팅 시 자동 시작 여부를 결정합니다.

바인드 마운트에서 권한 거부(permission denied)가 발생하는 이유는 무엇입니까?

Rocky Linux와 AlmaLinux는 기본적으로 SELinux(Security-Enhanced Linux)를 enforcing 모드로 실행합니다. getenforce 명령을 실행하여 이를 확인하십시오. 이 명령은 Enforcing를 출력합니다.

Docker 컨테이너는 SELinux 타입인 container_t 하에서 실행되며, 해당 타입은 container_file_t로 레이블이 지정된 파일만 읽고 쓸 수 있습니다. 호스트에서 생성한 디렉터리는 상위 경로의 레이블을 상속받는데, 이는 container_file_t가 아닙니다. 호스트 측에서 소유자, 그룹, 모드가 올바르게 보이더라도 컨테이너는 접근이 거부됩니다. 다음 세 가지 명령으로 이를 재현할 수 있습니다.

sudo mkdir -p /srv/site
echo hello | sudo tee /srv/site/index.html
sudo docker run --rm -v /srv/site:/usr/share/nginx/html:ro nginx:alpine cat /usr/share/nginx/html/index.html

컨테이너는 다음과 같이 출력합니다.

cat: can't open '/usr/share/nginx/html/index.html': Permission denied

두 가지 명령으로 원인을 확인할 수 있습니다. ls -ldZ /srv/site은 레이블을 출력하는데, /srv 하위 경로의 경우 container_file_t이 아닌 system_u:object_r:var_t:s0로 나타납니다. 그 다음 sudo ausearch -m avc -ts recent은 커널의 감사 기록을 출력하며, 여기에는 avc: denied { read }, container_t을 명시하는 scontext= 필드, 그리고 방금 디렉터리에서 확인한 레이블을 명시하는 tcontext= 필드가 포함되어 있습니다. 이 두 필드 사이의 불일치가 모든 문제의 원인입니다.

해결 방법은 볼륨 인수에 접미사를 추가하는 것입니다. Docker가 경로의 레이블을 자동으로 다시 지정합니다.

sudo docker run --rm -v /srv/site:/usr/share/nginx/html:ro,z nginx:alpine cat /usr/share/nginx/html/index.html

소문자 :z는 콘텐츠를 공유 가능하도록 레이블을 다시 지정하므로, 여러 컨테이너가 동일한 디렉터리를 사용할 수 있습니다. 대문자 :Z은 이를 비공유 전용으로 레이블을 다시 지정하며, 특정 컨테이너 하나에 종속됩니다. 따라서 두 번째 컨테이너가 동일한 경로를 읽으려 하면 거부됩니다. 사이드카나 백업 컨테이너가 접근해야 하는 모든 대상에는 :z를 사용하십시오. 단일 컨테이너가 소유하는 데이터베이스 디렉터리에는 :Z를 사용하십시오.

Docker 문서에는 반복해서 강조할 만한 경고가 있습니다. 레이블 재지정은 재귀적으로 수행되기 때문입니다. /home이나 /usr과 같은 시스템 디렉터리를 :Z로 바인드 마운트하면 "호스트 머신이 작동 불능 상태가 될 수 있으며, 호스트 머신의 파일을 수동으로 다시 레이블링해야 할 수도 있습니다." 이러한 접미사는 컨테이너를 위해 직접 생성한 디렉터리에만 사용하고, 시스템 경로에는 절대 사용하지 마십시오.

Compose에서는 동일한 문자열에 접미사를 추가합니다.

services:
  web:
    image: nginx:alpine
    volumes:
      - /srv/site:/usr/share/nginx/html:ro,z

두 가지 제한 사항에 주의해야 합니다. --mount 플래그는 SELinux 레이블을 설정할 수 없으므로, 레이블이 필요한 경우에는 -v을 사용하십시오. 명명된 볼륨(Named volumes)은 Docker가 /var/lib/docker/volumes 하위에 직접 생성한 디렉터리에 레이블을 지정하므로 접미사가 필요 없습니다.

SELinux를 끄지 마십시오. sudo setenforce 0는 1분 정도의 테스트 용도로만 사용하십시오. 만약 컨테이너가 정상 작동한다면 문제는 레이블에 있는 것이며, :z이 해결책입니다. 테스트 후에는 즉시 sudo setenforce 1로 복구하십시오. Enterprise Linux에서 바인드 마운트 시 발생하는 permission denied는 컨테이너 내부에서 볼 때 동일해 보이는 두 가지 원인이 있습니다. 하나는 SELinux 레이블 문제이고, 다른 하나는 일반적인 숫자 기반의 사용자 및 그룹 소유권 문제입니다. 이는 PUID 및 PGID 변수가 존재하는 이유이기도 합니다. ls -lnZ을 사용하면 모드, 숫자 기반 소유자, 레이블을 한 줄로 확인할 수 있으므로 어떤 문제에 직면했는지 파악할 수 있습니다.

firewalld가 닫혀 있는데도 게시된 포트에 접근할 수 있는 이유는 무엇입니까?

firewalld는 Rocky Linux와 AlmaLinux의 기본 방화벽입니다. sudo systemctl is-active firewalld 명령으로 방화벽이 실행 중인지 확인하십시오. 이 서버에서 아직 설정을 완료하지 않았다면 firewalld로 SSH 및 웹 포트 열기를 먼저 수행해야 합니다. 아래에서 설명할 현상은 비교할 수 있는 정상적인 영역(zone) 규칙 세트가 있을 때만 이해할 수 있기 때문입니다. 이제 포트를 게시하고 firewalld가 무엇을 열려 있다고 판단하는지 확인해 보십시오.

sudo docker run -d --name web -p 8080:80 nginx:alpine
sudo firewall-cmd --list-ports

firewall-cmd 명령은 빈 줄을 출력합니다. 다른 머신에서 curl -I http://YOUR_SERVER_IP:8080/ 명령을 실행하면 HTTP/1.1 200 OK 응답이 돌아옵니다. 포트는 인터넷에 열려 있는데 방화벽은 아무것도 보고하지 않는 상태입니다.

원인은 패킷이 이동하는 경로에 있습니다. firewalld의 영역 규칙은 호스트 자체를 대상으로 하는 트래픽을 필터링합니다. 게시된 포트는 호스트를 대상으로 하지 않습니다. Docker는 패킷이 호스트의 입력 경로에 도달하기 전에 목적지를 컨테이너 주소로 다시 쓰는 대상 NAT(네트워크 주소 변환) 규칙을 설치하므로, 커널은 패킷을 로컬로 전달하는 대신 포워딩합니다. Docker는 브리지 인터페이스를 docker이라는 firewalld 영역에 배치하며, 이 영역의 대상(target)은 ACCEPT입니다. 또한 모든 영역에서 docker 영역으로의 포워딩을 허용하는 docker-forwarding이라는 포워딩 정책을 추가합니다. 따라서 사용자의 영역 규칙은 패킷을 전혀 볼 수 없습니다.

가장 깔끔한 해결책은 방화벽 규칙을 사용하지 않는 것입니다. 게시할 호스트 측 포트를 루프백(loopback)에 바인딩하고 그 앞에 리버스 프록시를 두십시오.

sudo docker rm -f web
sudo docker run -d --name web -p 127.0.0.1:8080:80 nginx:alpine
curl -I http://127.0.0.1:8080/

로컬에서 curl를 실행하면 HTTP/1.1 200 OK이 반환되지만, 다른 머신에서 동일한 요청을 보내면 더 이상 연결되지 않습니다. -p 인수에 호스트 주소가 없는 모든 설정은 모든 인터페이스에 게시되므로, 단순히 -p 8080:80만 지정하는 것은 해당 서비스를 공개적으로 노출하겠다는 결정으로 간주해야 합니다.

특정 주소에서만 서비스에 접근할 수 있도록 제한해야 할 경우, Docker가 예약해 둔 체인을 사용하십시오. DOCKER-USER는 Docker 자체의 허용 규칙보다 먼저 처리되므로, 여기에 규칙을 추가하면 Docker가 재시작되어 체인을 다시 작성하더라도 규칙이 유지됩니다.

sudo iptables -I DOCKER-USER -i enp1s0 ! -s 203.0.113.10 -j DROP
sudo iptables -S DOCKER-USER

eth0을 가정하지 말고 ip route show default에서 인터페이스 이름을 확인하십시오. 최신 EL 이미지들은 enp1s0나 ens3 같은 이름을 사용하기 때문입니다. Rocky 및 AlmaLinux에서 iptables 명령은 nftables 위의 호환성 계층이며, Docker의 체인은 이를 통해 확인할 수 있습니다. 이 방식으로 추가한 규칙은 저장하지 않으면 재부팅 후 사라지므로, 설정이 만족스럽다면 systemd 유닛에 기록해 두십시오.

2025년에 출시된 Docker Engine 28.0은 인접한 보안 허점을 해결했습니다. 게시되지 않은 컨테이너 포트에 대한 직접적인 라우팅 접근이 이제 DOCKER 체인에서 차단됩니다. 이 변경 사항은 게시된 포트에는 영향을 미치지 않으므로, 위에서 설명한 모든 내용은 현재 버전에서도 동일하게 적용됩니다. 한 가지 운영 습관을 들이는 것이 좋습니다. sudo firewall-cmd --reload을 수행한 후에는 항상 게시된 포트를 다시 테스트하십시오. 만약 응답이 멈췄다면 sudo systemctl restart docker 명령으로 Docker의 규칙을 다시 설치할 수 있습니다.

Ubuntu 관리자들도 다른 도구를 통해 동일한 벽에 부딪히며, 이는 게시된 Docker 포트가 ufw 규칙을 무시하는 이유와 같습니다. 두 경우 모두 NAT 경로가 원인이며, 앞단에 있는 방화벽의 종류만 다를 뿐입니다.

docker 그룹에 root가 아닌 사용자 추가하기

모든 docker 명령 앞에 sudo를 입력하는 것은 번거로운 일이며, docker 그룹을 사용하면 이러한 불편을 해소할 수 있습니다.

sudo usermod -aG docker $USER
newgrp docker
docker run --rm hello-world

usermod -aG는 /etc/group를 수정하지만, 현재 셸은 이미 이전 그룹 목록을 가지고 있으므로 새로운 셸을 실행하기 전까지는 변경 사항이 적용되지 않습니다. newgrp docker를 실행하면 해당 그룹이 적용된 셸이 시작되므로 즉시 테스트할 수 있습니다. 새로운 SSH 세션은 자동으로 변경 사항을 반영합니다.

이 그룹이 부여하는 권한의 의미를 명확히 이해해야 합니다. 그룹 멤버십은 /var/run/docker.sock에 대한 쓰기 권한을 부여하며, 이 소켓과 통신할 수 있는 모든 주체는 호스트 파일 시스템을 마운트하는 컨테이너를 실행하도록 데몬에 요청할 수 있습니다. 다음 명령 한 줄로 그 의미를 확인할 수 있습니다.

docker run --rm -v /:/host alpine wc -l /host/etc/shadow

이 명령은 sudo 권한이 없는 계정으로도 root만 읽을 수 있는 파일을 읽어옵니다. Docker의 공식 설치 후 가이드에서도 명시하듯, docker 그룹은 root와 동일한 권한을 부여합니다. 해당 계정에 sudo를 부여할 의사가 있는 경우에만 계정을 그룹에 추가하십시오. 새로운 서버에 계정을 설정하는 중이라면, 사후에 처리하기보다 VPS에서의 최소 권한 사용자 설정 과정에서 함께 결정하십시오.

Docker는 데몬을 권한이 없는 사용자로 실행하는 rootless 모드도 제공합니다. 이는 별도의 설치 경로를 따르며 스토리지 드라이버 및 1024번 미만 포트의 동작 방식이 변경되므로, 나중에 추가하는 옵션이 아닌 별도의 프로젝트로 계획해야 합니다.

다음 단계

이제 엔진, compose 플러그인, 재부팅 후에도 유지되는 서비스, 그리고 위에서 설명한 세 가지 EL 전용 동작을 모두 갖추었습니다. 다음 단계는 서비스별 compose.yaml를 설정하는 것이며, Compose 파일의 구조에서 파일 형식과 이를 제어하는 명령어를 다룹니다. 컨테이너 호스트를 처음 운영하는 경우, VPS에서 Docker 실행하기를 통해 이 가이드에서 다루지 않은 서버 크기 산정, 스토리지, 이미지 관리 문제를 확인하십시오.

FAQ

Does Docker's CentOS repository work on Rocky Linux and AlmaLinux?

Yes. Add https://download.docker.com/linux/centos/docker-ce.repo with dnf config-manager. The baseurl in that file contains $releasever, and Rocky Linux and AlmaLinux expand it to the major version number, so an EL 9 box resolves to the CentOS 9 tree and an EL 10 box to the CentOS 10 tree. Confirm the expansion with sudo dnf repoinfo docker-ce-stable and read the Repo-baseurl line. A Status code: 404 when dnf fetches metadata means the variable expanded to a point release, and editing /etc/yum.repos.d/docker-ce.repo to use the bare major number fixes it.

Can Docker and podman be installed on the same server?

Docker's documentation lists podman and runc as conflicting packages and tells you to remove both before installing Docker Engine. The concrete clash is the podman-docker package, which owns /usr/bin/docker and turns every docker command into a podman command. Run rpm -qf "$(command -v docker)" to see which package owns that path. If the output starts with podman-docker, podman is answering. Keeping both engines is not a layout Docker supports, so on a server that matters, pick one.

Why does my container get permission denied on a bind mount?

SELinux is enforcing by default on Rocky Linux and AlmaLinux. Containers run as the type container_t and may only touch files labelled container_file_t, so a directory you created carries the wrong label and access is denied regardless of its owner and mode. Confirm it with ls -ldZ on the host path and sudo ausearch -m avc -ts recent, which prints avc: denied with the two mismatched contexts. Add :z to the volume argument for content shared between containers, or :Z for content private to one. Never point :Z at /home or /usr, because the relabel is recursive and will break the host.

Do I need to open a port in firewalld to publish a container port?

No, and that is the problem. Docker's NAT rule rewrites the destination address before the packet reaches the host's input path, so firewalld's zone rules never inspect it. Docker also puts its bridges in a firewalld zone called docker with target ACCEPT. A container started with -p 8080:80 is reachable from the internet while sudo firewall-cmd --list-ports prints nothing. Publish to a specific address with -p 127.0.0.1:8080:80 when only the host should reach the service, or insert filtering rules into the DOCKER-USER chain, which Docker processes before its own accept rules.

Is adding my user to the docker group safe?

It grants root. A member of the docker group can write to /var/run/docker.sock, and docker run --rm -v /:/host alpine wc -l /host/etc/shadow then reads a root-only file from an account with no sudo rights. Docker's post-install documentation states the same equivalence. Add only accounts you would already trust with sudo, and keep using sudo docker for shared or service accounts. Rootless mode is the alternative when you need containers under an unprivileged user, and it is a separate install path rather than a setting.