先日、セルフホスト可能なWebベースのファイラーで、かなり使い勝手の良さそうな「nextExplorer」(GitHub)を紹介ましたが、どうやらOnlyOfficeとも連携出来るようなので、試してみました。

構成の全体像
[ブラウザ]
↕ HTTPS (Tailscale)
[nextExplorer :3317] ←→ [OnlyOffice :3322]
↕ ファイルAPI / コールバック
(同一コンテナ内 → localhost で疎通可能)
OnlyOfficeはTailscale Serveで別ポート(例:3322)として公開し、nextExplorerからは http://127.0.0.1:3322 で参照します。
LXDコンテナ内にnextExplorerとOnlyOfficeをセットアップ
#!/usr/bin/env bash
# =============================================================================
# nextExplorer + OnlyOffice セットアップスクリプト v4
# - /opt/nextexplorer に nextExplorer を配置
# - /opt/onlyoffice に OnlyOffice Document Server を配置
# - Tailscale Serve: nextExplorer :3317 / OnlyOffice :3322
# - 既存の Serve 設定は壊さない(--bg)
# - JWT シークレットを自動生成して両サービスに共有
# =============================================================================
set -euo pipefail
# ── 固定設定 ──────────────────────────────────────────────────────────────────
INSTALL_DIR="/opt/nextexplorer"
OO_INSTALL_DIR="/opt/onlyoffice"
HOST_PORT=3317
OO_HOST_PORT=3322
CONTAINER_PORT=3000
OO_CONTAINER_PORT=80
DATA_DIR="/srv/nextexplorer"
DEFAULT_MOUNT_SRC="/opt/lxd-data"
DEFAULT_MOUNT_LABEL="Files"
PUID=$(id -u)
PGID=$(id -g)
# ── 色付きログ ─────────────────────────────────────────────────────────────────
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
ok() { echo -e "\033[1;32m[ OK ]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
die() { echo -e "\033[1;31m[ERR ]\033[0m $*" >&2; exit 1; }
# ── 前提チェック ───────────────────────────────────────────────────────────────
info "前提確認..."
command -v docker >/dev/null 2>&1 || die "docker が見つかりません"
docker compose version >/dev/null 2>&1 || die "docker compose (v2) が見つかりません"
command -v tailscale >/dev/null 2>&1 || die "tailscale が見つかりません"
tailscale status >/dev/null 2>&1 || die "Tailscale が認証されていません"
ok "前提 OK"
# ── Tailscale hostname 取得 ────────────────────────────────────────────────────
TS_HOSTNAME=$(tailscale status --json \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['Self']['DNSName'].rstrip('.'))" \
2>/dev/null) || die "Tailscale の DNSName を取得できませんでした"
PUBLIC_URL="https://${TS_HOSTNAME}:${HOST_PORT}"
OO_PUBLIC_URL="https://${TS_HOSTNAME}:${OO_HOST_PORT}"
info "Tailscale ホスト : ${TS_HOSTNAME}"
info "nextExplorer URL : ${PUBLIC_URL}"
info "OnlyOffice URL : ${OO_PUBLIC_URL}"
# ── OnlyOffice インストール確認 ────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
read -rp " OnlyOffice Document Server もインストールしますか? [Y/n]: " install_oo
install_oo="${install_oo:-Y}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ── マウントディレクトリの入力 ────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " マウントするディレクトリを設定します。"
echo " 複数指定可能。空 Enter で終了(1つも入力しない場合はデフォルト設定を使用)。"
echo " デフォルト: ${DEFAULT_MOUNT_SRC} → UI ラベル「${DEFAULT_MOUNT_LABEL}」"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
declare -a MOUNT_ENTRIES=()
mount_index=1
while true; do
read -rp " マウント #${mount_index} ホストパス(空 Enter でスキップ/終了): " input_path
[[ -z "${input_path}" ]] && break
if [[ ! -d "${input_path}" ]]; then
warn "${input_path} は存在しません。作成しますか? [y/N]: "
read -rp " " yn
if [[ "${yn}" =~ ^[Yy]$ ]]; then
mkdir -p "${input_path}"
ok "${input_path} を作成しました"
else
warn "スキップします"
continue
fi
fi
default_label=$(basename "${input_path}")
read -rp " マウント #${mount_index} UI ラベル名(空 Enter で「${default_label}」): " input_label
[[ -z "${input_label}" ]] && input_label="${default_label}"
MOUNT_ENTRIES+=("${input_path}:${input_label}")
ok "追加: ${input_path} → /mnt/${input_label}"
(( mount_index++ ))
done
if [[ ${#MOUNT_ENTRIES[@]} -eq 0 ]]; then
warn "入力なし。デフォルト設定を使用します: ${DEFAULT_MOUNT_SRC} → ${DEFAULT_MOUNT_LABEL}"
mkdir -p "${DEFAULT_MOUNT_SRC}"
MOUNT_ENTRIES=("${DEFAULT_MOUNT_SRC}:${DEFAULT_MOUNT_LABEL}")
fi
echo ""
info "マウント設定:"
for entry in "${MOUNT_ENTRIES[@]}"; do
echo " ${entry%%:*} → /mnt/${entry##*:}"
done
echo ""
# ── ディレクトリ作成 & パーミッション設定 ─────────────────────────────────────
info "ディレクトリ作成..."
mkdir -p "${INSTALL_DIR}"
mkdir -p "${DATA_DIR}/config"
mkdir -p "${DATA_DIR}/cache"
# DATA_DIR(config/cache)のみパーミッション設定。
# マウントソース(/opt/lxd-data 等)は他サービスのファイルが混在する可能性があるため触らない。
chown -R "${PUID}:${PGID}" "${DATA_DIR}"
chmod -R u=rwX,g=rwX,o=rX "${DATA_DIR}"
if command -v setfacl >/dev/null 2>&1; then
setfacl -R -m u:1000:rwX "${DATA_DIR}"
setfacl -R -d -m u:1000:rwX "${DATA_DIR}"
info "ACL: ${DATA_DIR} に uid=1000:rwX を付与しました"
else
chmod -R o+w "${DATA_DIR}/config" "${DATA_DIR}/cache"
warn "acl 未インストールのため chmod o+w で代替しました(sudo apt install acl で改善可)"
fi
ok "ディレクトリ & パーミッション設定完了"
warn "マウントソースのパーミッションは変更していません。アップロード失敗時は手動で設定してください。"
# ── シークレット生成 ───────────────────────────────────────────────────────────
# SESSION_SECRET(既存があれば再利用)
SECRET_FILE="${INSTALL_DIR}/.session_secret"
if [[ -f "${SECRET_FILE}" ]]; then
SESSION_SECRET=$(cat "${SECRET_FILE}")
info "既存の SESSION_SECRET を再利用します"
else
SESSION_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
echo "${SESSION_SECRET}" > "${SECRET_FILE}"
chmod 600 "${SECRET_FILE}"
info "SESSION_SECRET を新規生成しました"
fi
# ONLYOFFICE JWT シークレット(既存があれば再利用)
OO_SECRET_FILE="${INSTALL_DIR}/.onlyoffice_secret"
if [[ -f "${OO_SECRET_FILE}" ]]; then
OO_SECRET=$(cat "${OO_SECRET_FILE}")
info "既存の ONLYOFFICE_SECRET を再利用します"
else
OO_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
echo "${OO_SECRET}" > "${OO_SECRET_FILE}"
chmod 600 "${OO_SECRET_FILE}"
info "ONLYOFFICE_SECRET を新規生成しました"
fi
# ── OnlyOffice セットアップ ────────────────────────────────────────────────────
if [[ "${install_oo}" =~ ^[Yy] ]]; then
info "OnlyOffice のディレクトリを作成..."
mkdir -p "${OO_INSTALL_DIR}"/{logs,data,lib,db}
cat > "${OO_INSTALL_DIR}/docker-compose.yml" <<EOF
# OnlyOffice Document Server — docker-compose.yml
# 生成日: $(date '+%Y-%m-%d %H:%M:%S')
# アクセス URL: ${OO_PUBLIC_URL}
services:
onlyoffice:
image: onlyoffice/documentserver:latest
container_name: onlyoffice
restart: unless-stopped
ports:
- "127.0.0.1:${OO_HOST_PORT}:${OO_CONTAINER_PORT}"
environment:
JWT_ENABLED: "true"
JWT_SECRET: "${OO_SECRET}"
volumes:
- ${OO_INSTALL_DIR}/logs:/var/log/onlyoffice
- ${OO_INSTALL_DIR}/data:/var/www/onlyoffice/Data
- ${OO_INSTALL_DIR}/lib:/var/lib/onlyoffice
- ${OO_INSTALL_DIR}/db:/var/lib/postgresql
EOF
ok "OnlyOffice docker-compose.yml → ${OO_INSTALL_DIR}/docker-compose.yml"
info "OnlyOffice コンテナを起動..."
docker compose -f "${OO_INSTALL_DIR}/docker-compose.yml" pull
docker compose -f "${OO_INSTALL_DIR}/docker-compose.yml" up -d
ok "OnlyOffice コンテナ起動完了(初回起動は2〜3分かかります)"
# Tailscale Serve に OnlyOffice を追加
EXISTING_SERVE=$(tailscale serve status 2>/dev/null || true)
if echo "${EXISTING_SERVE}" | grep -q ":${OO_HOST_PORT}"; then
warn "ポート ${OO_HOST_PORT} はすでに Tailscale Serve に登録されています。スキップします。"
else
info "Tailscale Serve にポート ${OO_HOST_PORT} を追加..."
tailscale serve --bg --https="${OO_HOST_PORT}" "http://127.0.0.1:${OO_HOST_PORT}"
ok "OnlyOffice の Tailscale Serve 設定追加完了"
fi
else
info "OnlyOffice のインストールをスキップします"
# 既存の OnlyOffice シークレットを流用するか確認
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^onlyoffice$"; then
info "既存の onlyoffice コンテナを検出。シークレットを取得します..."
DETECTED=$(docker exec onlyoffice \
jq -r '.services.CoAuthoring.secret.session.string' \
/etc/onlyoffice/documentserver/local.json 2>/dev/null || true)
if [[ -n "${DETECTED}" ]]; then
OO_SECRET="${DETECTED}"
echo "${OO_SECRET}" > "${OO_SECRET_FILE}"
ok "既存コンテナからシークレットを取得しました"
fi
fi
fi
# ── nextExplorer docker-compose.yml 生成 ──────────────────────────────────────
info "nextExplorer の docker-compose.yml を生成..."
VOLUMES_YAML=" - ${DATA_DIR}/config:/config"$'\n'
VOLUMES_YAML+=" - ${DATA_DIR}/cache:/cache"
for entry in "${MOUNT_ENTRIES[@]}"; do
VOLUMES_YAML+=$'\n'" - ${entry%%:*}:/mnt/${entry##*:}"
done
# OnlyOffice 環境変数ブロック(インストールした or 既存コンテナがある場合に追加)
if [[ "${install_oo}" =~ ^[Yy] ]] || docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^onlyoffice$"; then
OO_ENV_BLOCK=" ONLYOFFICE_URL: \"${OO_PUBLIC_URL}\"
ONLYOFFICE_SECRET: \"${OO_SECRET}\"
ONLYOFFICE_LANG: \"ja\""
else
OO_ENV_BLOCK=" # ONLYOFFICE_URL: \"https://${TS_HOSTNAME}:3322\"
# ONLYOFFICE_SECRET: \"your-secret-here\"
# ONLYOFFICE_LANG: \"ja\""
fi
cat > "${INSTALL_DIR}/docker-compose.yml" <<EOF
# nextExplorer — docker-compose.yml
# 生成日: $(date '+%Y-%m-%d %H:%M:%S')
# アクセス URL: ${PUBLIC_URL}
services:
nextexplorer:
image: nxzai/explorer:latest
container_name: nextexplorer
restart: unless-stopped
ports:
- "127.0.0.1:${HOST_PORT}:${CONTAINER_PORT}"
environment:
NODE_ENV: production
PORT: "${CONTAINER_PORT}"
PUBLIC_URL: "${PUBLIC_URL}"
TRUST_PROXY: "loopback"
SESSION_SECRET: "${SESSION_SECRET}"
PUID: "${PUID}"
PGID: "${PGID}"
${OO_ENV_BLOCK}
volumes:
${VOLUMES_YAML}
EOF
ok "docker-compose.yml → ${INSTALL_DIR}/docker-compose.yml"
# ── nextExplorer 起動 ─────────────────────────────────────────────────────────
info "nextExplorer コンテナを起動..."
docker compose -f "${INSTALL_DIR}/docker-compose.yml" pull
docker compose -f "${INSTALL_DIR}/docker-compose.yml" up -d
ok "nextExplorer コンテナ起動完了"
# ── nextExplorer の Tailscale Serve 設定 ──────────────────────────────────────
EXISTING_SERVE=$(tailscale serve status 2>/dev/null || true)
if echo "${EXISTING_SERVE}" | grep -q ":${HOST_PORT}"; then
warn "ポート ${HOST_PORT} はすでに Tailscale Serve に登録されています。スキップします。"
else
info "Tailscale Serve にポート ${HOST_PORT} を追加..."
tailscale serve --bg --https="${HOST_PORT}" "http://127.0.0.1:${HOST_PORT}"
ok "nextExplorer の Tailscale Serve 設定追加完了"
fi
# ── 完了サマリー ──────────────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
ok "セットアップ完了!"
echo ""
echo " nextExplorer : ${PUBLIC_URL}"
if [[ "${install_oo}" =~ ^[Yy] ]]; then
echo " OnlyOffice : ${OO_PUBLIC_URL}"
echo ""
fi
echo ""
echo " 設定ファイル : ${INSTALL_DIR}/docker-compose.yml"
echo " データ領域 : ${DATA_DIR}/"
echo ""
echo " マウント済みボリューム:"
for entry in "${MOUNT_ENTRIES[@]}"; do
echo " ${entry%%:*} → UI ラベル「${entry##*:}」"
done
echo ""
echo " 現在の Tailscale Serve 設定:"
tailscale serve status
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
書き込み権限を変更
マウントソースは他サービスのファイルが混在する共有領域なので、スクリプトからは一切触らない設計にしています。アップロードや書き込みに失敗する場合は対象サブディレクトリだけ手動で権限を変更します。
# 書き込みたいディレクトリだけピンポイントで
sudo setfacl -R -m u:1000:rwX /opt/lxd-data/対象フォルダ
sudo setfacl -R -d -m u:1000:rwX /opt/lxd-data/対象フォルダ
空のOfficeファイルも新規作成可能
これで、nextExplorer上でOfficeファイルをダブルクリックすれば、OnlyOfficeで開いて編集できるはずです。初回は読み込みに時間がかかり、初回は表示中に一度接続が切れるかもしれませんが、2回目以降はスムーズに表示されるはず。
nextExplorer上でファイルを新規作成する場合は、デフォルトではtxtファイルになりますが、拡張子を変えることで、空のmdファイルやxlsxファイル、docxファイルなども作成出来るのは地味に便利ですね。

Nextcloudでメール機能やノート機能などを利用していないなら、これで置き換えが出来そうです。
OnlyOfficeにフォントを追加
OnlyOfficeにフォントを追加します。/opt/lxd-data/Fonts にフォントファイルを置いておいてください。スクリプトを実行すれば、このフォントファイルをセットアップ済みのOnlyOfficeコンテナの/usr/share/fonts/customにマウントして、コンテナを再作成しフォントを再生成します。
#!/usr/bin/env bash
# =============================================================================
# OnlyOffice フォント追加スクリプト
# - /opt/lxd-data/Fonts/ を /usr/share/fonts/custom にマウント追加
# - docker-compose.yml を更新してコンテナ再作成
# - OnlyOffice 内のフォントキャッシュを再生成
# =============================================================================
set -euo pipefail
OO_INSTALL_DIR="/opt/onlyoffice"
FONT_SRC="/opt/lxd-data/Fonts"
FONT_DST="/usr/share/fonts/custom"
COMPOSE_FILE="${OO_INSTALL_DIR}/docker-compose.yml"
# ── 色付きログ ─────────────────────────────────────────────────────────────────
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
ok() { echo -e "\033[1;32m[ OK ]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
die() { echo -e "\033[1;31m[ERR ]\033[0m $*" >&2; exit 1; }
# ── 前提チェック ───────────────────────────────────────────────────────────────
info "前提確認..."
[[ -f "${COMPOSE_FILE}" ]] || die "compose ファイルが見つかりません: ${COMPOSE_FILE}"
[[ -d "${FONT_SRC}" ]] || die "フォントディレクトリが見つかりません: ${FONT_SRC}"
FONT_COUNT=$(find "${FONT_SRC}" -maxdepth 2 -type f \( \
-iname "*.ttf" -o -iname "*.otf" -o -iname "*.woff" -o -iname "*.woff2" \
\) | wc -l)
[[ "${FONT_COUNT}" -gt 0 ]] || die "フォントファイル(ttf/otf/woff)が ${FONT_SRC} に見つかりません"
ok "フォントファイル ${FONT_COUNT} 件を確認"
docker compose version >/dev/null 2>&1 || die "docker compose (v2) が見つかりません"
docker ps --format '{{.Names}}' | grep -q "^onlyoffice$" \
|| die "onlyoffice コンテナが起動していません(docker ps で確認してください)"
ok "前提 OK"
# ── compose.yml にマウントが既に存在するか確認 ─────────────────────────────────
if grep -q "${FONT_DST}" "${COMPOSE_FILE}"; then
warn "すでに ${FONT_DST} のマウントが登録されています。"
warn "フォントキャッシュの再生成のみ行います。"
SKIP_COMPOSE_EDIT=true
else
SKIP_COMPOSE_EDIT=false
fi
# ── docker-compose.yml にマウント行を追加 ──────────────────────────────────────
if [[ "${SKIP_COMPOSE_EDIT}" == false ]]; then
info "docker-compose.yml にフォントマウントを追加..."
# volumes: ブロックの末尾に追記(既存の最後のボリューム行の直後に挿入)
# sedで "volumes:" セクション内の最後のエントリの後に追加
python3 - "${COMPOSE_FILE}" "${FONT_SRC}" "${FONT_DST}" <<'PYEOF'
import sys, re
compose_path = sys.argv[1]
font_src = sys.argv[2]
font_dst = sys.argv[3]
new_line = f" - {font_src}:{font_dst}:ro"
with open(compose_path) as f:
content = f.read()
# volumes: ブロックを探して末尾に追記
# " volumes:" の後に続く " - ..." 行群の最後を特定
pattern = r'( volumes:(?:\n - [^\n]+)+)'
def append_volume(m):
return m.group(0) + "\n" + new_line
new_content, n = re.subn(pattern, append_volume, content)
if n == 0:
# volumes: セクションが無い場合は作って追加
new_content = content.rstrip() + f"\n volumes:\n{new_line}\n"
with open(compose_path, "w") as f:
f.write(new_content)
print("OK")
PYEOF
ok "docker-compose.yml を更新しました"
info "追加内容: ${FONT_SRC} → ${FONT_DST} (read-only)"
fi
# ── バックアップ ───────────────────────────────────────────────────────────────
BACKUP="${COMPOSE_FILE}.bak.$(date +%Y%m%d_%H%M%S)"
cp "${COMPOSE_FILE}" "${BACKUP}"
info "compose ファイルのバックアップ: ${BACKUP}"
# ── コンテナ再作成 ─────────────────────────────────────────────────────────────
info "コンテナを再作成します(データは保持されます)..."
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate
ok "コンテナ再作成完了"
# ── OnlyOffice 起動待ち ────────────────────────────────────────────────────────
info "OnlyOffice の起動を待機中..."
for i in $(seq 1 30); do
if docker exec onlyoffice supervisorctl status ds:docservice 2>/dev/null \
| grep -q "RUNNING"; then
ok "OnlyOffice 起動確認(${i}秒)"
break
fi
if [[ "${i}" -eq 30 ]]; then
warn "30秒待機しましたが起動確認できませんでした。フォント生成を続行します。"
fi
sleep 1
done
# ── フォントキャッシュ再生成 ───────────────────────────────────────────────────
info "コンテナ内でフォントキャッシュを再生成します..."
docker exec onlyoffice bash -c "
set -e
echo '[1/4] fc-cache を実行...'
fc-cache -f -v /usr/share/fonts/custom 2>&1 | tail -5
echo '[2/4] AllFonts.js を生成...'
cd /var/www/onlyoffice/documentserver
node core/DocService/sources/AllFontsGen.js \
--fonts-dir=/usr/share/fonts \
--out=/var/www/onlyoffice/documentserver/core-fonts 2>&1 | tail -5 || \
node tools/fontgen/allfonts.js 2>&1 | tail -5 || \
/usr/bin/documentserver-generate-allfonts.sh 2>&1 | tail -5 || \
true
echo '[3/4] プレゼンテーションテーマを再生成...'
/usr/bin/documentserver-generate-all-themes.sh 2>&1 | tail -3 || true
echo '[4/4] JS キャッシュを再生成...'
/usr/bin/documentserver-pluginsmanager.sh 2>&1 | tail -3 || true
"
# ── nginx リロード ─────────────────────────────────────────────────────────────
info "nginx をリロード..."
docker exec onlyoffice nginx -s reload 2>/dev/null || true
# ── フォント認識確認 ───────────────────────────────────────────────────────────
echo ""
info "認識されたカスタムフォント一覧:"
docker exec onlyoffice fc-list /usr/share/fonts/custom 2>/dev/null \
| awk -F: '{print " ", $2}' | sort -u \
|| warn "fc-list での確認に失敗しました(フォント自体は追加されています)"
# ── 完了 ───────────────────────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
ok "フォント追加完了!"
echo ""
echo " フォントソース : ${FONT_SRC} (${FONT_COUNT} ファイル)"
echo " コンテナ内パス : ${FONT_DST} (read-only マウント)"
echo " compose ファイル: ${COMPOSE_FILE}"
echo " バックアップ : ${BACKUP}"
echo ""
echo " ブラウザキャッシュをクリアしてから OnlyOffice でフォントを確認してください。"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
なお、フォントを追加・変更した場合は同じスクリプトを再実行すれば再生成されます(マウント行の二重追加はスキップされます)。
マウントするディレクトリを追加
#!/usr/bin/env bash
# =============================================================================
# nextExplorer Location 追加スクリプト
# - 現在のマウント設定を表示
# - 追加するマウントソース・ラベルを対話入力
# - docker-compose.yml を更新してコンテナ再起動
# =============================================================================
set -euo pipefail
INSTALL_DIR="/opt/nextexplorer"
COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml"
# ── 色付きログ ─────────────────────────────────────────────────────────────────
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
ok() { echo -e "\033[1;32m[ OK ]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
die() { echo -e "\033[1;31m[ERR ]\033[0m $*" >&2; exit 1; }
# ── 前提チェック ───────────────────────────────────────────────────────────────
[[ -f "${COMPOSE_FILE}" ]] || die "compose ファイルが見つかりません: ${COMPOSE_FILE}"
docker compose version >/dev/null 2>&1 || die "docker compose (v2) が見つかりません"
# ── 現在の Location 表示 ───────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 現在の Location(/mnt/ マウント):"
echo ""
grep -E "^\s+- .+:/mnt/" "${COMPOSE_FILE}" | while read -r line; do
src=$(echo "${line}" | sed 's|.*- \(.*\):/mnt/.*|\1|')
label=$(echo "${line}" | sed 's|.*/mnt/\(.*\)|\1|')
echo " ${src} → UI ラベル「${label}」"
done
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# ── 追加するマウントの入力 ─────────────────────────────────────────────────────
declare -a NEW_ENTRIES=()
mount_index=1
while true; do
read -rp " 追加 #${mount_index} ホストパス(空 Enter で終了): " input_path
[[ -z "${input_path}" ]] && break
# 既存マウントの重複チェック
if grep -qF "${input_path}:/mnt/" "${COMPOSE_FILE}"; then
warn "${input_path} はすでに登録されています。スキップします。"
continue
fi
# パスの存在確認
if [[ ! -d "${input_path}" ]]; then
read -rp " ${input_path} は存在しません。作成しますか? [y/N]: " yn
if [[ "${yn}" =~ ^[Yy]$ ]]; then
mkdir -p "${input_path}"
ok "${input_path} を作成しました"
else
warn "スキップします"
continue
fi
fi
default_label=$(basename "${input_path}")
read -rp " 追加 #${mount_index} UI ラベル名(空 Enter で「${default_label}」): " input_label
[[ -z "${input_label}" ]] && input_label="${default_label}"
# ラベルの重複チェック
if grep -qF "/mnt/${input_label}" "${COMPOSE_FILE}"; then
warn "ラベル「${input_label}」はすでに使われています。別の名前を入力してください。"
continue
fi
NEW_ENTRIES+=("${input_path}:${input_label}")
ok "追加予定: ${input_path} → UI ラベル「${input_label}」"
(( mount_index++ ))
done
# ── 入力がなければ終了 ─────────────────────────────────────────────────────────
if [[ ${#NEW_ENTRIES[@]} -eq 0 ]]; then
warn "追加エントリがありません。終了します。"
exit 0
fi
# ── 確認 ───────────────────────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 以下を追加します:"
for entry in "${NEW_ENTRIES[@]}"; do
echo " ${entry%%:*} → UI ラベル「${entry##*:}」"
done
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
read -rp " 続行しますか? [Y/n]: " confirm
confirm="${confirm:-Y}"
[[ "${confirm}" =~ ^[Yy] ]] || { warn "キャンセルしました。"; exit 0; }
# ── バックアップ ───────────────────────────────────────────────────────────────
BACKUP="${COMPOSE_FILE}.bak.$(date +%Y%m%d_%H%M%S)"
cp "${COMPOSE_FILE}" "${BACKUP}"
info "バックアップ: ${BACKUP}"
# ── compose.yml に追記(Python3 で安全にパース) ───────────────────────────────
python3 - "${COMPOSE_FILE}" "${NEW_ENTRIES[@]}" << 'PYEOF'
import sys, re
compose_path = sys.argv[1]
entries = sys.argv[2:] # "src:label" の配列
with open(compose_path) as f:
content = f.read()
new_lines = "\n".join(f" - {e.split(':')[0]}:/mnt/{e.split(':')[1]}" for e in entries)
# volumes: ブロック末尾に追記
pattern = r'( volumes:(?:\n - [^\n]+)+)'
def append_volume(m):
return m.group(0) + "\n" + new_lines
new_content, n = re.subn(pattern, append_volume, content)
if n == 0:
die("volumes: セクションが見つかりませんでした")
with open(compose_path, "w") as f:
f.write(new_content)
print("OK")
PYEOF
ok "docker-compose.yml を更新しました"
# ── コンテナ再起動 ─────────────────────────────────────────────────────────────
info "コンテナを再起動します..."
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate
ok "コンテナ再起動完了"
# ── 完了:更新後の Location 一覧を表示 ────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
ok "Location 追加完了!"
echo ""
echo " 更新後の Location 一覧:"
grep -E "^\s+- .+:/mnt/" "${COMPOSE_FILE}" | while read -r line; do
src=$(echo "${line}" | sed 's|.*- \(.*\):/mnt/.*|\1|')
label=$(echo "${line}" | sed 's|.*/mnt/\(.*\)|\1|')
echo " ${src} → UI ラベル「${label}」"
done
echo ""
echo " バックアップ: ${BACKUP}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
アップデート
nextExplorer
docker compose -f /opt/nextexplorer/docker-compose.yml pull
docker compose -f /opt/nextexplorer/docker-compose.yml up -d
OnlyOffice
docker compose -f /opt/onlyoffice/docker-compose.yml pull
docker compose -f /opt/onlyoffice/docker-compose.yml up -d
どちらも pull で最新イメージを取得し、up -d でコンテナを入れ替えます。データ・設定・シークレットはすべてボリューム側に残るので消えません。
アンインストール
nextExplorer
# コンテナ停止・削除
docker compose -f /opt/nextexplorer/docker-compose.yml down
# Tailscale Serve から削除
tailscale serve --https=3317 off
# ファイル削除(データも消す場合)
rm -rf /opt/nextexplorer
rm -rf /srv/nextexplorer # config / cache
# マウントしたディレクトリ(/opt/lxd-data など)は手動で判断してください
# イメージも消す場合
docker rmi nxzai/explorer
OnlyOffice
# コンテナ停止・削除
docker compose -f /opt/onlyoffice/docker-compose.yml down
# Tailscale Serve から削除
tailscale serve --https=3322 off
# ファイル削除(データも消す場合)
rm -rf /opt/onlyoffice # logs / data / lib / db / compose.yml
# イメージも消す場合
docker rmi onlyoffice/documentserver
注意点:
| nextExplorer | OnlyOffice | |
|---|---|---|
| ユーザーDB | /srv/nextexplorer/config/ | /opt/onlyoffice/db/ |
| アップロードファイル | マウント先(/opt/lxd-data等) | /opt/onlyoffice/data/ |
| シークレット | /opt/nextexplorer/.session_secret/opt/nextexplorer/.onlyoffice_secret | compose.yml の JWT_SECRET |
アンインストール後に再インストールする場合は .session_secret と .onlyoffice_secret を残しておくと、セットアップスクリプトが自動で再利用してシークレットが変わりません。


