Server on a Mac Mini

Before getting an NOI2024 gold medal, I had always wanted to set up a server at home, and after that, I started considering it seriously.

As a server at home, it’s important to consider size, noise, and power consumption, so traditional servers are not a suitable choice. Considering future needs like setting up local AI models, Minecraft Server, or even using it as a test machine, CPU and GPU performance are also very important.

After careful consideration, I took advantage of an educational discount to get an M2 Mac Mini, with 24GB of memory, upgraded to 10GbE, and a built-in 256GB SSD. I’ll expand it via Thunderbolt 4 later. The total cost was just over 7000 and included free AirPods (it was indeed a bit of a loss buying the M2 at this point, but if I didn’t hurry, I would be bored to death at home).

When it arrived, I realized it was a bit larger than I expected (I thought it would be only slightly bigger than the Apple TV).

Mac Mini Unboxing

Infrastructure

After receiving the Mac Mini, first connect it to a monitor using an HDMI cable for the initial setup. For macOS Sonoma, open System Settings, go to General > Sharing, enable Remote Management and Remote Login, click the info icon on the right, set the remote login password and allowed remote management users, and finally check all permissions in Options. It is recommended to set a strong password to prevent brute force attacks.

General > Sharing

Remote Management

Remote Options

Since the server needs to be on 24/7, set all power options in Lock Screen to Never. The server also needs to auto-login on restart, so in Users & Groups, select the user to auto-login in Automatically log in as. If you need to auto-start after a power failure, enable Start up automatically after a power failure in Energy Saver.

Lock Screen

Users & Groups

Energy Saver

Next, you can shut down the Mac Mini, disconnect the monitor, and place it where you want before powering it on. To access the remote desktop within the LAN, also using macOS Sonoma, open Screen Sharing.app and enter the Mac Mini’s local hostname or address and login password, for example, vnc://yaoxi-stds-mac-mini.local. For Windows or Linux, you can use other VNC clients.

Screen Sharing.app

From here, you can choose according to your needs.

Docker

Simply download the macOS version of Docker Desktop.

I initially wanted to try using OrbStack instead of Docker Desktop for performance optimization, but since it’s unofficial, it had some inexplicable issues. For example, I couldn’t pull images using OrbStack on my MacBook Pro.

To properly pull Docker Hub images, you’ll also need to configure a proxy.

I allocated 20GB of memory to Docker on the server, which should be more than enough.

Docker Desktop

Dial-Up Internet

The following steps are recommended to be performed on the same LAN as the Mac Mini.

macOS supports PPPoE dial-up directly, so connect the Mac Mini and the original router to the optical modem via Ethernet cables and dial separately. This avoids the need to configure port forwarding on the router (unless your router is integrated into the optical modem, in which case port forwarding will be necessary). However, first, ensure the dial-up IP address is not NATed by the ISP.

Setting up dial-up internet is straightforward. In System Settings under Network, click the ... in the bottom right to Add Service, select PPPoE for Interface, and choose the Ethernet port used to connect to the optical modem. Then enter the dial-up account and password in the PPPoE settings; the account is usually a phone number, and you can ask the ISP’s customer service for the password.

Since the Mac Mini is now in the office rather than at home (and even if it were at home, I wouldn’t be able to physically operate it while doing pre-university at THU), it needs to automatically reconnect if the dial-up disconnects (otherwise, it would be useless) and start automatically on boot. So use a script like the one below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/bash

SERVICE_NAME="PPPoE"

if networksetup -showpppoestatus "$SERVICE_NAME" | grep -q "disconnected"; then
  echo "PPPoE connection is down. Attempting to reconnect..."
  networksetup -connectpppoeservice "$SERVICE_NAME"
else
  echo "PPPoE connection is active."
fi

You can check the $SERVICE_NAME using networksetup -listpppoeservices.

Then you need to set it to start on boot and run in the background by creating a launchd.plist configuration file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>yaoxi-std.pppoe-reconnect</string>

    <key>ProgramArguments</key>
    <array>
      <string>/path/to/connect.sh</string>
    </array>

    <key>StartInterval</key>
    <integer>30</integer>

    <key>RunAtLoad</key>
    <true />

    <key>KeepAlive</key>
    <true />
  </dict>
</plist>

Then install the service:

1
2
sudo cp launchd.plist /Library/LaunchDaemons/yaoxi-std.pppoe-reconnect.plist
sudo launchctl load /Library/LaunchDaemons/yaoxi-std.pppoe-reconnect.plist

Manually disconnect PPPoE to test. If it reconnects automatically within 30 seconds, it’s successful. You can also test by rebooting the server.

Public Access

China Mobile’s network environment relies on NAT, and home broadband is all NATed, so there is no public IP. Therefore, I chose China Telecom and placed the Mac Mini in the office.

Mac Mini in Office

However, China Telecom blocks ports 80 and 443 on home broadband, so without registration, you can’t directly map a domain name to that IP for HTTP(s) access. And the .dev domain I chose cannot be registered according to the regulations (it’s not that I don’t want to), so I had to use some tricks.

The dilemma is that if you use an overseas server for forwarding or internal network penetration, services with high network latency requirements like Minecraft may be affected.

So I came up with a brilliant idea: use two subdomains, one for Cloudflare Tunnel for internal network penetration, ensuring that no port number is needed in the browser address; the other directly resolves via ddns to the dynamic IP generated by the server dial-up since China Telecom does not block the default Minecraft Server port 25565.

Let’s get started. Adhering to the idea of putting all services in Docker containers, use a docker-compose.yml to set it up:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
networks:
  shared-network:
    external: true

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    networks:
      - shared-network
    restart: unless-stopped
    command: tunnel --protocol http2 --no-autoupdate run --token <cloudflare-tunnel-token>
  cloudflare-ddns:
    image: favonia/cloudflare-ddns:latest
    container_name: cloudflare-ddns
    security_opt:
      - no-new-privileges:true
    network_mode: host
    restart: unless-stopped
    read_only: true
    cap_drop: [all]
    environment:
      - CF_API_TOKEN=<cloudflare-api-token>
      - DOMAINS=i.yaoxi-std.dev,mc.yaoxi-std.dev
      - PROXIED=false

Here, cloudflared uses shared-network to prevent the service from exposing ports on the host and instead connect directly to nginx through Docker’s internal network and forward based on domain names to the virtual host. First, create a network named shared-network with docker network create shared-network, which will not need to be recreated after restarting Docker.

Then configure Cloudflare Tunnel. Note that you should enter nginx:80 instead of localhost:80 because different containers’ internal networks are not the same.

Tunnel Configuration

Remember to replace <cloudflare-tunnel-token> and <cloudflare-api-token>.

Note that Docker defaults to using the system proxy. To prevent ddns from getting the proxy server’s IP, add 1.1.1.1 to the bypass list in Docker Desktop.

Docker Desktop Proxy Bypass

Remember to add Docker Desktop to start on boot, which can be adjusted in System Settings.

Nginx

Install it in a Docker container and connect it to the previously created shared-network.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
networks:
  shared-network:
    external: true

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    networks:
      - shared-network
    restart: unless-stopped
    volumes:
      - ./conf.d:/etc/nginx/conf.d:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

Firewall

Since public access is allowed, it’s recommended to enable the firewall. Turn on the built-in macOS firewall in the settings.

Remember to add local services and Docker Desktop to the firewall whitelist.

Firewall Settings

Remote Desktop

If you use macOS’s built-in Screen Sharing.app to connect to the remote desktop, you’ll find that you cannot change the Mac Mini’s resolution and it defaults to 1080p.

I tried using ScreenResX to adjust the resolution based on other blogs, but the resolution can only be changed when a monitor is connected. When disconnected, it reverts to 1080p. Finally, I found a software called BetterDisplay on Reddit, which creates a virtual display with any resolution, solving the issue.

BetterDisplay Settings

“Stage Manager” seems quite suitable for server monitoring

Rescue Mode

To prevent being unable to SSH directly via the public IP due to ISP issues, consider keeping an SSH tunnel open in Cloudflare Tunnel.

Some places might block port 5900 (I don’t know why), but they usually don’t block port 22. In this case, to access the Mac Mini via VNC, you can use an SSH tunnel:

1
ssh -L 5901:localhost:5900 user@hostname

This maps port 5900 on the remote host to port 5901 on the local machine, so you can connect to vnc://localhost:5901 in your VNC client.

If there are issues causing ddns to fail, try changing the last digit of the IP address within a certain range. If PPPoE fails, it’s irrecoverable.

Blog

To be updated.

Minecraft Server

To be updated.

Online Judge

To be updated.


Last modified on 2024-08-15