Deploy a Palworld dedicated server on Linux in one command, using Terraform (provision the server) and Ansible (configure the OS and the game).
make deploy PROVIDER=<name>The project is built to be extended by the community:
| Layer | Pluggability |
|---|---|
| Cloud providers | Self-contained directory under terraform/providers/. DigitalOcean ships ready to use; add Hetzner/OVH/AWS by copying _template and filling in the TODOs — see CONTRIBUTING.md |
| Linux distributions | Debian 13 and Ubuntu 22.04/24.04/26.04 supported out of the box; the Ansible roles select distro-specific vars/tasks at runtime, so new distros are additive files, not edits |
make deploy PROVIDER=<name>
│
┌────────────┴─────────────┐
▼ make provision ▼ make configure
terraform apply ansible-playbook
terraform/providers/<name> │ reads the dynamic inventory
│ │ (cloud.terraform plugin ← terraform state)
│ creates the VM + ▼
│ registers it in the roles: common → steamcmd → firewall → palworld
└─► ansible inventory (OS prep) (installer) (ufw) (game+systemd)
Terraform and Ansible stay decoupled: Terraform writes an ansible_host
resource into its state (via the shared
ansible-inventory module), and
Ansible reads it back through the
cloud.terraform.terraform_provider
dynamic inventory plugin — no IP copy-pasting, no generated files, and each
tool runs (and re-runs) on its own.
| Tool | Version | Notes |
|---|---|---|
| Terraform | >= 1.6 | |
| Ansible | core >= 2.15 | pipx install ansible |
| GNU make | any | |
| An account at a supported provider | see make list-providers |
-
Pick a provider and follow its README to set up credentials:
make list-providers
Provider README DigitalOcean terraform/providers/digitalocean/README.md -
Set the game passwords (required — the deploy fails fast without them):
Option A — via
make(recommended):eval $(make gen-passwords)
Option B — manually:
export PALWORLD_SERVER_PASSWORD="$(openssl rand -base64 15)" export PALWORLD_ADMIN_PASSWORD="$(openssl rand -base64 15)"
Variable Purpose PALWORLD_SERVER_PASSWORDPlayers need this to join PALWORLD_ADMIN_PASSWORDAdmin/RCON password Read by
ansible/group_vars/palworld/palworld.ymlat deploy time (swap foransible-vaultif you prefer). Setpalworld_allow_empty_passwords: truethere to run password-less. -
Deploy:
make deploy PROVIDER=<name>
-
Play — get the server IP with
make output PROVIDER=<name>, then connect in-game to<ip>:8211.
| Command | Effect |
|---|---|
make help |
list all targets |
make list-providers |
list available providers |
make deps |
install the required Ansible collections |
make init PROVIDER=<p> |
initialize Terraform for the selected provider |
make plan PROVIDER=<p> |
preview infrastructure changes |
make provision PROVIDER=<p> |
create/update the server (terraform apply) |
make configure PROVIDER=<p> |
(re-)run the Ansible configuration — day-2 changes |
make deploy PROVIDER=<p> |
provision + configure |
make output PROVIDER=<p> |
show server IP / name |
make destroy PROVIDER=<p> |
tear everything down |
make forget-host PROVIDER=<p> |
drop the server's SSH key from known_hosts |
make gen-passwords |
generate random server and admin passwords |
make fmt |
format all Terraform code |
make lint |
run every linter CI runs |
make test |
run the Terraform tests (mocked providers, no credentials) |
Changing game settings later: edit the files in ansible/group_vars/palworld/, then
make configure — the palworld role re-renders the config and restarts the
service (via handlers) only when something actually changed.
Always run make from the repository root. The dynamic inventory resolves its
project_pathrelative to the working directory.
All user-facing knobs live in
ansible/group_vars/palworld/ — one file per
role: palworld.yml (passwords, server name, gameplay tweaks via
palworld_settings_overrides, backup retention), firewall.yml (SSH port,
inbound rules), steamcmd.yml (steam user) and common.yml (OS preparation).
The full catalogs with defaults:
| Area | Reference |
|---|---|
| Game settings (115 keys, descriptions, types, defaults) | docs/palworld-settings.md |
| Systemd limits, backups, paths, service behavior | ansible/roles/palworld/defaults/main.yml |
| Firewall policies & rules | ansible/roles/firewall/defaults/main.yml |
| Steam user | ansible/roles/steamcmd/defaults/main.yml |
| OS preparation | ansible/roles/common/defaults/main.yml |
| Infrastructure (region, size, image, SSH key name) | terraform/providers/<name>/README.md and terraform.tfvars.example |
| Aspect | Behavior |
|---|---|
| Process | Runs as the unprivileged steam user under systemd (palworld.service); auto-restarts on crash and every 4h (runtime_max_sec) to mitigate the server's memory leak |
| Backups | palworld-maintenance.sh updates via steamcmd and archives saves to ~steam/palworld_backups/ before every start (retention: palworld_backup_retention_days, default 5 days) |
| Restore | Stop the service, extract the archive over .../PalServer/Pal/Saved, start the service |
ssh root@<server-ip>
systemctl status palworld # logs: journalctl -u palworld| Area | Posture |
|---|---|
| Credentials | Cloud tokens are sensitive = true Terraform variables, supplied via TF_VAR_* env vars or git-ignored *.tfvars |
| SSH | Key-only auth; trust-on-first-use host key checking (StrictHostKeyChecking=accept-new) — a changed host key is a hard failure (run make forget-host after destroy/recreate on a recycled IP) |
| Firewall | Default deny inbound; only 22/tcp (SSH) and 8211/udp (game) are open. RCON (25575/tcp) stays closed unless you opt in |
| Game passwords | The deploy refuses to ship an unprotected server unless you explicitly opt out |
| Runtime user | The game runs as a locked, unprivileged steam user |
| To add | How |
|---|---|
| A cloud provider (Hetzner, OVH, ...) | Copy terraform/providers/_template/, fill in the numbered TODOs — the Makefile and CI pick it up automatically. Guide: CONTRIBUTING.md · interface: docs/provider-contract.md |
| A distribution (Fedora, ...) | Add per-role vars/ + tasks/ files keyed on ansible_os_family — no shared code changes. Guide: CONTRIBUTING.md |
| Question | Answer |
|---|---|
Can I run Ansible/Terraform directly instead of make? |
Yes — see the provider's README for the exact paths, or run make help to see how the Makefile composes the commands |
| Upgrading from the pre-modular layout? | Terraform addresses changed — terraform destroy with your old checkout first, or migrate state manually with terraform state mv |
| How do I transfer a world to a new server? | Back up on the old server (the maintenance script does this on every start), copy the archive, extract over .../PalServer/Pal/Saved on the new one, restart |
| Where is the Terraform state stored? | Locally in terraform/providers/<name>/ (git-ignored); remote backends are supported — see Remote state |