複数のサービスを入れているとごちゃごちゃしてきたので、ここで一度整理します。
ベースは出来れば稼働サービスの少ないUbuntu Serverにし、VMやLXD、Dockerで管理するのが環境を綺麗に保てそうです。VMはメモリをガッツリ確保しますが、LXDやDockerは動的にリソースを使うので、、「GUIが必要なOSはVM」「ミドルウェアはLXD」「アプリはDocker」と分けるのが良さそうですね。
LXD-UIはVMも扱えますが、VM操作ならCockpitのほうが多機能で分かりやすいので、それぞれ次のように使い分けるのが扱いやすそうでしょうか。
| ツール名 | 主な管理対象 | 強み・特徴 | 主なメリット |
| Cockpit | VM (KVM/QEMU) / システム全体 | 仮想マシンの作成、ISOマウント、コンソール操作が非常に直感的。 | リソース監視も可能で、サーバー全体の「総合ダッシュボード」に最適。 |
| LXD-UI | LXD / LXC (コンテナ) | Canonical公式に近い操作感。軽量なLinuxコンテナの素早い展開に特化。 | VMより低リソース。隔離されたCUI実験環境などの構築に最適。 |
| Dockhand | Docker | Docker特化型。Composeスタックの管理やイメージ更新が容易。 | Portainerよりも軽量な選択肢。Docker管理に特化したい場合に好相性。 |
Docker関連の設定ファイルも、ホームフォルダに配置すると扱いやすい反面、ごちゃごちゃしてしまうので、すべて/opt/docker/以下にフォルダで分けて配置することにします。そうすればバックアップも行いやすくなるはず。
/opt/docker/
├── web-app/ # アプリA(例: Next.js / Rails など)
│ ├── compose.yaml # Docker Compose 定義
│ └── data/ # ホスト側マウント用永続データ
│ ├── postgres/ # 例
│ └── uploads/
├── database/ # アプリB(例: PostgreSQL / MySQL)
│ ├── compose.yaml
│ └── conf/ # カスタム設定ファイル置き場
│ └── postgresql.conf
└── proxy/ # アプリC(リバースプロキシ)
├── compose.yaml
└── nginx.conf # または conf.d/*.conf
あとはUbuntu Serverセットアップ直後のイメージで、各サービスをセットアップしていきます。
Tailscale
sudo apt update && sudo apt upgrade -y
sudo apt install curl
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
SSH
sudo apt install -y openssh-server
Cockpit
sudo apt install -y cockpit
# Ubuntu25.10で管理者権限変更出来るように
sudo update-alternatives --set sudo /usr/bin/sudo.ws
# 仮想マシンプラグイン
sudo apt install -y cockpit-machines
# ファイラーNavigaterプラグイン
sudo apt install -y git
git clone https://github.com/45Drives/cockpit-navigator.git
cd cockpit-navigator
git checkout v0.5.8
sudo apt install -y make
sudo make install
cd ..
rm -rf cockpit-navigator
# SambaをCockpitで
sudo apt install -y samba
wget https://github.com/45Drives/cockpit-file-sharing/releases/download/v4.5.3-4/cockpit-file-sharing_4.5.3-4jammy_all.deb
sudo apt install -y ./cockpit-file-sharing_4.5.3-4jammy_all.deb
rm cockpit-file-sharing_4.5.3-4jammy_all.deb
echo "完了!http://localhost:9090 にアクセスしてください"
LXD-UI
# LXDインストール
sudo snap install lxd --channel=latest/stable
# 初期化
sudo lxd init --minimal
# HTTPS APIを有効化
sudo lxc config set core.https_address :8443
# UI有効化
sudo snap set lxd ui.enable=true
sudo systemctl reload snap.lxd.daemon
# ユーザーをlxdグループに追加
sudo usermod -aG lxd $USER
echo "完了!ログアウト・再ログイン後に https://localhost:8443 にアクセスしてください"
Docker
# 必要パッケージのインストール
sudo apt update
sudo apt install -y ca-certificates gnupg
# GPGキー
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# aptリポジトリの追加(Ubuntu 25.x は noble を明示指定)
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
noble stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
# Docker Engineや関連ツールのインストール
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 起動確認
sudo systemctl status docker --no-pager
# Docker関連用ディレクトリを作成
sudo mkdir -p /opt/docker
# 所有者を「自分」に、グループを「docker」に変更
sudo chown -R $USER:docker /opt/docker
# 権限を設定(自分とグループが読み書きできるようにする)
sudo chmod -R 775 /opt/docker
# SGIDを設定(新しく作る子ディレクトリのグループを強制的に「docker」にする)
sudo chmod -R g+s /opt/docker
# 一般ユーザーへ権限追加
sudo usermod -aG docker $USER
# 動作確認(newgrp でグループを反映した上で実行)
echo "ログアウト・再ログイン後に以下を実行してください:"
echo " docker run --rm hello-world"
Immich
# ディレクトリ作成
sudo mkdir -p /opt/docker/immich
sudo chown -R $USER:docker /opt/docker/immich
sudo chmod -R 775 /opt/docker/immich
cd /opt/docker/immich
# docker-compose.yml 作成
cat > docker-compose.yml << 'EOF'
name: immich
services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
volumes:
- ${UPLOAD_LOCATION}:/data
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- '2283:2283'
depends_on:
- redis
- database
restart: always
healthcheck:
disable: false
immich-machine-learning:
container_name: immich_machine_learning
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
volumes:
- model-cache:/cache
env_file:
- .env
restart: always
healthcheck:
disable: false
redis:
container_name: immich_redis
image: docker.io/redis:6.2-alpine
healthcheck:
test: redis-cli ping || exit 1
restart: always
database:
container_name: immich_postgres
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
restart: always
volumes:
model-cache:
EOF
# .env 作成
cat > .env << 'EOF'
UPLOAD_LOCATION=./library
DB_DATA_LOCATION=./postgres
IMMICH_VERSION=release
DB_PASSWORD=postgres
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
EOF
# コンテナ起動
docker compose up -d
echo "完了!http://localhost:2283 にアクセスしてセットアップしてください"
Trilium Notes
mkdir -p /opt/docker/trilium && cat > /opt/docker/trilium/docker-compose.yml << 'EOF'
services:
trilium:
image: triliumnext/trilium:latest
container_name: trilium
restart: unless-stopped
ports:
- "7070:8080"
volumes:
- /opt/docker/trilium/trilium-data:/home/node/trilium-data
EOF
cd /opt/docker/trilium && docker compose up -d
echo "完了!http://localhost:7070 にアクセスしてセットアップしてください"
Nextcloud & Onlyoffice
#!/bin/bash
set -e
# === ホスト名を入力 ===
read -p "ホスト名またはIPアドレスを入力してください: " HOST_NAME
# === JWT シークレット(変更推奨) ===
JWT_SECRET="my_stable_secret_2026"
# === 共有ネットワーク作成(既存なら再利用) ===
docker network inspect onlyoffice_net >/dev/null 2>&1 \
|| docker network create onlyoffice_net
echo "[1/2] ネットワーク onlyoffice_net を確認しました"
# ============================================================
# Nextcloud
# ============================================================
sudo mkdir -p /opt/docker/nextcloud/{data,db}
sudo chown -R $USER:$USER /opt/docker/nextcloud
cd /opt/docker/nextcloud
# container_name を固定 → OnlyOffice から "nextcloud-app" で名前解決できる
cat > docker-compose.yaml << EOF
services:
db:
image: mariadb:10.6
container_name: nextcloud-db
restart: always
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
volumes:
- ./db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_PASSWORD: nextcloudpass
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
networks:
- default
app:
image: nextcloud:latest
container_name: nextcloud-app
restart: always
ports:
- "8080:80"
depends_on:
- db
volumes:
- ./data:/var/www/html
environment:
MYSQL_PASSWORD: nextcloudpass
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_HOST: nextcloud-db
NEXTCLOUD_TRUSTED_DOMAINS: "${HOST_NAME}:8080 nextcloud-app localhost"
OVERWRITEHOST: "${HOST_NAME}:8080"
OVERWRITEPROTOCOL: http
networks:
- default
- onlyoffice_net
networks:
default:
onlyoffice_net:
external: true
EOF
docker compose up -d
echo "[1/2] Nextcloud 起動完了"
# ============================================================
# OnlyOffice
# ============================================================
sudo mkdir -p /opt/docker/onlyoffice/{logs,data,lib,db}
sudo chown -R $USER:$USER /opt/docker/onlyoffice
cd /opt/docker/onlyoffice
# .env に JWT_SECRET を書き出す
# → ヒアドキュメントを 'EOF' にしても変数が届かない問題を回避
cat > .env << EOF
JWT_SECRET=${JWT_SECRET}
EOF
# compose 本体はシングルクォートヒアドキュメント(変数展開なし)
cat > docker-compose.yaml << 'EOF'
services:
onlyoffice-docs:
image: onlyoffice/documentserver:latest
container_name: onlyoffice-docs
restart: always
stdin_open: true
tty: true
ports:
- "9000:80"
environment:
JWT_ENABLED: "true"
JWT_SECRET: "${JWT_SECRET}" # .env から自動読み込み
volumes:
- /opt/docker/onlyoffice/logs:/var/log/onlyoffice
- /opt/docker/onlyoffice/data:/var/www/onlyoffice/Data
- /opt/docker/onlyoffice/lib:/var/lib/onlyoffice
- /opt/docker/onlyoffice/db:/var/lib/postgresql
networks:
- onlyoffice_net
networks:
onlyoffice_net:
external: true
EOF
docker compose up -d
echo "[2/2] OnlyOffice 起動完了"
# ============================================================
# 完了メッセージ
# ============================================================
echo ""
echo "======================================"
echo " 起動完了"
echo "======================================"
echo ""
echo " Nextcloud : http://${HOST_NAME}:8080"
echo " OnlyOffice : http://${HOST_NAME}:9000"
echo ""
echo "======================================"
echo " Nextcloud 管理画面 → ONLYOFFICE 設定"
echo "======================================"
echo ""
echo " ドキュメントサービスのアドレス(ブラウザ用):"
echo " http://${HOST_NAME}:9000"
echo ""
echo " 認証ヘッダー:(空白のまま)"
echo ""
echo " JWT シークレット: ${JWT_SECRET}"
echo ""
echo " ドキュメントサービスの内部アドレス(サーバー間):"
echo " http://onlyoffice-docs"
echo ""
echo " Nextcloud サーバーの内部アドレス(コールバック用):"
echo " http://nextcloud-app"
echo ""
echo "======================================"
echo " 個別アップデート手順"
echo "======================================"
echo ""
echo " Nextcloud のみ更新:"
echo " cd /opt/docker/nextcloud && docker compose pull && docker compose up -d"
echo ""
echo " OnlyOffice のみ更新:"
echo " cd /opt/docker/onlyoffice && docker compose pull && docker compose up -d"
echo ""
FleshRSS
mkdir -p /opt/docker/freshrss && cat > /opt/docker/docker-compose.yml << 'EOF'
version: '3'
services:
freshrss:
image: freshrss/freshrss:latest
container_name: freshrss
restart: unless-stopped
ports:
- "6060:80"
volumes:
- freshrss_data:/var/www/FreshRSS/data
- freshrss_extensions:/var/www/FreshRSS/extensions
environment:
TZ: Asia/Tokyo
CRON_MIN: '*/15'
volumes:
freshrss_data:
freshrss_extensions:
EOF
cd /opt/docker/freshrss && docker compose up -d && sleep 10 && \
sudo apt install -y unzip && \
curl -L "https://framagit.org/nicofrand/xextension-threepanesview/-/archive/master/xextension-threepanesview-master.zip" -o tpv.zip && \
unzip tpv.zip && \
docker cp xextension-threepanesview-master freshrss:/var/www/FreshRSS/extensions/xExtension-ThreePanesView && \
rm -rf tpv.zip xextension-threepanesview-master && \
curl -L https://github.com/Niehztog/freshrss-af-readability/archive/refs/heads/master.zip -o af.zip && \
unzip af.zip && \
docker cp freshrss-af-readability-master freshrss:/var/www/FreshRSS/extensions/xExtension-af_readability && \
rm -rf af.zip freshrss-af-readability-master && \
echo "完了!http://localhost:6060 にアクセスしてセットアップしてください"
CSS
#stream {
width: 280px !important;
min-width: 280px !important;
}
#aside_feed {
width: 200px !important;
min-width: 200px !important;
}
#current-article {
flex: 1 !important;
width: auto !important;
}
#stream .flux .flux_header .item .date,
#stream .flux .flux_header .date {
display: none !important;
}
#stream .flux .flux_header .item .title,
#stream .flux .flux_header .item .title:has(~ .date) {
position: relative !important;
padding-right: 0 !important;
max-width: 100% !important;
width: 100% !important;
}
#stream .flux .websitename .item.website,
#stream .flux .item.website {
width: 32px !important;
min-width: 32px !important;
max-width: 32px !important;
overflow: hidden !important;
padding: 0 !important;
}
#stream .flux .flux_header .item.website .item-element {
padding: 0 6px !important;
}
#stream .flux .flux_header .websiteName {
display: none !important;
}
#stream .flux .flux_header .item.titleAuthorSummaryDate {
width: auto !important;
max-width: 100% !important;
}
Dockhand
# ディレクトリ作成&yml編集
mkdir -p /opt/docker/dockhand && cat > /opt/docker/dockhand/docker-compose.yml << 'EOF'
services:
dockhand:
image: fnsys/dockhand:latest
container_name: dockhand
restart: unless-stopped
ports:
- 3000:3000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- dockhand_data:/app/data
volumes:
dockhand_data:
EOF
# 起動
cd /opt/docker/dockhand && docker compose up -d
echo "完了!http://localhost:3000 にアクセスしてセットアップしてください"
Nginx Proxy Manager (NPM)
mkdir -p /opt/docker/npm && cat > /opt/docker/npm/docker-compose.yml << 'EOF'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '443:443'
- '81:81'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
EOF
cd /opt/docker/npm/ && docker compose up -d
ポータルHTML生成
多数のサービスを入れているとアクセスが分散するので、ポータルのようなHTMLも作成してみました。
パスワードはソースで見えるので気休め程度です。あくまでもLAN内やTailscale内限定のホームサーバという位置付けで。もし外部に公開する場合は修正が必要です。
sudo mkdir -p /opt/docker/npm/data/html
sudo nano /opt/docker/npm/data/html/index.html
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Server Portal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Noto+Sans+JP:wght@300;400;500&display=swap" rel="stylesheet">
<style>
/* =====================================================
CSS変数 - ここを編集してテーマを変更
===================================================== */
:root {
--bg: #0d0f14;
--surface: #151820;
--border: #252a35;
--accent: #4af0a0;
--accent2: #4ab4f0;
--text: #c8d0e0;
--text-dim: #5a6278;
--text-head: #e8edf8;
--danger: #f04a6a;
--font-mono: 'Space Mono', monospace;
--font-body: 'Noto Sans JP', sans-serif;
--radius: 6px;
--card-w: 280px;
}
/* =====================================================
リセット&ベース
===================================================== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
min-height: 100vh;
overflow-x: hidden;
}
/* =====================================================
ロック画面
===================================================== */
#lockscreen {
position: fixed; inset: 0;
background: var(--bg);
display: flex; align-items: center; justify-content: center;
z-index: 9999;
transition: opacity .5s, visibility .5s;
}
#lockscreen.hidden { opacity: 0; visibility: hidden; pointer-events: none; }
.lock-box {
border: 1px solid var(--border);
background: var(--surface);
padding: 3rem 2.5rem;
width: min(400px, 90vw);
position: relative;
overflow: hidden;
}
.lock-box::before {
content: '';
position: absolute; top: 0; left: 0; right: 0; height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
}
.lock-title {
font-family: var(--font-mono);
font-size: .75rem;
letter-spacing: .2em;
color: var(--accent);
text-transform: uppercase;
margin-bottom: 1.5rem;
}
.lock-sub {
font-size: .85rem;
color: var(--text-dim);
margin-bottom: 2rem;
line-height: 1.6;
}
.lock-input-wrap { position: relative; margin-bottom: 1rem; }
.lock-input-wrap input {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text-head);
font-family: var(--font-mono);
font-size: 1rem;
padding: .75rem 1rem;
outline: none;
transition: border-color .2s;
border-radius: var(--radius);
letter-spacing: .1em;
}
.lock-input-wrap input:focus { border-color: var(--accent); }
.lock-btn {
width: 100%;
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
font-family: var(--font-mono);
font-size: .85rem;
letter-spacing: .15em;
padding: .75rem;
cursor: pointer;
text-transform: uppercase;
transition: background .2s, color .2s;
border-radius: var(--radius);
}
.lock-btn:hover { background: var(--accent); color: var(--bg); }
.lock-err {
color: var(--danger);
font-size: .8rem;
font-family: var(--font-mono);
margin-top: .75rem;
min-height: 1.2em;
}
/* =====================================================
グリッド背景
===================================================== */
body::before {
content: '';
position: fixed; inset: 0;
background-image:
linear-gradient(var(--border) 1px, transparent 1px),
linear-gradient(90deg, var(--border) 1px, transparent 1px);
background-size: 40px 40px;
opacity: .3;
pointer-events: none;
z-index: 0;
}
/* =====================================================
ヘッダー
===================================================== */
header {
position: relative; z-index: 1;
padding: 2.5rem 2rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex; align-items: flex-end; justify-content: space-between;
flex-wrap: wrap; gap: 1rem;
background: linear-gradient(180deg, rgba(74,240,160,.04) 0%, transparent 100%);
}
.header-left {}
.logo-line {
display: flex; align-items: center; gap: .75rem;
margin-bottom: .4rem;
}
.logo-dot {
width: 8px; height: 8px;
background: var(--accent);
border-radius: 50%;
box-shadow: 0 0 12px var(--accent);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .4; }
}
h1 {
font-family: var(--font-mono);
font-size: 1.5rem;
color: var(--text-head);
letter-spacing: .05em;
}
.header-host {
font-family: var(--font-mono);
font-size: .8rem;
color: var(--text-dim);
letter-spacing: .1em;
}
.header-host span { color: var(--accent2); }
.header-right {
display: flex; align-items: center; gap: 1rem;
}
.header-time {
font-family: var(--font-mono);
font-size: .75rem;
color: var(--text-dim);
text-align: right;
line-height: 1.6;
}
.header-time .time { color: var(--text-head); font-size: 1.1rem; }
/* =====================================================
セクション
===================================================== */
main {
position: relative; z-index: 1;
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
.section { margin-bottom: 2.5rem; }
.section-label {
font-family: var(--font-mono);
font-size: .7rem;
letter-spacing: .25em;
color: var(--text-dim);
text-transform: uppercase;
margin-bottom: 1rem;
display: flex; align-items: center; gap: .75rem;
}
.section-label::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
/* =====================================================
カードグリッド
===================================================== */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--card-w), 1fr));
gap: 1rem;
}
/* =====================================================
カード
===================================================== */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
text-decoration: none;
display: block;
transition: border-color .2s, transform .2s, box-shadow .2s;
position: relative;
cursor: pointer;
}
.card::before {
content: '';
position: absolute; top: 0; left: 0; right: 0; height: 2px;
background: var(--card-accent, var(--accent));
opacity: 0;
transition: opacity .2s;
}
.card:hover {
border-color: var(--card-accent, var(--accent));
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,.4);
}
.card:hover::before { opacity: 1; }
.card-inner { padding: 1.25rem 1.25rem 1rem; }
.card-header {
display: flex; align-items: center; gap: .75rem;
margin-bottom: .75rem;
}
.card-icon {
width: 36px; height: 36px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 1.1rem;
background: rgba(255,255,255,.04);
border: 1px solid var(--border);
flex-shrink: 0;
}
.card-name {
font-weight: 500;
color: var(--text-head);
font-size: .95rem;
}
.card-badge {
margin-left: auto;
font-family: var(--font-mono);
font-size: .6rem;
padding: .2em .5em;
border-radius: 3px;
letter-spacing: .05em;
background: rgba(255,255,255,.05);
color: var(--text-dim);
border: 1px solid var(--border);
flex-shrink: 0;
}
.card-desc {
font-size: .8rem;
color: var(--text-dim);
line-height: 1.5;
margin-bottom: .75rem;
}
.card-url {
font-family: var(--font-mono);
font-size: .7rem;
color: var(--card-accent, var(--accent));
opacity: .7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
padding-top: .75rem;
border-top: 1px solid var(--border);
}
/* カラーバリエーション */
.c-green { --card-accent: #4af0a0; }
.c-blue { --card-accent: #4ab4f0; }
.c-purple { --card-accent: #a07af0; }
.c-orange { --card-accent: #f0a04a; }
.c-pink { --card-accent: #f04ab4; }
.c-cyan { --card-accent: #4af0f0; }
.c-yellow { --card-accent: #f0e04a; }
.c-red { --card-accent: #f04a6a; }
/* =====================================================
フッター
===================================================== */
footer {
position: relative; z-index: 1;
text-align: center;
padding: 1.5rem;
border-top: 1px solid var(--border);
font-family: var(--font-mono);
font-size: .7rem;
color: var(--text-dim);
letter-spacing: .1em;
}
/* =====================================================
レスポンシブ
===================================================== */
@media (max-width: 600px) {
h1 { font-size: 1.1rem; }
main { padding: 1rem; }
}
</style>
</head>
<body>
<!-- =====================================================
ロック画面
===================================================== -->
<div id="lockscreen">
<div class="lock-box">
<div class="lock-title">// authentication required</div>
<div class="lock-sub">このポータルはパスワードで保護されています。<br>一度認証すると、次回からは不要です。</div>
<div class="lock-input-wrap">
<input type="password" id="pw-input" placeholder="パスワードを入力..." autocomplete="current-password">
</div>
<button class="lock-btn" onclick="tryUnlock()">→ アクセス</button>
<div class="lock-err" id="pw-err"></div>
</div>
</div>
<!-- =====================================================
ヘッダー
===================================================== -->
<header>
<div class="header-left">
<div class="logo-line">
<div class="logo-dot"></div>
<h1>HOME SERVER</h1>
</div>
<div class="header-host">HOST <span id="disp-host">検出中...</span></div>
</div>
<div class="header-right">
<div class="header-time">
<div class="time" id="clock">--:--:--</div>
<div id="datestr">----/--/--</div>
</div>
</div>
</header>
<!-- =====================================================
メインコンテンツ
===================================================== -->
<main>
<!-- ── ファイル / ナレッジ ── -->
<section class="section">
<div class="section-label">ファイル & ナレッジ</div>
<div class="cards">
<a class="card c-green" id="card-trilium" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">📝</div>
<span class="card-name">Trilium Notes</span>
<span class="card-badge">HTTPS</span>
</div>
<div class="card-desc">階層型ノート管理。個人ウィキ・メモ・日記に。</div>
<span class="card-url" id="url-trilium">https://hostname:7070</span>
</div>
</a>
<a class="card c-blue" id="card-nextcloud" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">☁️</div>
<span class="card-name">Nextcloud</span>
<span class="card-badge">HTTPS</span>
</div>
<div class="card-desc">セルフホスト型クラウドストレージ。ファイル共有・カレンダー・連絡先。</div>
<span class="card-url" id="url-nextcloud">https://hostname:8080</span>
</div>
</a>
<a class="card c-cyan" id="card-immich" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">📷</div>
<span class="card-name">Immich</span>
<span class="card-badge">HTTPS</span>
</div>
<div class="card-desc">高性能フォト管理。Google Photos代替。AI顔認識・自動分類。</div>
<span class="card-url" id="url-immich">https://hostname:2283</span>
</div>
</a>
<a class="card c-orange" id="card-freshrss" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">📡</div>
<span class="card-name">FreshRSS</span>
<span class="card-badge">HTTPS</span>
</div>
<div class="card-desc">セルフホストRSSリーダー。複数フィード一括管理。</div>
<span class="card-url" id="url-freshrss">https://hostname:5050</span>
</div>
</a>
</div>
</section>
<!-- ── テレビ / メディア ── -->
<section class="section">
<div class="section-label">テレビ & メディア</div>
<div class="cards">
<a class="card c-purple" href="https://my.local.konomi.tv:7000/tv/" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">📺</div>
<span class="card-name">KonomiTV</span>
<span class="card-badge">HTTPS</span>
</div>
<div class="card-desc">地上波・BS・CSをブラウザでライブ視聴。録画再生にも対応。</div>
<span class="card-url">https://my.local.konomi.tv:7000/tv/</span>
</div>
</a>
<a class="card c-pink" id="card-edcb" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">⏺️</div>
<span class="card-name">EDCB</span>
<span class="card-badge">HTTP</span>
</div>
<div class="card-desc">EPGベース録画予約システム。番組表から直接予約。</div>
<span class="card-url" id="url-edcb">http://hostname:5510/</span>
</div>
</a>
<a class="card c-pink" id="card-edcb-epg" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">📅</div>
<span class="card-name">EDCB-EPG</span>
<span class="card-badge">HTTP</span>
</div>
<div class="card-desc">EDCB番組表ビュー。週間EPGを見やすく表示。</div>
<span class="card-url" id="url-edcb-epg">http://hostname:5510/EMWUI/epg.html</span>
</div>
</a>
</div>
</section>
<!-- ── インフラ管理 ── -->
<section class="section">
<div class="section-label">インフラ管理</div>
<div class="cards">
<a class="card c-yellow" id="card-cockpit" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">🖥️</div>
<span class="card-name">Cockpit</span>
<span class="card-badge">HTTP</span>
</div>
<div class="card-desc">Linux系統管理WebUI。CPU・メモリ・ストレージをリアルタイム監視。</div>
<span class="card-url" id="url-cockpit">http://hostname:9090</span>
</div>
</a>
<a class="card c-blue" id="card-lxd" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">📦</div>
<span class="card-name">LXD-UI</span>
<span class="card-badge">HTTPS</span>
</div>
<div class="card-desc">LXDコンテナ・VM管理WebUI。インスタンスの作成・管理。</div>
<span class="card-url" id="url-lxd">https://hostname:8443</span>
</div>
</a>
<a class="card c-green" id="card-dockhand" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">🐳</div>
<span class="card-name">Dockhand</span>
<span class="card-badge">HTTPS</span>
</div>
<div class="card-desc">Dockerコンテナ管理UI。コンテナの起動・停止・ログ確認。</div>
<span class="card-url" id="url-dockhand">https://hostname:3000</span>
</div>
</a>
<a class="card c-orange" id="card-npm" href="#" target="_blank" rel="noopener">
<div class="card-inner">
<div class="card-header">
<div class="card-icon">🔀</div>
<span class="card-name">Nginx Proxy Manager</span>
<span class="card-badge">HTTP</span>
</div>
<div class="card-desc">リバースプロキシ管理。SSL証明書の自動取得・ホスト設定。</div>
<span class="card-url" id="url-npm">http://hostname:81</span>
</div>
</a>
</div>
</section>
</main>
<footer>
HOME SERVER PORTAL · powered by Nginx Proxy Manager
· <span id="footer-host"></span>
</footer>
<!-- =====================================================
JavaScript
===================================================== -->
<script>
/* =====================================================
設定 - ここを編集してカスタマイズ
===================================================== */
const CONFIG = {
// ポータルのパスワード(変更してください)
password: "homeserver2026",
// localStorageのキー(変更不要)
storageKey: "portal_auth_v1",
// ポートが空なら自動検出したホスト名を使う
// falseにすると window.location.hostname を使う
autoDetectHost: true,
// ホスト名を手動で上書きしたい場合はここに書く(空文字で自動)
manualHost: "",
};
/* =====================================================
ホスト名検出
===================================================== */
const hostname = CONFIG.manualHost || window.location.hostname;
// 表示
document.getElementById("disp-host").textContent = hostname;
document.getElementById("footer-host").textContent = hostname;
// URL書き換え関数
function buildUrl(proto, port, path = "/") {
return `${proto}://${hostname}:${port}${path}`;
}
// 各カードのURLを差し込む
const urlMap = {
"card-trilium": { el: "url-trilium", href: buildUrl("http", 7070) },
"card-immich": { el: "url-immich", href: buildUrl("http", 2283) },
"card-nextcloud": { el: "url-nextcloud", href: buildUrl("http", 8080) },
"card-freshrss": { el: "url-freshrss", href: buildUrl("http", 6060) },
"card-edcb": { el: "url-edcb", href: buildUrl("http", 5510) },
"card-edcb-epg": { el: "url-edcb-epg", href: buildUrl("http", 5510, "/EMWUI/epg.html") },
"card-cockpit": { el: "url-cockpit", href: buildUrl("http", 9090) },
"card-lxd": { el: "url-lxd", href: buildUrl("https", 8443) },
"card-dockhand": { el: "url-dockhand", href: buildUrl("http", 3000) },
"card-npm": { el: "url-npm", href: buildUrl("http", 81) },
};
Object.entries(urlMap).forEach(([cardId, { el, href }]) => {
const card = document.getElementById(cardId);
const urlEl = document.getElementById(el);
if (card) card.href = href;
if (urlEl) urlEl.textContent = href;
});
/* =====================================================
時計
===================================================== */
function updateClock() {
const now = new Date();
document.getElementById("clock").textContent =
now.toLocaleTimeString("ja-JP");
document.getElementById("datestr").textContent =
now.toLocaleDateString("ja-JP", { year: "numeric", month: "2-digit", day: "2-digit", weekday: "short" });
}
updateClock();
setInterval(updateClock, 1000);
/* =====================================================
ロック画面
===================================================== */
function tryUnlock() {
const val = document.getElementById("pw-input").value;
if (val === CONFIG.password) {
localStorage.setItem(CONFIG.storageKey, "1");
document.getElementById("lockscreen").classList.add("hidden");
} else {
const err = document.getElementById("pw-err");
err.textContent = "× パスワードが違います";
setTimeout(() => err.textContent = "", 2000);
document.getElementById("pw-input").value = "";
document.getElementById("pw-input").focus();
}
}
// Enterキー対応
document.getElementById("pw-input").addEventListener("keydown", e => {
if (e.key === "Enter") tryUnlock();
});
// 認証済みチェック
if (localStorage.getItem(CONFIG.storageKey) === "1") {
document.getElementById("lockscreen").classList.add("hidden");
}
</script>
</body>
</html>
HTMLを保存したらNPMでHTMLを表示する設定を追加。
http://サーバーIP:81 にアクセス → Proxy Hosts → Add Proxy Host
| 項目 | 値 |
|---|---|
| Domain Names | サーバーのホスト名 |
| Scheme | http |
| Forward Hostname | 127.0.0.1 |
| Forward Port | 80 |
歯車マークを押し下記を貼り付けて保存すれば完了です。
location / {
root /data/html;
index index.html;
}
HTMLの修正箇所は見ると分かりますが、このあたりやカードURLなど。
const CONFIG = {
password: "homeserver2026", // ← パスワード変更
manualHost: "", // ← 空=自動検出 / "192.168.1.10"で固定も可
};

