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.
To put an HTTPS reverse proxy in front of RSPM while keeping automatic delivery, use selfHostExternalUrl as described below. Use autoHost: false only when another system owns both hosting and player delivery.
Delivery Decision Tree
When a player joins, RSPM picks a delivery URL using this precedence:
Normal automatic delivery requires autoHost: true. With autoHost: false, only the explicit selfHostForce: true override sends a URL.
selfHostForce: true— straight to self-host, no probes, no remote upload. Mainly for testing the self-host path. OverridesautoHostandselfHostEnabled; URL validation and listener startup must still succeed.preferSelfHost: trueANDselfHostEnabled: true: try self-hosting with the checks below, including on proxy backends. Fall back to remote hosting if a check fails.- Otherwise — reuse or upload the pack to
https://magmaguy.com/rsp/and announce that URL. If initialization, the SHA1 check or upload fails, try verified self-hosting (assumingselfHostEnabled: true).SESSION_NOT_FOUNDalso clears the remote session so a later attempt can initialize again. Automatic fallback runs the same checks as self-host-first; if both paths fail, no new delivery URL is announced.
The remote host accepts Java packs up to 50,000 KiB (51,200,000 bytes). RSPM checks the size before uploading. A larger pack is never uploaded: the console prints Automatic hosting skipped: resource pack is <size> bytes; the limit is 51200000 bytes. RSPM then stops using remote hosting for that pack and keeps retrying verified self-hosting (when selfHostEnabled: true) until hosting restarts for a new pack or a reload. Reduce the pack size or make self-hosting reachable, then run /rspm reload.
Once a URL is in hand, RSPM uses Minecraft's multi-pack API, so its Java pack can coexist with other server-sent packs. The current Bukkit plugin descriptor requires Minecraft 1.21.4 or newer.
The Three Sanity Checks
RSPM runs these checks in order before selecting automatic self-hosting, including fallback after a remote-hosting failure. Only selfHostForce: true bypasses them:
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.* or localhost), link-local (169.254.*), or unspecified (0.0.0.0), skip self-hosting. IPv6 unspecified, loopback, link-local, site-local and unique-local addresses are also rejected. This is a host-string heuristic; a hostname still needs the download checks below.
This catches the very common "ipify lookup failed, fell back to LAN IP" failure mode.
Layer 2 — Local and advertised URL downloads
RSPM sends a GET with a byte-range request first to http://127.0.0.1:<port>/rspm.zip, then to the exact URL players would receive. It compares up to the first 1 KiB with the local pack. HTTP 200 or 206 is accepted; reported lengths must match, and a 206 response must contain the expected Content-Range. Redirects are rejected. This 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)
- incorrect routes, HTML error pages or different pack bytes
- an advertised URL that the server itself cannot download, including broken NAT loopback
Each connection and read has a 3 s timeout, with a separate 3 s deadline for reading the sample. These are bounded sample checks, not complete client downloads.
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 with HTTP 200 or 206 → the probe reached the URL from its public vantage point. Commit to self-host.
- reachable=false or another HTTP status → verification failed. Withdraw the candidate Java URL and try remote hosting through magmaguy.com. In network mode, keep the backend HTTP listener available for proxy artifact polling.
- probe communication itself fails (IOException), or the probe client is unavailable → reachability is unverified. Try remote hosting; inability to contact the probe does not establish that uploading will fail. Automatic fallback cannot bypass this check.
What the checks still don't detect
The advertised-URL download now catches NAT-loopback failures from the server's own network. It cannot establish reachability from every player's network or prove that a full Minecraft client download succeeds. Test the complete download and pack acceptance with a real client; use a separate external network when checking public access.
For a quick host-machine test, set preferSelfHost: false and use the remote fallback, or test the public URL from a cellular/external network. For a permanent self-hosted setup, use split-horizon DNS (the same public hostname resolves to the server's LAN address inside your network). Do not set selfHostExternalHost to 127.0.0.1: loopback and private hosts are rejected by the public-host sanity check and cannot serve external clients.
Port Resolution
Two settings interact:
selfHostPort— explicit port from1to65535, or-1(default) for auto-derivation. The derived port must also be within that range; other values prevent the listener from starting.networkHttpOffset-v2— only consulted whenselfHostPort = -1. Added to the Minecraft server port to derive the HTTP port. Default1. (On proxy networks this same value is also the proxy's fallback guess for a backend's HTTP port — see below.)
The default is selfHostPort: -1 + networkHttpOffset-v2: 1, so:
- MC port 25565 → HTTP port 25566
- MC port 25584 → HTTP port 25585
Unique Minecraft ports yield unique derived HTTP ports, but a derived port can still collide with another backend's Minecraft port or another service. Check every listener and the ports allocated by your host.
Why offset 1?
Shared and managed hosting may allocate only a small set of ports to each container. An offset of 1 keeps the derived port close to the game port, but does not reserve it or guarantee that the host allocated or opened it. Confirm the exact HTTP port in your panel.
Self-hosted admins with full port control can bump this to any value. On a proxy network the backend announces the exact HTTP port it bound to the proxy automatically, so the proxy follows along even if you change the offset or set an explicit selfHostPort. Bumping the proxy's network-http-offset-v2 to match only matters as a fallback for the brief window before a backend has announced (or if announcement can't reach the proxy).
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, which could place the listener outside a hosting allocation. The v2 default is 1; that port still needs to be available and reachable.
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:
- api.ipify.org / checkip.amazonaws.com — returns the public IPv4 of this host. Cached once per
/rspm reloadso the IP services aren't hammered. Bukkit.getIp()— the server's bind address, when non-empty and not0.0.0.0. Usually a LAN address.InetAddress.getLocalHost()— best-effort.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
HTTPS and reverse proxies
Set selfHostExternalUrl to the complete public download URL:
autoHost: true
selfHostEnabled: true
preferSelfHost: true
selfHostPort: 25566
selfHostExternalUrl: "https://packs.example.com/rspm.zip"
Configure your reverse proxy to forward that public path to RSPM's internal http://<server>:25566/rspm.zip route and terminate TLS. RSPM still runs an HTTP listener; this setting does not install a certificate or configure your web server.
The exact URL is sent to Java players and used by the external reachability probe. No /rspm.zip suffix is added. It overrides selfHostExternalHost and public-IP detection for Java delivery; selfHostPort still controls the internal listener. Proxy polling for Bedrock artifacts continues to use the backend HTTP endpoint.
Only complete HTTP or HTTPS URLs with a host and without embedded credentials or fragments are accepted. An explicit URL port must be between 1 and 65535. An invalid URL prevents self-hosting. Normal automatic hosting can fall back to the remote host; forced self-hosting cannot. Leave the setting empty to use host and port detection.
Settings
# Whether the built-in HTTP server may be used as a delivery path at all.
# When false, ordinary self-host attempts are disabled. selfHostForce still overrides it.
selfHostEnabled: true
# Port for the self-host HTTP server.
# -1 (default) = auto-derive: HTTP port = Minecraft server port + networkHttpOffset-v2.
# Set to a value from 1 to 65535 to force an explicit port.
selfHostPort: -1
# Added to the Minecraft server port when selfHostPort = -1.
# Default 1 (MC 25565 -> HTTP 25566). On a proxy network this is only a FALLBACK:
# the backend announces the exact HTTP port it actually bound to the proxy, so
# you no longer have to match this against the proxy's network-http-offset-v2.
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: ""
# Optional complete public Java-pack URL, including the path.
# Your reverse proxy supplies HTTPS and forwards to the internal /rspm.zip route.
selfHostExternalUrl: ""
# 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
# Print every step of pack preparation and the hosting handshake.
# Default false — see "What the console actually prints" below.
# `/rspm verbose on|off` flips this same key at runtime and saves it here.
verboseLogging: false
What the Console Actually Prints
Remote Java hosting and Bedrock relay failures use randomized exponential backoff. Each flow allows one active attempt and up to five retries, then waits 15–30 minutes before starting another burst. Success resets the retry budget; the HTTP client adds no automatic retries underneath it. Java hosting checks the remote SHA-1 before uploading. Bedrock retries reuse successfully uploaded, unchanged artifacts for up to 20 minutes. The regular 25-minute relay renewal still uploads to refresh the server's lease.
RSPM deliberately prints one line about hosting on a normal boot — the outcome, not the journey:
[ResourcePackManager] Resource pack is live via self-hosting — http://play.example.com:25566/rspm.zip
or, when the remote path won:
[ResourcePackManager] Resource pack is live via automatic hosting — https://magmaguy.com/rsp/<uuid>
That line prints once each time a delivery path is freshly initialized, not for player retries or keep-alive ticks. A reload whose pack is unchanged can reuse the existing hosted registration instead of printing a new outcome.
Failed reachability checks are logged at detail level. For example: Self-host check: local pack link is not public. This is OK. The host and localhost checks end with This is OK.; the advertised-URL download and the external verification report their failure without that suffix. Invalid URL settings, listener startup failures and remote communication failures can still produce warnings. A failed check alone does not establish that the remote fallback succeeded; check the final delivery status.
To see the whole decision chain — which check failed, the probe URL, the reason code, each pack staged and each cluster merged — run:
/rspm verbose on
Then /rspm reload. This is the switch to flip before opening a bug report about hosting. The command writes verboseLogging: true into config.yml and the setting persists across restarts, so run /rspm verbose off when you're finished; editing the config key by hand has exactly the same effect.
Note that verboseLogging covers pack preparation and hosting. The Bedrock converter has its own separate switch, bedrockConverterDebug — see Bedrock conversion.
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 proxy), 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
http://<host>:<port>/rspm-update.jar # the universal RSPM update jar (authenticated)
The pack and Bedrock artifact routes are file-backed and resolve the current published artifact on each request. A successfully published re-mix becomes available without restarting the HTTP server; merely replacing a file on disk does not authorize it. Routes return 404 when no current artifact is authorized, the file is absent, or its bytes no longer match the publication digest. Bedrock routes also require conversion to remain enabled. Strong ETags support If-None-Match, which takes precedence over the compatibility If-Modified-Since check; unchanged polls can receive a bodyless 304. The update route is separately protected by a bearer token derived from the shared network key and is only available when a valid update jar can be offered.
Verifying Self-host is Active
/rspm status shows:
Active delivery: SELF-HOSTED— self-host is in useActive delivery: REMOTE (magmaguy.com)— remote auto-host is in useURL: ...— the actual URL clients will seeResolved external host— whatselfHostExternalHostresolved toPublic IP (auto-detected)— what ipify/AWS reported, if anythingselfHostPort— auto vs explicit, and the resolved value
If you expected self-host but see remote, /rspm status is the place to look — the boot log stays quiet about self-healed probe failures by design. Run /rspm verbose on and /rspm reload to see which sanity check failed and why.
Common Mistakes
- Treating probe success as a complete client test — the local sample and public probe cover particular network paths. Verify a complete pack download and acceptance with a real player; forced self-hosting skips these checks entirely.
- Bumping
networkHttpOffset-v2past your hosting provider's port range — the symptom is a silent CONNECT_FAILED forever on the proxy side. Backends announce their real bound port to the proxy, but the HTTP server still has to bind to a reachable port and the proxy still has to be able to reach it; ifmcPort + offsetlands outside your container's allocated port band, the bind/firewall fails regardless of announcement. Check that the port is in your container's allocated band before raising the offset.