Fullmoon System

閉域環境のNetBox構築:Rocky Linux 8.10とPodmanコンテナバンドル

EdwardMoon

閉域環境のNetBox構築では、オンラインサーバーでパッケージとOCIイメージを集め、完全性を検証してオフラインサーバーへ搬入します。イメージだけをコピーすると、Podman依存RPM、tag、DB初期化、セキュリティ、自動起動で失敗しやすくなります。

対象はRocky Linux 8.10 x86_64のrootful Podmanです。2026年7月20日に確認した例はNetBox v4.6.5-5.0.2、PostgreSQL 18-alpine、Valkey 9.1-alpineです。本番搬入前に公式リリースとregistry digestを必ず再確認してください。

閉域NetBox構築:バージョン固定、オンライン収集、SHA-256検証、Podman起動
オンライン収集から起動、バックアップまでの流れ

前提と検証基準

  • 作成元と対象のRockyマイナー版、CPU、有効リポジトリをそろえる。
  • latestではなく正確な版のtagを使い、digestを記録する。
  • RPMは--resolve --alldepsでインストール済み依存も取得する。
  • 同一条件の試験サーバーでオフライン導入を再現する。
  • rootfulとrootlessを混用しない。閉域側はすべてsudo podmanで統一する。
cat /etc/rocky-release
uname -m
sudo dnf repolist --enabled
sudo podman --version
要素固定値検証点
NetBox Dockerv4.6.5-5.0.2NetBoxとnetbox-dockerの互換tag
PostgreSQL18-alpine既存volumeとのメジャー互換性
Valkey9.1-alpinetasksとcacheを分離
ホストRocky Linux 8.10同じCPU、リポジトリ、セキュリティ方針

アーキテクチャ

Web、worker、PostgreSQL、tasks用Valkey、cache用Valkeyを1つのPodに入れます。同じPodはネットワークnamespaceを共有するため、127.0.0.1でDBとValkeyへ接続できます。外部にはNetBoxの8080だけをホストの8000へ公開します。

netbox-pod
├── netbox             127.0.0.1:8080  ← host:8000
├── netbox-worker      RQ worker
├── netbox-postgres    127.0.0.1:5432
├── netbox-valkey      127.0.0.1:6379  (tasks)
└── netbox-valkey-cache 127.0.0.1:6380 (cache)

ネットワークの概念に不慣れなら、コンテナの概念演習を参照してください。

オンラインでバンドルを作成する

1:バージョンとディレクトリを固定する

搬入後に同じtagを再現できるよう変数へ固定します。ファイル名に版とCPUを含めると、複数バンドルの取り違えを減らせます。

NETBOX_TAG="v4.6.5-5.0.2"
POSTGRES_TAG="18-alpine"
VALKEY_TAG="9.1-alpine"
BUNDLE_ID="netbox-airgap-${NETBOX_TAG}-rocky8-$(uname -m)"
BUNDLE_ROOT="/opt/${BUNDLE_ID}"

sudo mkdir -p "${BUNDLE_ROOT}"/{rpms,images,env,scripts,manifests}
sudo chown -R "$(id -u):$(id -g)" "${BUNDLE_ROOT}"

2:Podman RPMと依存関係を集める

dnf download --resolveだけでは作成元に導入済みのパッケージを省く場合があります。閉域用では--alldepsも使います。有効リポジトリが異なると署名・依存問題が起こるため、一覧もmanifestへ保存します。

sudo dnf install -y dnf-plugins-core

sudo dnf download --resolve --alldeps   --destdir="${BUNDLE_ROOT}/rpms"   podman tar gzip openssl

sudo dnf repolist --enabled > "${BUNDLE_ROOT}/manifests/dnf-repolist.txt"
rpm -Kv "${BUNDLE_ROOT}"/rpms/*.rpm   > "${BUNDLE_ROOT}/manifests/rpm-signatures.txt"

3:正確なtagをPull・Saveする

NetBox DockerのtagはNetBox版と支援ファイル版を組み合わせています。正確なtagを使い、Pull直後のdigestとimage IDを記録します。podman saveは層とtagを保持し、FSだけを平坦化するpodman exportとは異なります。

podman pull "docker.io/netboxcommunity/netbox:${NETBOX_TAG}"
podman pull "docker.io/postgres:${POSTGRES_TAG}"
podman pull "docker.io/valkey/valkey:${VALKEY_TAG}"

{
  podman image inspect --format 'netbox {{.Digest}} {{.Id}}'     "docker.io/netboxcommunity/netbox:${NETBOX_TAG}"
  podman image inspect --format 'postgres {{.Digest}} {{.Id}}'     "docker.io/postgres:${POSTGRES_TAG}"
  podman image inspect --format 'valkey {{.Digest}} {{.Id}}'     "docker.io/valkey/valkey:${VALKEY_TAG}"
} > "${BUNDLE_ROOT}/manifests/image-digests.txt"

podman save --format oci-archive   -o "${BUNDLE_ROOT}/images/netbox.tar"   "docker.io/netboxcommunity/netbox:${NETBOX_TAG}"
podman save --format oci-archive   -o "${BUNDLE_ROOT}/images/postgres.tar"   "docker.io/postgres:${POSTGRES_TAG}"
podman save --format oci-archive   -o "${BUNDLE_ROOT}/images/valkey.tar"   "docker.io/valkey/valkey:${VALKEY_TAG}"

4:既定パスワードを使わず環境ファイルを作る

例示パスワードを公開・再利用しないでください。次は作成時に乱数を生成し、権限を600に制限します。ALLOWED_HOSTSの名前とアドレスを実環境へ変更してください。

umask 077

DB_PASSWORD="$(openssl rand -hex 24)"
VALKEY_PASSWORD="$(openssl rand -hex 24)"
VALKEY_CACHE_PASSWORD="$(openssl rand -hex 24)"
ADMIN_PASSWORD="$(openssl rand -hex 18)"
API_TOKEN_PEPPER_1="$(openssl rand -hex 32)"
SECRET_KEY="$(podman run --rm   "docker.io/netboxcommunity/netbox:${NETBOX_TAG}"   python /opt/netbox/netbox/generate_secret_key.py)"

cat > "${BUNDLE_ROOT}/env/postgres.env" << EOF
POSTGRES_DB=netbox
POSTGRES_USER=netbox
POSTGRES_PASSWORD=${DB_PASSWORD}
EOF

cat > "${BUNDLE_ROOT}/env/valkey.env" << EOF
VALKEY_PASSWORD=${VALKEY_PASSWORD}
EOF

cat > "${BUNDLE_ROOT}/env/valkey-cache.env" << EOF
VALKEY_PASSWORD=${VALKEY_CACHE_PASSWORD}
EOF

cat > "${BUNDLE_ROOT}/env/netbox.env" << EOF
DB_HOST=127.0.0.1
DB_PORT=5432
DB_NAME=netbox
DB_USER=netbox
DB_PASSWORD=${DB_PASSWORD}

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DATABASE=0
REDIS_PASSWORD=${VALKEY_PASSWORD}
REDIS_SSL=false

REDIS_CACHE_HOST=127.0.0.1
REDIS_CACHE_PORT=6380
REDIS_CACHE_DATABASE=1
REDIS_CACHE_PASSWORD=${VALKEY_CACHE_PASSWORD}
REDIS_CACHE_SSL=false

SECRET_KEY=${SECRET_KEY}
API_TOKEN_PEPPER_1=${API_TOKEN_PEPPER_1}
ALLOWED_HOSTS=netbox.internal 10.20.30.40 localhost
TIME_ZONE=Asia/Seoul
LOGIN_REQUIRED=true
ISOLATED_DEPLOYMENT=true
CENSUS_REPORTING_ENABLED=false
COPILOT_ENABLED=false

SKIP_SUPERUSER=false
SUPERUSER_NAME=admin
SUPERUSER_EMAIL=netbox-admin@example.internal
SUPERUSER_PASSWORD=${ADMIN_PASSWORD}
EOF

chmod 600 "${BUNDLE_ROOT}"/env/*
printf 'admin password: %s
' "${ADMIN_PASSWORD}"   > "${BUNDLE_ROOT}/env/initial-admin-password.txt"
chmod 600 "${BUNDLE_ROOT}/env/initial-admin-password.txt"

初回ログイン後は管理者パスワードを変更し、netbox.envSUPERUSER_PASSWORDを除いてSKIP_SUPERUSER=trueへ変更すると安全です。公式変数名はSUPERUSER_USERNAMEではなくSUPERUSER_NAMEです。

5:起動スクリプト

PostgreSQLと両Valkeyの準備完了後にWebとworkerを起動します。公式構成に合わせてnetbox:rootで実行し、workerでは管理者生成を省きます。

cat > "${BUNDLE_ROOT}/scripts/run-netbox-pod.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

NETBOX_TAG="v4.6.5-5.0.2"
POSTGRES_TAG="18-alpine"
VALKEY_TAG="9.1-alpine"
BUNDLE_ROOT="/opt/netbox-airgap-${NETBOX_TAG}-rocky8-$(uname -m)"

NETBOX_ENV="${BUNDLE_ROOT}/env/netbox.env"
PG_ENV="${BUNDLE_ROOT}/env/postgres.env"
VALKEY_ENV="${BUNDLE_ROOT}/env/valkey.env"
VALKEY_CACHE_ENV="${BUNDLE_ROOT}/env/valkey-cache.env"

for file in "${NETBOX_ENV}" "${PG_ENV}" "${VALKEY_ENV}" "${VALKEY_CACHE_ENV}"; do
  [[ -r "${file}" ]] || { echo "[ERROR] missing ${file}" >&2; exit 1; }
done

if ! podman pod exists netbox-pod; then
  podman pod create --name netbox-pod -p 8000:8080
fi

for volume in netbox-postgres netbox-valkey-data netbox-valkey-cache-data               netbox-media-files netbox-reports-files netbox-scripts-files; do
  podman volume inspect "${volume}" >/dev/null 2>&1 || podman volume create "${volume}"
done

if ! podman container exists netbox-postgres; then
  podman run -d --name netbox-postgres --pod netbox-pod     --env-file "${PG_ENV}"     -v netbox-postgres:/var/lib/postgresql     "docker.io/postgres:${POSTGRES_TAG}"
fi

if ! podman container exists netbox-valkey; then
  podman run -d --name netbox-valkey --pod netbox-pod     --env-file "${VALKEY_ENV}"     -v netbox-valkey-data:/data     "docker.io/valkey/valkey:${VALKEY_TAG}"     sh -c 'valkey-server --appendonly yes --port 6379 --requirepass "$VALKEY_PASSWORD"'
fi

if ! podman container exists netbox-valkey-cache; then
  podman run -d --name netbox-valkey-cache --pod netbox-pod     --env-file "${VALKEY_CACHE_ENV}"     -v netbox-valkey-cache-data:/data     "docker.io/valkey/valkey:${VALKEY_TAG}"     sh -c 'valkey-server --port 6380 --requirepass "$VALKEY_PASSWORD"'
fi

dependencies_ready=0
for attempt in $(seq 1 90); do
  if podman exec netbox-postgres sh -c        'pg_isready -q -t 2 -d "$POSTGRES_DB" -U "$POSTGRES_USER"'     && podman exec netbox-valkey sh -c        'valkey-cli --pass "$VALKEY_PASSWORD" -p 6379 ping 2>/dev/null | grep -qx PONG'     && podman exec netbox-valkey-cache sh -c        'valkey-cli --pass "$VALKEY_PASSWORD" -p 6380 ping 2>/dev/null | grep -qx PONG'; then
    dependencies_ready=1
    break
  fi
  sleep 2
done

[[ "${dependencies_ready}" -eq 1 ]] || {
  echo "[ERROR] PostgreSQL or Valkey did not become ready" >&2
  exit 1
}

if ! podman container exists netbox; then
  podman run -d --name netbox --pod netbox-pod     --user netbox:root     --env-file "${NETBOX_ENV}"     -v netbox-media-files:/opt/netbox/netbox/media     -v netbox-reports-files:/opt/netbox/netbox/reports     -v netbox-scripts-files:/opt/netbox/netbox/scripts     "docker.io/netboxcommunity/netbox:${NETBOX_TAG}"
fi

netbox_ready=0
for attempt in $(seq 1 120); do
  if podman exec netbox curl -fsS http://127.0.0.1:8080/login/ >/dev/null 2>&1; then
    netbox_ready=1
    break
  fi
  sleep 2
done

[[ "${netbox_ready}" -eq 1 ]] || {
  echo "[ERROR] NetBox did not become ready" >&2
  podman logs --tail 100 netbox >&2
  exit 1
}

if ! podman container exists netbox-worker; then
  podman run -d --name netbox-worker --pod netbox-pod     --user netbox:root     --env-file "${NETBOX_ENV}"     -e SKIP_SUPERUSER=true     -v netbox-media-files:/opt/netbox/netbox/media     -v netbox-reports-files:/opt/netbox/netbox/reports     -v netbox-scripts-files:/opt/netbox/netbox/scripts     "docker.io/netboxcommunity/netbox:${NETBOX_TAG}"     /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py rqworker
fi

podman pod ps
podman ps --pod
echo "NetBox: http://<server-ip>:8000"
SCRIPT

chmod 700 "${BUNDLE_ROOT}/scripts/run-netbox-pod.sh"

6:データを保持する停止・初期化スクリプト

名前にpostgresredisを含む全コンテナとvolumeを消すと、他サービスのデータも失う場合があります。次は正確なNetBoxリソースだけを対象にし、既定ではデータvolumeを保持します。

cat > "${BUNDLE_ROOT}/scripts/reset-netbox-pod.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

mode="${1:-keep-data}"

podman pod rm -f netbox-pod 2>/dev/null || true
podman rm -f netbox netbox-worker netbox-postgres   netbox-valkey netbox-valkey-cache 2>/dev/null || true

if [[ "${mode}" == "--delete-data" ]]; then
  read -r -p '데이터를 완전히 삭제하려면 DELETE-NETBOX-DATA 입력: ' answer
  [[ "${answer}" == "DELETE-NETBOX-DATA" ]] || {
    echo "취소했습니다."
    exit 1
  }

  for volume in netbox-postgres netbox-valkey-data netbox-valkey-cache-data                 netbox-media-files netbox-reports-files netbox-scripts-files; do
    podman volume rm -f "${volume}" 2>/dev/null || true
  done
else
  echo "컨테이너만 제거했습니다. 데이터 볼륨은 보존됩니다."
fi
SCRIPT

chmod 700 "${BUNDLE_ROOT}/scripts/reset-netbox-pod.sh"

7:SHA-256 manifestと圧縮ファイル

chmod 600 "${BUNDLE_ROOT}"/env/*

(
  cd "${BUNDLE_ROOT}"
  find rpms images env scripts manifests -type f -print0     | sort -z     | xargs -0 sha256sum > SHA256SUMS
)

cd /opt
sudo tar --xattrs --selinux -czf "${BUNDLE_ID}.tar.gz" "${BUNDLE_ID}"
sudo sha256sum "${BUNDLE_ID}.tar.gz" > "${BUNDLE_ID}.tar.gz.sha256"

オフラインでインストールする

1:外側と内部ファイルの完全性を検証する

展開前に外側のSHA-256、展開後に内部manifestを検証します。1つでも失敗したら導入せず、媒体と原本を確認してください。

cd /opt
sudo sha256sum -c netbox-airgap-v4.6.5-5.0.2-rocky8-x86_64.tar.gz.sha256
sudo tar --xattrs --selinux -xzf netbox-airgap-v4.6.5-5.0.2-rocky8-x86_64.tar.gz

cd /opt/netbox-airgap-v4.6.5-5.0.2-rocky8-x86_64
sudo sha256sum -c SHA256SUMS
sudo chown -R root:root .
sudo chmod 600 env/*

2:外部リポジトリなしでPodmanを導入する

cd /opt/netbox-airgap-v4.6.5-5.0.2-rocky8-x86_64
sudo dnf install -y --disablerepo='*' ./rpms/*.rpm
sudo podman --version

依存エラー時は閉域側で任意のRPMを追加せず、作成元のOS、CPU、有効リポジトリが対象と同じか確認して作り直します。

3:OCIをロードしてtagを確認する

cd /opt/netbox-airgap-v4.6.5-5.0.2-rocky8-x86_64

sudo podman load -i images/netbox.tar
sudo podman load -i images/postgres.tar
sudo podman load -i images/valkey.tar

sudo podman images --digests   | grep -E 'netboxcommunity/netbox|postgres|valkey/valkey'
cat manifests/image-digests.txt

manifestと一致しなければ起動しません。rootlessでロードしたイメージはsudo podmanに見えないため、ロードと実行ユーザーを合わせます。

4:ファイアウォールを制限して初回起動する

例は管理網10.20.0.0/16からTCP 8000だけを許可します。実管理網へ変更してください。先にファイアウォールを設定してから起動すると、ネットワーク再適用の問題を減らせます。

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.20.0.0/16" port protocol="tcp" port="8000" accept'
sudo firewall-cmd --reload

cd /opt/netbox-airgap-v4.6.5-5.0.2-rocky8-x86_64
sudo ./scripts/run-netbox-pod.sh
curl -fsS http://127.0.0.1:8000/login/ -o /dev/null
sudo podman pod ps
sudo podman ps --pod

ブラウザーからhttp://<閉域サーバーIP>:8000へ接続します。長期運用では8000を広く公開せず、内部リバースプロキシとTLSを使う方が安全です。

再起動後の自動開始

systemdへ移すまでは起動スクリプトは新規リソース作成用です。同名の停止コンテナがあれば、先にsudo podman pod start netbox-podで開始します。systemd移行後はsystemctlへ統一し、初期化前にsudo systemctl stop pod-netbox-pod.serviceで管理サービスを止めます。

Rocky Linux 8.10のPodmanでは生成したsystemd unitを利用できます。--newの生成はベストエフォートなので、必ず内容を確認して導入します。Podman 5系ではこのコマンドは非推奨のため、新規基盤ではQuadletを優先して検討します。

unit_dir="$(mktemp -d)"
cd "${unit_dir}"
sudo podman generate systemd --new --files --name netbox-pod

sudo install -m 0644 ./*.service /etc/systemd/system/
sudo restorecon -Rv /etc/systemd/system

sudo podman pod rm -f netbox-pod
sudo systemctl daemon-reload
sudo systemctl enable --now pod-netbox-pod.service
sudo systemctl status pod-netbox-pod.service --no-pager

セキュリティ確認

  • ALLOWED_HOSTS=*を使わず、実FQDN・IPとlocalhostを指定する。
  • 既定パスワードを避け乱数を生成し、環境・バックアップファイルを600にする。
  • 初回後に管理者パスワードを変更し、環境ファイルの初期値を除く。
  • 8000は管理網だけに許可し、可能なら内部TLSプロキシを使う。
  • tagだけでなくdigestと全バンドルのSHA-256を承認記録へ残す。
  • 初期化は正確なNetBoxリソースだけを消し、全体のpruneを自動実行しない。

状態確認と障害対応

基本状態とログ

sudo podman pod ps
sudo podman ps -a --pod

sudo podman logs --tail 100 netbox
sudo podman logs --tail 100 netbox-worker
sudo podman logs --tail 100 netbox-postgres
sudo podman logs --tail 100 netbox-valkey
sudo podman logs --tail 100 netbox-valkey-cache

sudo podman exec netbox   /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py version

ログでルートが満杯なら無闇にコンテナを削除せず、No space left on deviceの診断でブロック、inode、開いたままの削除ファイルを切り分けます。

よくあるエラー

症状最初の確認対処
DB待機後にNetBox終了PostgreSQLログとDB_*同じPodの127.0.0.1:5432か確認
400 Bad RequestALLOWED_HOSTS実FQDN・IPを空白区切りで追加
初期adminログイン失敗SUPERUSER_NAME変数名と初回起動ログを確認
sudoでイメージが見えないrootlessとrootfulの混用sudo podman loadで再ロード
firewall reload後に接続不能Podmanネットワークルールsudo podman network reload --all
ローカルRPMの依存エラーOS、CPU、リポジトリ同条件で--alldepsを使い再作成
sudo podman exec netbox-postgres sh -c   'pg_isready -d "$POSTGRES_DB" -U "$POSTGRES_USER"'

sudo podman exec netbox-valkey sh -c   'valkey-cli --pass "$VALKEY_PASSWORD" -p 6379 ping'

sudo podman network reload --all

マイグレーションはNetBox起動中に行われます。手動のmanage.py migrateは、DBバックアップとイメージ互換性を確認した後の最終手段にしてください。

バックアップと復旧

更新・再配布前にPostgreSQL dumpとmediaを一緒に保存します。Redis・Valkeyはキューとキャッシュなので主要原本はDBとmediaですが、復旧目標に応じてscriptsとreportsも保管します。

# アプリケーションの書き込みを止めた変更時間帯に、DBとmediaを1セットで保存します。
sudo bash -euo pipefail <<'BASH'
backup_dir="/var/backups/netbox/$(date +%F-%H%M%S)"
install -d -m 0700 "$backup_dir"
umask 077
podman exec netbox-postgres pg_dump -U netbox -d netbox -Fc > "$backup_dir/netbox.dump"
test -s "$backup_dir/netbox.dump"
podman exec -i netbox-postgres pg_restore --list < "$backup_dir/netbox.dump" > "$backup_dir/dump-contents.txt"
podman volume export --output "$backup_dir/netbox-media-files.tar" netbox-media-files
(cd "$backup_dir" && sha256sum netbox.dump netbox-media-files.tar dump-contents.txt > SHA256SUMS && sha256sum -c SHA256SUMS)
BASH

作成だけでは不十分です。別の試験サーバーでDB復元と添付アクセスを定期検証してください。

よくある質問

Podを使う理由は何ですか

namespaceを共有してDBとValkeyへ127.0.0.1で接続でき、外部公開ポートもまとめて管理できるためです。リソース分離や独立した拡張を重視するなら、別Podやオーケストレーターを検討します。

RPM作成サーバーの条件は何ですか

同じRocky Linux 8.10、CPU、有効リポジトリが最も安全です。作成元の導入済み依存も含むよう--alldepsを使います。

latestを使ってもよいですか

推奨しません。同じ名前が別イメージへ移り、承認済みバンドルの再現が難しくなります。正確なtagとdigestを記録し、更新は新バンドルとして分けて検証します。

まとめ

安全な閉域NetBox構築の中心はコピーではなく、版の固定、同じRPM条件、digestとSHA-256、secret管理、正確な削除範囲、復元試験です。この順序で、インターネットのないRocky Linuxでも再現可能なPodman運用を行えます。

公式資料