How to Set Up Minecraft Server At Home And Expose It To Public

我把 Minecraft Java 版伺服器放在家裡的電腦上,朋友從外網用一個固定的公網位址就能連進來。伺服器跑在虛擬機裡的 Docker 中,跟日常使用的系統隔開。外網那一端只放一台很小的 AWS 機器,負責把連接埠轉回家。遊戲行程和世界存檔都留在家裡。

這篇只涵蓋 Java 版。Java 版走 TCP 25565,反向 SSH 隧道轉得動。基岩版(Bedrock)走的是 UDP,同一條隧道轉不了。下面的遊戲版本和 AWS 免費額度,以 2026 年 10 月的情況來寫。

最後長這樣

1
2
3
4
5
6
7
8
9
10
朋友的 Minecraft 客戶端
│ TCP 25565
▼
AWS EC2(公網 IP,只轉流量)
│ 反向 SSH,由家裡主動連出
▼
家裡電腦上的虛擬機
│ 127.0.0.1:25565
▼
Docker 裡的 Minecraft Java 伺服器

家裡的虛擬機開機後自己連去 AWS。連線是從家裡打出去的,所以家用路由器不用做任何連接埠轉發,電信把用戶放在 CGNAT 後面也沒關係。朋友連的是 AWS 的位址。世界存檔留在家裡的硬碟上。

比較過的幾種做法

做法 它解決什麼 卡在哪
Port mapping(連接埠轉發) 路由器把外網的 25565 轉到家裡某台電腦 很多家用線路沒有真正的公網 IP,外面連不進來。連得進來時,家裡的連接埠也直接暴露在外網,IP 一變朋友存的位址就失效
Virtual Server(虛擬伺服器) 同上。這是不少家用路由器韌體裡的名字,背後還是連接埠轉發 跟 Port mapping 是同一件事
DDNS 給會變的家用 IP 一個固定網域名稱 只解決「位址會變」。連不進家裡、以及家裡連接埠對外開放,兩件事都還在
Reverse SSH tunnel(反向 SSH 隧道) 家裡主動連上一台有公網 IP 的小機器,由那台機器代收 25565 多一台常開的小機器,以及它的公網 IPv4 費用

我用反向 SSH 隧道。家裡沒有穩定的公網 IP,也不想為了一台遊戲伺服器去改路由器、把家裡網路露出去。

為什麼外面只放一台小機器

Minecraft 吃 CPU 和記憶體的地方在家裡。AWS 上那台機器只轉 TCP,t2.micro(1 vCPU、1 GiB 記憶體)就夠。它不跑 Java,也不存世界。

免費額度要看帳號是什麼時候開的,以 AWS 文件 為準:

帳號開立時間 額度
2025-07-15 之前 開通後 12 個月。每個月 750 小時的 t2.micro 或 t3.micro,以及搭配 EC2、使用中的公網 IPv4 750 小時
2025-07-15 當日或之後 改成點數制,最高約 200 美元,用完或滿 6 個月為止。主機要選當時標成 free tier eligible 的機型,新帳號通常是 t3.micro 這一代,不一定還有 t2.micro

公網 IPv4 從 2024-02-01 起每小時 0.005 美元,一個位址全月大約 3.65 美元。舊額度的 750 小時通常蓋得住「掛在一台運行中的 EC2 上的那一個位址」。下面兩種情況會計入帳單:

  • 執行個體停了,Elastic IP 還掛在上面
  • 帳號裡另外躺著沒在用的 Elastic IP

這台跳板要一直開著。朋友存的是它的位址,機關掉,伺服器就從外網消失。

任何有公網 IPv4 的小 VPS 都能擔任同一個角色。後面的 SSH 設定不依賴 AWS。

家裡:用虛擬機把伺服器隔開

Minecraft、Docker、以及連去 AWS 的金鑰都放在虛擬機裡。宿主機不開 25565,也不持有那把隧道金鑰。

VirtualBox VMware Workstation
取得方式 直接安裝 個人使用曾改為免費,授權條件時有變動,安裝前看一次目前條款
沒有桌面時怎麼跑 VBoxManage startvm "minecraft" --type headless 有自己的 headless 模式
這次的需求 客體要能主動連出外網 同左

我用 VirtualBox。客體用 NAT 就夠了:隧道是虛擬機自己連出去,VirtualBox 不用做連接埠轉發。

客體我裝 Ubuntu Server LTS,給它:

  • 2 顆 vCPU
  • 4 GB 記憶體(下面的 Java 堆疊用 2 GB,剩下留給系統和容器)
  • 30 GB 虛擬磁碟(世界和備份都放這裡,地圖大了再加)

宿主機開機後要把這台虛擬機以 headless 拉起來,裡面的 systemd 才會接著跑。Linux 宿主上就是開機時執行:

1
VBoxManage startvm "minecraft" --type headless

虛擬機裡用 Docker 跑伺服器

Docker 負責三件事:Java 版本不用自己管、世界資料放在一個目錄裡、升級時拉新映像再重建容器。

映像用 itzg/minecraft-server。先在客體裝 Docker Engine 和 Compose 外掛,步驟以 Docker 的 Ubuntu 安裝說明 為準,重點是最後要有 docker compose 這條指令,以及把使用者加入 docker 群組:

1
sudo usermod -aG docker "$USER"

登出再登入,群組才會生效。

在家目錄建一個目錄,寫入 compose.yaml:

1
mkdir -p ~/minecraft
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ~/minecraft/compose.yaml
services:
mc:
image: itzg/minecraft-server
tty: true
stdin_open: true
restart: unless-stopped
ports:
# 只聽虛擬機自己。外面進來的路是下一節的 SSH 隧道。
- "127.0.0.1:25565:25565"
environment:
EULA: "TRUE"
TYPE: PAPER
# 改成朋友客戶端主畫面左下角的版本。兩邊不一致就會被踢出。
# 26.3 是 2026-09 的 Java 正式版。
VERSION: "26.3"
MEMORY: "2G"
ENABLE_RCON: "true"
RCON_PASSWORD: "replace-with-a-long-password"
TZ: "Asia/Taipei"
MOTD: "home"
volumes:
- ./data:/data

EULA: "TRUE" 是在接受 Minecraft EULA。不設的話容器會直接退出。這份 EULA 不允許靠伺服器本身向玩家收費;這裡是朋友一起玩的私人伺服器。

TYPE: PAPER 用的是 Paper。同樣的視距,它比官方伺服器省 CPU。想改回官方伺服器,拿掉 TYPE 這行即可,預設是 VANILLA。

RCON 只活在容器裡,compose 沒有把 25575 公布出去。密碼換掉,這個檔案不要再貼到別的地方。

啟動並看日誌,等到出現 Done:

1
2
3
cd ~/minecraft
docker compose up -d
docker compose logs -f

第一次會下載伺服器,多等幾分鐘。確認只聽在本機:

1
2
ss -lnt | grep 25565
# 要看到 127.0.0.1:25565

世界在 ~/minecraft/data。docker compose down 不會刪掉這個目錄。升級映像:

1
2
docker compose pull
docker compose up -d

版本號要升級時,先備份 data,再改 VERSION,然後執行上面兩行。

跳板:讓外網連得到 25565

在離玩家近的區域開一台 Amazon Linux 2023。舊免費額度用 t2.micro,新帳號改選當下標成 free tier eligible 的最小機型。安全群組先開兩個規則:

連接埠 來源 用途
TCP 22 家裡目前的 IP(主控台的 My IP) 自己登入
TCP 25565 0.0.0.0/0 朋友連 Minecraft

22 不要對全世界開。家裡 IP 變了導致登不進去時,從 AWS 主控台改這條規則即可,主控台不走這台機器的 SSH。

接著申請一個 Elastic IP 並掛到這台執行個體。沒掛之前,停止再開啟會換一個公網 IP,朋友存的位址就失效。掛上之後,原本自動配的那個公網 IP 會被放掉,帳號裡仍然只有一個位址。

SSH 預設把反向隧道綁在 127.0.0.1,那樣只有跳板自己連得到。要讓安全群組放進來的流量被接到隧道上,跳板的 sshd 得允許客戶端指定監聽位址。新增:

1
2
3
4
5
# /etc/ssh/sshd_config.d/99-minecraft-tunnel.conf
AllowTcpForwarding yes
GatewayPorts clientspecified
ClientAliveInterval 30
ClientAliveCountMax 3

GatewayPorts clientspecified 的意思是:客戶端有寫監聽位址才對外聽。沒寫的話仍然只聽 loopback。ClientAlive* 讓斷掉的隧道被清掉,避免 25565 被一條已經死掉的連線占住。

檢查語法、確認設定有被讀進去,再重載:

1
2
3
sudo sshd -t
sudo sshd -T | grep -i gatewayports
sudo systemctl reload sshd

輸出要是 gatewayports clientspecified。若仍是 gatewayports no,就是主設定檔裡已有 GatewayPorts no,而且比這個 drop-in 先被讀到。把那一行改成 GatewayPorts clientspecified 再查一次。sshd 同一項設定以先讀到的為準。

另外建一個只能轉連接埠的使用者,不拿來登入做別的事:

1
2
3
sudo useradd --create-home --shell /bin/bash tunnel
sudo mkdir -p /home/tunnel/.ssh
sudo chmod 700 /home/tunnel/.ssh

/home/tunnel/.ssh/authorized_keys 先留空,下一節把虛擬機的公鑰貼進來。那一行會限制這把金鑰只能轉 25565。

把隧道做成斷線會自己回來

在虛擬機裡產生一把沒有密碼的金鑰。沒有密碼是為了讓開機能自己連;權限靠跳板上的 authorized_keys 收緊,這把金鑰做不了別的事。

1
2
ssh-keygen -t ed25519 -f ~/.ssh/mc-tunnel -N ""
cat ~/.ssh/mc-tunnel.pub

把印出來的公鑰放到跳板的 /home/tunnel/.ssh/authorized_keys,整行是:

1
restrict,port-forwarding,permitlisten="*:25565" ssh-ed25519 AAAA... mc-tunnel

restrict 關掉終端、指令和轉發,後面的 port-forwarding 再把轉發打開,permitlisten 只放行 25565。檔案權限:

1
2
sudo chown -R tunnel:tunnel /home/tunnel/.ssh
sudo chmod 600 /home/tunnel/.ssh/authorized_keys

permitlisten 需要 OpenSSH 7.9 之後的版本。Amazon Linux 2023 和 Ubuntu 22.04 之後都有。

回到虛擬機,先手動試一次。203.0.113.10 換成自己的 Elastic IP:

1
2
3
4
5
6
7
8
ssh -N \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=accept-new \
-i ~/.ssh/mc-tunnel \
-R 0.0.0.0:25565:127.0.0.1:25565 \
tunnel@203.0.113.10

-R 0.0.0.0:25565:127.0.0.1:25565 是整篇的關鍵:跳板在所有介面上聽 25565,流量送回虛擬機的 Minecraft。少寫前面的 0.0.0.0:,跳板就只聽它自己,朋友會一直逾時。ExitOnForwardFailure=yes 讓「連接埠沒綁上」變成直接失敗,而不是一條看起來還活著、其實沒在轉的連線。

在跳板上另開一個工作階段確認:

1
2
sudo ss -lntp | grep 25565
# 要看到 0.0.0.0:25565,行程是 sshd

再用手機網路,或任何不在家裡的機器:

1
nc -vz 203.0.113.10 25565

TCP 通了只代表隧道通了。再用 Minecraft 客戶端加這組位址,連接埠維持預設的 25565。

手動這條確認沒問題後,用 Ctrl-C 停掉,改由 systemd 接手。虛擬機裡新增 /etc/systemd/system/mc-tunnel.service。User= 和 /home/mc 換成客體裡實際的登入名稱(金鑰就是上一步 ssh-keygen 寫在那個家目錄的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Unit]
Description=Minecraft reverse SSH tunnel
After=network-online.target docker.service
Wants=network-online.target

[Service]
User=mc
ExecStart=/usr/bin/ssh -N \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=accept-new \
-i /home/mc/.ssh/mc-tunnel \
-R 0.0.0.0:25565:127.0.0.1:25565 \
tunnel@203.0.113.10
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
1
2
3
sudo systemctl daemon-reload
sudo systemctl enable --now mc-tunnel.service
systemctl status mc-tunnel.service

之後虛擬機重開,Docker 會把 Minecraft 拉起來(restart: unless-stopped),這個服務會把隧道重新連上。

朋友怎麼連

客戶端的伺服器位址填 Elastic IP。左下角的遊戲版本要和 VERSION 一致,否則會被拒絕進入。

白名單打開之前,不要把 IP 傳出去。虛擬機裡:

1
2
3
cd ~/minecraft
docker compose exec mc rcon-cli whitelist on
docker compose exec mc rcon-cli whitelist add 朋友的遊戲 ID

保持預設的正版驗證(online mode)。沒有登入 Mojang 帳號的客戶端連不上。

備份就是把 data 打包。打包前先停伺服器,避免抄到寫到一半的世界:

1
2
3
4
cd ~/minecraft
docker compose stop
tar -czf ~/mc-backup-$(date +%F).tar.gz data
docker compose start

隧道通了但人進不去時

依序看這四個地方:

  1. 跳板上 ss 顯示的是 127.0.0.1:25565。-R 少了 0.0.0.0:,或是 sshd 的 GatewayPorts 仍是預設的 no。
  2. 客戶端立刻失敗,日誌寫 remote port forwarding failed。25565 還被舊的 sshd 占著,或是 permitlisten 那一行和實際監聽位址不符。在跳板上把占住 25565 的那條 sshd 結束,虛擬機的 mc-tunnel 會自己重連。
  3. 客戶端說版本不符。把 VERSION 改成客戶端顯示的版本,備份 data 之後重建容器。
  4. 容器反覆重啟。docker compose logs 裡如果是 EULA,就是 EULA 沒設成 TRUE;如果是記憶體,把客體記憶體加大,或把 MEMORY 從 2G 降下來。

小結

  1. 伺服器放在家裡的 VirtualBox 客體裡,用 Docker 跑 Paper,連接埠只綁 127.0.0.1。
  2. 外網入口是一台很小的 EC2,只轉 TCP,不跑遊戲。
  3. 反向隧道由家裡主動連出,路由器不用開連接埠,也避開 CGNAT。
  4. 跳板的 sshd 要設 GatewayPorts clientspecified,客戶端的 -R 要寫 0.0.0.0:25565。
  5. 隧道金鑰在 authorized_keys 裡收成只能轉 25565,SSH 的 22 只對家裡的 IP 開放。
  6. 白名單先開,再把 Elastic IP 給朋友。世界資料在 ~/minecraft/data,停機後打包即可備份。