Saltar al contenido principal

Self-hosting the Resource Pack

ResourcePackManager ships its own small HTTP server. When autoHost: true (the default) and preferSelfHost: true (the default), the plugin tries to host the merged pack from the same JVM as the Minecraft server itself — no external file host, no separate web server, no manual URL paste.

This page covers how that self-host path works, what the sanity checks do, how the port and hostname are chosen, and what to configure when the defaults don't fit your setup.

If instead you want to host the zip through your own existing web server outside of RSPM, see the Troubleshooting page section on autoHost: false.

Delivery Decision Tree

When a player joins, RSPM picks a delivery URL using this precedence:

  1. selfHostForce: true — straight to self-host, no probes, no remote upload. Mainly for testing the self-host path. Bypasses every other flag.
  2. preferSelfHost: true AND selfHostEnabled: true AND not in network mode — try self-host with three sanity checks (see below). If all pass, commit to self-host. If any fails, fall back to the remote path.
  3. Otherwise — upload the pack to https://magmaguy.com/rsp/ and announce that URL. If the upload fails or SHA1 check reports SESSION_NOT_FOUND, fall back to self-host (assuming selfHostEnabled: true).

Once a URL is in hand, RSPM uses Minecraft's multi-pack API on 1.20.3+ (so it coexists with other server-sent packs) and the older single-pack method on older versions.

The Three Sanity Checks

When preferSelfHost: true, RSPM runs these checks in order before committing to self-host:

Layer 1 — Heuristic check on the resolved external host

If the resolved host (see "External Host Detection" below) is RFC1918 (10.*, 172.16-31.*, 192.168.*), loopback (127.*), link-local (169.254.*), or unspecified (0.0.0.0), self-host can't possibly work for internet clients. Skip immediately and use remote hosting.

This catches the very common "ipify lookup failed, fell back to LAN IP" failure mode.

Layer 2 — Localhost self-probe

Open a HEAD request to http://127.0.0.1:<port>/rspm.zip, verify HTTP 200 and non-empty body. Catches:

  • port-bind collisions (something else is on the chosen port)
  • missing pack file (the route is registered but the zip isn't on disk yet)
  • route-registration bugs

Times out aggressively (3 s) so a slow probe can't drag out boot.

Layer 3 — External reachability probe

POST the announced URL to POST /rsp/probe on the magmaguy.com hoster. The hoster fetches the URL from a public vantage point (with SSRF guards and a tight timeout) and reports back whether it's reachable.

Catches the most common production failure mode: the server has a public IP but the HTTP port isn't forwarded at the router or firewall. Layer 2 passes (the server responds on 127.0.0.1), but no real client could ever download the pack.

Decision policy on probe outcomes:

  • reachable=true → external clients can reach our URL. Commit to self-host.
  • reachable=false → external clients can't. Tear down self-host and use remote hosting (which is universally reachable from magmaguy.com).
  • probe communication itself fails (IOException) → couldn't verify either way. Default to keeping self-host: refusing on inability-to-probe would be paradoxical because the remote path also needs magmaguy.com.

What the checks still don't detect

The NAT-hairpin edge case where the port is open to the public internet (Layer 3 passes) but the operator's own router doesn't loop traffic back from inside the LAN. External clients work, but the operator testing from the same machine fails.

Workaround for now: when testing from the host machine with a hairpin-broken router, set preferSelfHost: false OR selfHostExternalHost: 127.0.0.1.

Port Resolution

Two settings interact:

  • selfHostPort — explicit port (any positive integer) or -1 (default) for auto-derivation.
  • networkHttpOffset-v2 — only consulted when selfHostPort = -1. Added to the Minecraft server port. Default 1.

The default is selfHostPort: -1 + networkHttpOffset-v2: 1, so:

  • MC port 25565 → HTTP port 25566
  • MC port 25584 → HTTP port 25585

This auto-staggers HTTP ports across backends on a single-host network without any admin configuration — each backend already has a unique MC port so each gets a unique HTTP port.

Why offset 1?

Most shared / managed Minecraft hosting (Pterodactyl-based panels, etc.) allocates a narrow port range per container (often only 4–10 ports). Larger offsets land outside the range and the host firewall silently blocks the HTTP port. Offset 1 fits even tight allocations.

Self-hosted admins with full port control can bump this to any value, but if you run a proxy network you must also bump the proxy's network-http-offset-v2 to match.

Watch out: RCON collision

If your host enables RCON by default on MC port + 1, choose offset 2 or 3 to avoid a port collision. Check server.properties for rcon.port=.

Versioned config key

The setting in config.yml is literally named networkHttpOffset-v2. The v1 key was networkHttpOffset with default 100 — that default broke on shared / managed hosting where each game container only gets ~4–10 consecutive ports, MC + 100 fell outside the range, the HTTP server bound internally but the host firewall dropped external traffic, and the proxy got a silent CONNECT_FAILED forever. v2 ships with default 1 so MC + 1 stays well inside even the narrowest container allocations.

If you upgrade from v1, the dead v1 key sits in your config as a harmless artifact until you clean it up — RSPM intentionally does not read it.

External Host Detection

selfHostExternalHost controls what hostname clients see in the URL. Leave empty (default) to auto-detect in this priority order:

  1. api.ipify.org / checkip.amazonaws.com — returns the public IPv4 of this host. Cached once per /rspm reload so the IP services aren't hammered.
  2. Bukkit.getIp() — the server's bind address, when non-empty and not 0.0.0.0. Usually a LAN address.
  3. InetAddress.getLocalHost() — best-effort.
  4. localhost — last-resort fallback. Clients outside the box won't reach this.

If auto-detection lands on a non-routable address and preferSelfHost: true, the Layer 1 heuristic check fails and the plugin switches to remote hosting.

For the most reliable self-host setup, set selfHostExternalHost explicitly to your public hostname (e.g. play.example.com). This skips the ipify/AWS detection entirely and the probe runs against the explicit value.

Configuration Reference

# Whether the built-in HTTP server may be used as a delivery path at all.
# When false, self-host is never attempted regardless of the other flags.
selfHostEnabled: true

# Port for the self-host HTTP server.
# -1 (default) = auto-derive: HTTP port = Minecraft server port + networkHttpOffset-v2.
# Set to any positive value to force an explicit port.
selfHostPort: -1

# Added to the Minecraft server port when selfHostPort = -1.
# Default 1 (MC 25565 -> HTTP 25566). Must match the proxy's network-http-offset-v2
# in proxy networks.
networkHttpOffset-v2: 1

# Public hostname or IP clients use to reach the self-host server.
# Leave empty for auto-detect (api.ipify.org / checkip.amazonaws.com).
selfHostExternalHost: ""

# Try self-host FIRST with three sanity checks, fall back to remote if any fail.
# When false, use the legacy order: remote upload first, self-host only on upload failure.
preferSelfHost: true

# Skip ALL other delivery paths and force self-hosting.
# Bypasses sanity checks AND remote upload. Mainly for testing.
selfHostForce: false

Backend HTTP Server Routes

The built-in HTTP server always serves the pack zip at:

http://<host>:<port>/rspm.zip

In network mode (RSPM is behind a Velocity / BungeeCord / Waterfall proxy) two additional routes are registered for the proxy plugin to pull:

http://<host>:<port>/bedrock.zip   # the Bedrock-converted pack
http://<host>:<port>/mappings.json # the Geyser custom-mappings JSON

All three routes are file-backed: the route reads the file fresh on every request, so a re-mix that rewrites the same zip path is picked up automatically without restarting the HTTP server. The Bedrock routes support If-Modified-Since so the proxy poller pays roughly zero bandwidth when nothing has changed. All routes return 404 cleanly when the underlying file is absent (e.g. before the first mix completes).

Verifying Self-host is Active

/rspm status shows:

  • Active delivery: SELF-HOSTED — self-host is in use
  • Active delivery: REMOTE (magmaguy.com) — remote auto-host is in use
  • URL: ... — the actual URL clients will see
  • Resolved external host — what selfHostExternalHost resolved to
  • Public IP (auto-detected) — what ipify/AWS reported, if anything
  • selfHostPort — auto vs explicit, and the resolved value

If you expected self-host but see remote, the boot log explains which sanity check failed and why.

Common Mistakes

  • Setting selfHostPort to an arbitrary value and forgetting it on the proxy — in network mode the proxy uses mcPort + network-http-offset-v2 to compute each backend's HTTP port. An explicit selfHostPort overrides the auto-derivation but the proxy doesn't know about it, so it will still poll mcPort + offset. Leave selfHostPort: -1 in network mode unless you're prepared to coordinate.
  • Trusting the Layer 3 probe with a hairpin-broken router — the probe runs from magmaguy.com's vantage point, so it can't catch the case where external clients work but the operator's own LAN doesn't loop back. Test from a phone on cellular if you're unsure.
  • Bumping networkHttpOffset-v2 past your hosting provider's port range — the symptom is a silent CONNECT_FAILED forever on the proxy side. Check that mcPort + offset is in your container's allocated port band before raising the offset.