English · 한국어
Scripts and a native macOS app for running a Palworld dedicated server directly on an Apple Silicon Mac, using Rosetta 2 and a Wine compatibility layer — no virtualization, no Docker.
- Palworld ships no macOS server build. SteamCMD pulls the Windows depot and Wine runs it, with Wine itself executing as x86_64 under Rosetta 2. That is two translation layers (Wine: Win32 API → macOS, Rosetta: x86_64 → ARM64).
- It is a UE5 dedicated server, so crashes and degraded performance are real possibilities. Four to eight players generally holds up; large player counts or sprawling bases may not.
- Box64 is irrelevant here (it targets ARM Linux), and Whisky is discontinued.
The official WineHQ casks (
wine-stable,wine@devel,wine@staging) fail the macOS Gatekeeper check, are deprecated, and will be disabled on 2026-09-01. → The free path that currently works cleanly is the Gcenx repackaging of Apple's Game Porting Toolkit. - Nothing here is tied to a specific Wine build. Changing the single
WINE_BINline switches compatibility layers. - Rosetta 2 is unavoidable. GPTK's
wine64, Gcenx's plain builds (...-osx64.tar.xz) and CrossOver are all x86_64 binaries, andPalServer.exeis itself Windows x86-64. Rosetta 2 is the only x86→ARM translator on macOS (there is no FEX or box64 equivalent). This is why macOS shows a "Support Ending for Intel-based Apps" notice after installing GPTK — expected, and identical for any Wine you pick.
Apple has said Rosetta 2 will remain for a few more releases and then be narrowed. Upgrading macOS on the machine running this server can silently kill it. Before upgrading, take a backup with
./backup_save.sh, and afterwards confirm Wine still runs with./setup.sh.When Rosetta does go away, the durable answer is a native Linux server. App ID 2394010 provides a Linux depot, so a low-power mini PC or a VPS runs it with no translation at all. Save data is compatible as-is: extract a
./backup_save.sharchive intoPal/Saved/there (only the config path differs,Config/WindowsServer/→Config/LinuxServer/).
If stability matters more than convenience, a small Linux box is the better host. This project targets "run it on the MacBook I already own, for a few friends."
| Safe shutdown | RCON Save → Shutdown → SIGINT → SIGTERM → SIGKILL, escalating only on failure |
| Backups | Timestamped archives, integrity-checked, with named backups exempt from cleanup |
| Restore / migrate | Roll back to any backup, or import a Saved folder from another server |
| Auto-restart | Handles the memory leak: back up → stop → (optionally) update → start → verify |
| Game settings | All 119 PalWorldSettings.ini options, read and written safely |
| Version & updates | Compare the installed build against Steam's latest, then update in one step |
| macOS app | Menu bar + dashboard, built with swiftc alone — no Xcode required |
| Localized | App UI in English, Korean and Japanese |
- Apple Silicon Mac (Intel is not supported: the app builds arm64-only, and the installer stops early on an Intel machine)
- macOS 14 or newer
- About 7 GB of free disk space
- Xcode Command Line Tools and Homebrew (see below)
xcode-select --installThis makes python3 usable. On a new Mac /usr/bin/python3 exists as a file but is a
stub — running it only opens the developer-tools installer. RCON safe shutdown,
status queries and settings editing all depend on python3, so install.sh checks that
it actually runs and stops early if it does not.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile && exec zshHomebrew. The installer deliberately does not run this for you — fetching and executing a remote script is a decision you should make yourself.
git clone git@github.com:sangwon0001/mac-palworld-server-operator.git palworld-server
cd palworld-server && ./install.shIt handles dependency checks, the server download, config generation and building and installing the GUI app.
- Idempotent — finished steps are skipped, so re-running after an interruption is safe.
- Non-destructive — existing saves and a configured
AdminPasswordare never overwritten. - Runs as you — Homebrew refuses to run as root, so nothing is wrapped in
sudo. Only Rosetta, which genuinely needs it, asks for a password.
./install.sh --check # report state only, change nothing
./install.sh --yes # unattended
./install.sh --no-app # CLI only, skip the GUI appWhy not a
.pkg? A.pkgruns its postinstall as root, and Homebrew refuses to run as root. Putting a 5.6 GB download inside an installer wizard also hides progress and makes interruption hard to recover from. A drag-to-Applications.dmgis no better here: the app drives the shell scripts in this folder, so the app alone does nothing.
./start_server.sh # start (background, waits for the port to bind)
./status.sh # PID, CPU, RAM, port, players, saves, backups
./stop_server.sh # safe shutdown
./backup_save.sh # back up now
./update_check.sh # is there a newer build?Or use the app: Palworld Server.app lives in the menu bar with a dashboard window.
The shell scripts hold all the server-control logic. The app only invokes them, so the app, the terminal and cron run exactly the same code and anything done in the app is reproducible from the CLI.
palworld-server/
├── install.sh ★ one-shot installer — start here
├── config.sh shared settings, helpers, shell RCON client
├── config.local.sh.example template for personal settings
├── setup.sh prerequisite check and guidance
├── install_update.sh install/update via SteamCMD
├── start_server.sh start (nohup / tmux / foreground)
├── stop_server.sh safe shutdown (RCON → SIGINT → SIGTERM → SIGKILL)
├── backup_save.sh backups, naming, renaming
├── restore_save.sh restore, or import a save from another server
├── status.sh status (human-readable and --json)
├── auto_restart.sh backup + restart, on a schedule
├── install_launchd.sh schedule with launchd (recommended)
├── install_cron.sh schedule with cron (needs Full Disk Access)
├── settings.sh read/write the 119 game settings
├── update_check.sh installed build vs Steam's latest (1-hour cache)
├── uninstall.sh remove what was installed (report-only by default)
└── app/ macOS GUI app (SwiftUI)
├── Sources/
│ ├── PalworldServerApp.swift @main, menu bar + window
│ ├── ContentView.swift dashboard and tabs
│ ├── GameSettingsView.swift settings editor
│ ├── SettingsCatalog.swift labels, categories and ranges for 119 settings
│ ├── ServerController.swift script execution, polling, settings
│ ├── RconClient.swift native RCON (players, broadcast, kick, ban)
│ ├── Localization.swift en / ko / ja
│ └── Models.swift maps status.sh --json
├── Resources/<lang>.lproj/ translations
└── build.sh builds the .app without Xcode
Runtime locations, all created automatically:
~/PalworldServer/ the server itself, plus logs/ and run/
~/palworld_backups/ backup archives
~/.palworld_wine/ Wine prefix
./stop_server.sh # warn over RCON, then stop
./stop_server.sh --now # save and stop immediately
./stop_server.sh --force # also clean up an unresponsive processEach stage runs only if the previous one failed: RCON Save + Shutdown → SIGINT →
SIGTERM → SIGKILL. The RCON path is the one least likely to lose a save because the
game flushes its own data, so setting RCON_PASSWORD is strongly recommended.
./backup_save.sh # back up now
./backup_save.sh --name "before boss" # named backup
./backup_save.sh --list # list, with names
./backup_save.sh --rename 20260805_1130 "important"
./backup_save.sh --rename 20260805_1130 "" # clear the name- If the server is running, the save is flushed over RCON first, so progress held only in memory is not missed.
- Every archive is verified with
tar -tzfimmediately after creation; a corrupt one is deleted rather than left to be trusted. - Retention: older than 14 days is removed, but the newest 10 are always kept.
- Named backups are never auto-deleted. Naming one means you want to keep it. Clearing the name puts it back in scope for cleanup.
The name goes after the timestamp
(palworld_backup_20260805_113000_before_boss.tar.gz) so the existing glob and the
fixed-width date parsing keep working unchanged.
./stop_server.sh # required first
./restore_save.sh --latest # roll back to the newest backup
./restore_save.sh <backup.tar.gz> # roll back to a specific one
./restore_save.sh --yes <backup.tar.gz> # same, without the confirmation prompt
./restore_save.sh --import ~/Downloads/Saved # import from another serverThe current state is snapshotted as prerestore_*.tar.gz before anything is
overwritten, so a wrong restore is still recoverable. The newest three are kept;
older ones are cleared out with the ordinary backups once they pass the retention
period.
Keep the world ID folder name (a random hex string) exactly as it was — renaming it makes the server treat it as a new world. If
SaveGames/0/holds more than one world, name the one to use inDedicatedServerName.
./settings.sh --json # all settings as JSON (what the app reads)
./settings.sh --get ExpRate
./settings.sh --diff # only what differs from the defaults
./settings.sh --set ExpRate=2.0 Difficulty=Casual ServerName="My Server"
./settings.sh --reset # reset gameplay values
./settings.sh --reset --all # reset operational keys too (careful)In the app: the Game Settings tab groups all 119 options into eight categories with typed controls (switches, sliders, pickers, masked password fields) and search. Edits are staged and written in one batch on Apply, so nothing touches the file until you confirm.
Safety design. Rather than re-serialising the whole
OptionSettings=(...)line, only the requested keys are rewritten in place. A key added by a future game update is therefore never lost just because this toolkit doesn't know about it. The file is backed up toPalWorldSettings.ini.bak_<timestamp>before every write, and a value whose type doesn't match (text where a number belongs) is rejected without touching the file.
--resetprotects operational keys. ClearingAdminPasswordand flippingRCONEnabledback toFalsewould drop safe shutdown to signals and risk losing the save, so those are excluded by default.--alloverrides this; naming a key explicitly always honours the request.
./update_check.sh # installed build vs Steam's latest
./auto_restart.sh --update # back up → stop → update → startTwo version concepts are shown:
| Shown as | Source | Available when |
|---|---|---|
Game version (v1.0.2.101103) |
RCON Info |
only while the server runs |
Build number (24466863) |
appmanifest_2394010.acf |
always (a file read) |
Update detection compares build numbers. The latest build comes from the official
steamcmd +app_info_print, which takes about 6 seconds, so the result is cached for an
hour — a warm cache answers in 0.03 s without touching the network.
If the backup fails, the update is aborted; the server is never changed without a point to return to.
Automatic updates are deliberately not enabled. A game update can affect save compatibility or mods, so the timing should be yours. To automate anyway, add to cron:
0 4 * * 1 cd <this folder> && ./auto_restart.sh --update --if-empty
Two schedulers are available and they install the same three jobs. Pick one — installing both would back up and restart twice over, and each refuses to install while the other is present.
./install_launchd.sh --install # recommended
./install_launchd.sh --status
./install_launchd.sh --removelaunchd is what macOS actually uses. It needs no Full Disk Access grant, and it
makes up a run that was missed because the Mac was asleep. The trade-off is that
LaunchAgents live in your login session, so nothing fires while nobody is logged
in — if this Mac reboots unattended, either enable automatic login or use cron.
./install_cron.sh --show # preview
./install_cron.sh --install
./install_cron.sh --remove| When | What |
|---|---|
| hourly at :30 | back up the save |
| daily 05:00 | back up and restart (clears the memory leak) |
| daily 06:00 | restart only if memory exceeds 8 GB and nobody is connected |
The hourly backup sits at :30 on purpose. auto_restart.sh takes a backup of its
own before stopping the server, so on the hour it would collide with both daily
jobs. Should two operations ever overlap anyway — a cron job and a click in the
app, say — they take turns instead of interleaving: every script that touches the
save takes a lock first, and the one that arrives second waits and says what it
is waiting for.
Required if you chose cron: it needs permission to reach your home directory. System Settings → Privacy & Security → Full Disk Access → add
/usr/sbin/cron. Without it, cron jobs fail silently — check~/PalworldServer/logs/cron.logafterwards. The launchd jobs need none of this; their log is~/PalworldServer/logs/launchd.log.
./uninstall.sh # report what exists; removes nothing
./uninstall.sh --all # scheduling + app + server files + Wine prefix
./uninstall.sh --backups # the backups too (asks you to type DELETE)Nothing is deleted without naming it. --server takes a final backup before
removing the server folder, since the save games live inside it. Backups are
never removed by --all, and --yes does not skip the confirmation for them.
Homebrew packages are left alone; the command to remove Wine is printed instead.
./status.sh --address # the addresses to hand out- Port forwarding:
UDP 8211→ this Mac's LAN IP. - Reserve the IP in your router's DHCP settings, or players lose the address on reboot.
- Wi-Fi is fine. Palworld uses tens to hundreds of KB/s per player; what matters is
packet loss and jitter, not bandwidth. Check with:
0% loss with single-digit millisecond deviation is plenty.
ping -c 100 -i 0.2 "$(route -n get default | awk '/gateway:/{print $2}')" - Sleep:
start_server.shhandles this. It reads the actual assertions frompmset -g assertionsrather than the policy inpmset -g custom, leaves things alone if something (Amphetamine, say) already holds sleep off, and otherwise runscaffeinate -dims -w <server PID>, which releases automatically when the server stops. Closing the lid still sleeps without an external display, whatever is holding assertions.
cd app && ./build.sh --installBuilds with the Command Line Tools' swiftc alone — no Xcode.
The app carries no server-control logic of its own; pressing a button runs the
corresponding .sh script. Deleting the app changes nothing about how the server runs.
State is read from ./status.sh --json rather than by parsing human-readable output.
Player management uses a native Swift RCON client rather than the shell helper, because going through the shell spawns bash and python3 every time:
| Path | Round trip |
|---|---|
via shell (bash + python3 startup) |
~260 ms |
| native Swift | ~36 ms |
Server lifecycle stays with the scripts, though. RCON cannot start a stopped server, read
CPU/RAM/PID, or handle backups — and stop_server.sh's four-stage fallback exists
precisely for when RCON is unresponsive, so the app must not reimplement it.
When a response body contains non-ASCII text, the server declares a length larger than what it actually sends:
Info declared 74 / sent 58 ⚠ Save declared 24 / sent 24 ✔ Broadcast declared 54 / sent 39 ⚠ ShowPlayers declared 33 / sent 33 ✔Trusting the declared length means waiting for bytes that never arrive — a 5-second timeout in practice. A player with a non-ASCII nickname trips this via
ShowPlayers.Both clients (Swift and shell) now treat the declared length as an upper bound and stop at the packet terminator (
00 00), which mis-declared responses still send correctly.The truncation itself is server-side and cannot be fixed from here, so broadcasts are most reliable in plain letters and numbers. (Spaces are converted to underscores — Palworld's RCON treats them as argument separators.)
274 UI strings in English, Korean and Japanese. Settings (⚙️) → Language switches immediately, with no restart. The default follows the system language, falling back to English.
- The Korean source text doubles as the lookup key, so untranslated entries fall back to readable text instead of a blank label.
- Building without Xcode rules out String Catalogs (
.xcstrings), so the classicapp/Resources/<lang>.lproj/Localizable.stringsfiles are copied into the bundle. NSLocalizedStringfollows the system language and would need a restart, so the matching.lprojbundle is opened directly instead.
The Japanese was written by hand rather than machine-translated, but has not been reviewed by a native speaker. Corrections to
app/Resources/ja.lproj/Localizable.stringsfollowed bycd app && ./build.sh --installare welcome.
| Symptom | Check |
|---|---|
| "Wine not found" | Set an absolute WINE_BIN in config.local.sh |
| SteamCMD platform error | +@sSteamCmdForcePlatformType windows must come before +login |
| Port never binds | Run ./start_server.sh --fg to see Wine's errors directly |
| Crashes on start | ./install_update.sh --validate to verify file integrity |
| RCON auth fails | AdminPassword (ini) and RCON_PASSWORD (config.local.sh) must match, and RCONEnabled=True |
| Next start fails | A stale process is still around — ./stop_server.sh --force |
| cron does nothing | Grant Full Disk Access to /usr/sbin/cron; check logs/cron.log |
| Save not found | World ID folder must keep its original name, and SaveGames/0/ should hold only one |
tail -f ~/PalworldServer/logs/palserver.log # server log
tail -f ~/PalworldServer/logs/auto_restart.log # scheduled restarts
tail -f ~/PalworldServer/logs/operations.log # audit trailThe server log is buffered. When checking whether something just happened, trust the timestamps inside the log rather than the file's size or mtime.
- Shell scripts must stay bash 3.2 compatible — cron runs
/bin/bash, which is 3.2 on macOS. Nomapfile, no associative arrays. - Everything in the scripts is English: comments, messages and
--helpoutput. Only the app UI is translated, and there the Korean source text is the lookup key (seeapp/Sources/Localization.swift), soSettingsCataloglabels stay in Korean by design. config.local.shholds the RCON password and is gitignored. It is generated byinstall.shwith mode 600.
MIT — use it, change it, redistribute it, just keep the copyright notice. No warranty: running a Windows game server through two translation layers is inherently fragile, so back up your saves.
Palworld is a trademark of Pocketpair, Inc. This project is unaffiliated with and unendorsed by Pocketpair.