Running a server

The web map

On this page

The web map shows the overworld from above in a browser, as the in-game map item draws it, with the places plugins mark on it, land claims, and (if you turn it on) the players. It is a full-screen viewer: drag and zoom the map, follow a player, copy a link to the spot you are looking at. It can hide the ground nobody has explored yet, as a vanilla map fills in.

There are two kinds of map, chosen with render:

  • Live (render: live) is the world as it stands, builds and all, drawn when somebody looks and drawn again where it changes. This is the one for a survival server.
  • Drawn once (render: on-create or on-plugin-signal) is terrain only, drawn a single time and never again, so it never shows what players built. This is for a server like Rusty, where a base on the map is a base raided.

The web map is off by default. When it is on it listens on every address by default, with no login: anyone who can reach the host on its port can see the map. See Security and privacy.

Turning it on

In rustmc.yml, a live map for a survival server:

web-map:
  enabled: true           # default false
  render: live            # default on-create
  title: My Survival      # the page's heading and tab: the server's name, say
  radius: 1024            # blocks round spawn

Start the server. It prints where the map is:

Web map listening on http://0.0.0.0:8100, reachable by anyone who can reach this host

and the map is at http://<host>:8100/. On a drawn-once map the server also says when drawing starts and finishes; until then the page says how far it has got and looks again every ten seconds.

To keep the map to yourself, bind 127.0.0.1 and put a reverse proxy in front, or block the port in a firewall.

The settings

Every key, with its default:

web-map:
  enabled: false
  host: 0.0.0.0
  port: 8100
  render: on-create
  radius: 1024
  title: World map
  fog: off
  url:
  cache-mib: 128
  live:
    redraw-seconds: 30
    threads: 2
  players:
    show: false
    names: false
    interval-seconds: 5
    hide-sneaking: true
    hide-invisible: true
KeyWhat it does
enabledTurns the map on.
hostThe address it binds. 0.0.0.0 takes connections from anywhere; 127.0.0.1 keeps it to this machine.
portIts TCP port. 0 picks a free port, printed at startup.
renderlive, on-create or on-plugin-signal; see Live and Drawn once.
radiusHow far round spawn the map reaches, in blocks. A rusty-island world is drawn to its island border instead.
titleThe page's heading and browser tab, and the title in map.json. Shown as text, never as markup.
fogoff, shared or per-player; see Fog of war.
urlThe map's public address as players reach it (https://map.example.org/ behind a proxy, say). Used for the sign-in links /map link gives. Empty means http://<host>:<port>/ (localhost when host is 0.0.0.0).
cache-mibWith live, the memory encoded tiles may use, in MiB.
live.redraw-secondsWith live, the least time between two drawings of one tile, and how often the page asks for changed tiles.
live.threadsWith live, how many threads draw tiles.
players.*Whether and how players are shown; see Players.

What is drawn

  • Where. radius blocks round world spawn, as a square of whole chunks; or, for a rusty-island world, the island to its border. Only the overworld is drawn.
  • How. As the in-game map item draws a map at its closest scale: each block's map colour, lighter where the ground rises from the block to its north and darker where it falls, and water darker the deeper it is.
  • Tiles. The map is cut into tiles of 512 by 512 pixels. At zoom 0 a tile is one region file (512 by 512 blocks) at a pixel a block. Zooms −1 to −4 each halve the scale, so at zoom −4 a pixel is 16 blocks and a tile covers 16 by 16 region files. Each zoomed-out tile is made from the four tiles below it, never by reading the world again.
  • The overview. overview.png is the whole area on one image at most 1024 pixels across, for a page elsewhere that shows the map as a card.

Live

With render: live each tile is drawn when somebody asks for it, from the world as it stands at that moment, and kept in memory. Nothing is written to disk for the tiles.

  • From what. A chunk loaded in the game is read from memory. Any other chunk that has been generated is read from its region file, on its own, without touching the game's caches. A chunk never generated is transparent: the map never generates a chunk to draw it.
  • Kept. Encoded tiles are kept in memory up to cache-mib. The least recently asked-for go first, and are drawn again if asked for again.
  • Drawn again. When a chunk changes, its tile and the zoomed-out tiles above it go stale. The map hears of changes as the autosave writes them and, while somebody is looking, also notices edits not written yet (at most once a second). A stale tile is drawn again when it is next asked for, but no more than once every redraw-seconds; until then the last drawing is served. So a build shows on the map within about redraw-seconds, plus up to redraw-seconds more before the page asks again.
  • Off the game's tick. Tiles are drawn on live.threads threads of the map's own, at background priority. Up to 16 tiles wait in a queue; past it a request is answered 503 with Retry-After: 5, and no thread is ever added for a burst. Many requests for one tile share one drawing.
  • The overview is drawn from the zoomed-out tiles, kept in memory, and drawn again at most once every ten minutes, only when something under it changed.
  • When something goes wrong. A region file that cannot be read is reported once in the log, not per request; the last good drawing of its tile is served meanwhile (or the tile with that region's unloaded chunks left transparent), and it is tried again every redraw-seconds.

Drawn once

With on-create or on-plugin-signal the map never shows what players built, because a world's map is drawn once and never redrawn:

  • on-create (the default) draws the map as soon as the server runs a world that has none, from the generator alone: whatever stands in the world, the map shows what the seed makes.
  • on-plugin-signal waits until a plugin asks for it (see For plugin authors), then draws the chunks that exist as they then stand, and the generator's terrain everywhere else. This is for a gamemode that builds landmarks after the world is made: Rusty asks once its monuments stand, so they are on the map.

Chunks drawn from the generator are not saved, so the map can cover far more than anyone has explored without filling the disk. Drawing runs on its own thread and builds chunks on up to half the cores (one to four); the game's tick is not involved. A rusty-island world about 4,000 blocks across is about 61,000 chunks.

The map is kept in <world>/web-map/:

web-map/
  map.json          the seed it was drawn from, the area, the tiles
  overview.png      the whole area, scaled down to at most 1024 pixels across
  tiles/r.<x>.<z>.png

A restart serves the map as it is. A new world gets a new map: wiping the world deletes the folder with it, and a map.json from a different seed is drawn over. To redraw a map on purpose, stop the server and delete <world>/web-map/ (with on-plugin-signal, a plugin then has to ask again). The zoomed-out tiles are made in memory from the saved images, so the folder stays exactly as the drawing left it.

The page

Every mode serves the same full-screen viewer, one HTML file with its style and script inline.

  • Moving about. Drag with a mouse or a finger; zoom with the wheel, a pinch, the + and − buttons, or the keyboard (arrow keys pan, + and − zoom, once the map has focus). The scale runs from a pixel for every 32 blocks to 8 pixels a block. A tile still loading is drawn from a coarser tile scaled up, so there are no holes. Home recentres on spawn (or the middle of the area).
  • Coordinates of the block under the cursor (or the last touch) are shown bottom left.
  • Copy link copies a link to the current view. The view is kept in the address (#x=..&z=..&zoom=..) as the map moves, so a link opens on the same spot.
  • The sidebar (a drawer behind the menu button on a phone) shows the map's title, a link back to ../ ("All maps", for a hub page), how many players are on the map and who, with their heads (click one to follow them until you drag the map), switches for the players, markers and claims layers (remembered in the browser), and the named places (click one to centre on it).
  • Players are their heads with their names under them, moved smoothly between updates. Heads come from https://mc-heads.net/avatar/<name>/16; one that will not load shows the name's initial.
  • Markers are dots with labels; tap one for its label and coordinates.
  • Claims are outlines; click one for its details. See Land claims.
  • With fog, the sidebar says how much is explored ("Explored: 12.5%") and explains the dark ground; with per-player it shows who you are signed in as, and "Sign out".
  • Accessible. Every control is at least 44 pixels, focus is visible, prefers-reduced-motion turns the easing off, and the page works down to 360 pixels wide. It uses the Silkscreen and Rubik fonts if the viewer has them installed and fetches no font.

On a live map the page asks for the tiles in view again every redraw-seconds; an unchanged tile costs only a "not modified" answer.

Under a path prefix

Every address in the page and the JSON is relative (tiles/0/1_2.png, markers.json), so a reverse proxy can serve a map under a path with the prefix stripped, and several servers' maps can share one site. With Caddy:

map.example.com {
	handle_path /rusty/* {
		reverse_proxy 127.0.0.1:8101
	}
	reverse_proxy 127.0.0.1:8100
}

Link to a prefixed map with its trailing slash (/rusty/), so its relative addresses resolve under it.

Players

Players are off by default. A survival server where people like to see each other can turn them on:

web-map:
  players:
    show: true             # default false
    names: true            # default false
    interval-seconds: 5
    hide-sneaking: true
    hide-invisible: true
SettingWhat anyone who can open the map learns
show: false (the default)Nothing about any player but how many are in the mapped world (stats.online in map.json). /players.json does not exist and no position is read for the map.
show: trueFor each player who is online, in the overworld, inside the drawn area and not left off by a rule below: the block they stand in, x and z only (no height, facing, health or anything else). Without names, the list is in place order, so a dot cannot be followed between answers by its place in the list.
names: trueThe same players' names beside their dots.
interval-secondsHow fresh positions are: read at most this often, only when somebody is looking. The map is never more current than that.
hide-sneaking: trueCrouching players are left off, as the game hides a crouching player's name tag.
hide-invisible: truePlayers under the invisibility effect, and spectators, are left off.

Opting out. With show on, any player can type /map hide to leave the map and /map show to come back; /map alone says which they are. The choice is kept by account in the server's web-map-hidden.json (beside ops.json), so it outlasts restarts and new worlds, and it takes effect at once.

Vanish. rustmc has no vanish of its own. A vanish plugin that makes a player invisible (the effect) or puts them in spectator mode is honoured by hide-invisible.

Fog of war

fog hides the map where nobody has been. It works in every render mode, and is meant for a live survival map.

web-map:
  fog: per-player                  # default off
  url: https://map.example.org/    # for the links /map link gives
  • off (the default): the whole area is shown.
  • shared: a chunk shows once any player has been within their view distance of it. Everyone sees the same map.
  • per-player: each viewer sees where they themselves have been, once signed in. Signed out, a viewer sees the shared map, which overview.png and the stats always use.

What counts as explored. Once a second the map looks at the players in the overworld. A player who has crossed a chunk border since the last look explores every chunk within their view distance. A teleport reveals where it lands, never the way between. Spectators and players who chose /map hide explore too. The spawn area starts explored.

Hidden means never sent. Hidden ground is painted over with fog on the server before a tile is encoded, so nothing of what lies under it is in the image. Markers and claims in hidden ground are left out of what the page is sent. On a drawn-once map with fog, the saved images' tiles/r.<x>.<z>.png addresses answer 404, and overview.png is served with fog over it.

Signing in, with per-player. In game, /map link gives a clickable one-time link. The code travels after #, so it is in no request line, log or Referer; it is valid for portal.link-minutes (see the web portal) and works once. The page trades it for a cookie (HttpOnly; Secure; SameSite=Lax) that lasts while it is used within portal.session-minutes. Sessions are kept in memory, so a restart signs everyone out. From the console, /map link <player> prints a link to that player's map (by name, for a player online or seen since the server started, or by account id), so you can see the map as they do.

Kept. One bit a chunk over the area: <world>/web-map/fog/shared.bin and, with per-player, <uuid>.bin for each player. They are written on autosave and at stop. A file that will not read is logged once and taken as empty. Turning fog off keeps the files, and turning it back on finds them; a changed radius keeps what is inside the new area.

History. The first time fog is turned on for a world, the shared fog is seeded from the chunks' saved InhabitedTime: every chunk players have spent time near, as the game counts it. Because a chunk is only saved when something in it changed, this is a lower bound. A history-seeded file in the fog folder records that it was done, and it is never done again, not even after a reset. To see what it would reveal, run it on a copy of the world first:

cargo run --release -p rustmc-server --bin fog-history -- <copy of the world> --radius 5000

Clearing. /map fog reset puts every fog back to the spawn area; /map fog reset <player> clears one player's (a player online, or seen by the map since the server started). Both work from the console, and for players holding rustmc.map.fog.reset (operators of level three by default). The files are rewritten at once, every tile, the overview and the stats are drawn again, and the console says who did it.

Land claims

When the claims plugin is installed, the map draws its claims, under a "Claims" switch in the sidebar (on by default, and shown only where a claims service answers).

  • Shapes. A claim is a rectangle or a polygon, drawn as its outline with a translucent fill. A claim the plugin gives a colour is drawn in it; otherwise admin claims are gold and players' a soft parchment. Its name is shown in the middle once it is large enough on screen.
  • Subclaims are drawn over the claim they are part of, a little more strongly filled.
  • Details. Clicking a claim shows its name, whether it is an admin claim or whose it is, the claim it is a subclaim of, its size in blocks and its extent. Where claims overlap, the smallest is picked.
  • What is shown. Only what a public map may show: the shape, the name, the owner's name, whether it is an admin claim, its colour and its parent. Never an account id, and never who is trusted in it.
  • How fresh. Claims are read at most every 30 seconds, off the game's tick, and the page asks every 30 seconds. A claims plugin that is missing or failing is simply no claims, with nothing in the log.
  • Under fog, a claim is shown only if every chunk it covers is ground the viewer has revealed (signed out, or with shared, the shared fog's). A claim partly in fog is left out entirely, never clipped, so an outline never gives away where somebody built.

Markers

Markers are labelled places plugins put on the map: Rusty marks each monument with its name. A marker is a dot and a label the page draws over the tiles, not part of the images, so the map is never redrawn for one. They are kept in <world>/web-map/markers.json with the seed, so they survive a restart but not a new world. Each belongs to the plugin that set it and goes when that plugin is unloaded, or at the next start when it is no longer installed.

What it serves

GET only, apart from the two fog sign-in requests:

PathWhat
/The page
/map.jsonThe area, the tiles and the stats below. A live map's also says "mode": "live", its zooms and redraw_seconds; a drawn-once map's names the seed it was drawn from
/overview.pngThe whole area, scaled down
/tiles/<zoom>/<x>_<z>.pngOne tile, <zoom> 0 to −4; at zoom −k a tile covers 2^k by 2^k region files
/tiles/r.<x>.<z>.pngA drawn-once map's saved image for one region file (404 with fog on)
/markers.json{"markers": [{"id", "label", "x", "z", "kind"}]}
/players.jsonOnly with players.show on (404 otherwise): {"interval_seconds", "players": [{"x", "z", "name"?}]}
/claims.jsonThe claims this viewer may see: {"available", "claims": [{"id", "x0", "z0", "x1", "z1", "points", "name", "owner_name", "admin", "area", "rgb", "parent", "parent_name"}]}
/me.jsonThe fog as this viewer sees it: {"fog", "name"?, "explored_chunks", "area_chunks", "link_minutes"}
POST /session, POST /signoutWith fog: per-player only: trade a /map link code for a session cookie, and end it

Anything else is 404; there is no directory listing and no file outside the map's own is read.

Caching. Every tile carries an ETag, and a request with a matching If-None-Match is answered 304 with no body. Tiles, a live map's overview and map.json are Cache-Control: public, max-age=30, so a browser or a CDN in front (Cloudflare, say) can keep them for 30 seconds. With fog: per-player, tiles are private, no-cache and their ETag includes the viewer's own fog, so a proxy never serves one viewer's map to another. claims.json is revalidated every time.

stats, for a hub page

map.json carries a stats object in every mode, for a page elsewhere that shows each server's map as a card beside a few numbers. None of it loads a chunk:

FieldWhat
area_blocks[width, depth] of the mapped area, in blocks
explored_chunksWith fog, the chunks the shared fog has revealed; without, the chunks in the area that have been generated and saved (checked at most once a minute)
area_chunksChunks in the area
onlineHow many players are in the overworld now, whatever players.show says; players a plugin hid are not counted
world_createdWhen the world was made, ISO 8601 UTC (2026-11-03T10:00:00Z), or null where the filesystem does not record it
markersHow many markers are on the map
claimsHow many land claims anyone may see: with fog, those wholly in the shared fog's ground

Limits

  • Only the overworld is mapped, out to radius (or the island border); nothing outside the area is drawn or served.
  • The map never generates a chunk. On a live map, ground nobody has generated is transparent.
  • A live map is behind the world by up to about twice redraw-seconds; players by up to interval-seconds; claims by up to a minute.
  • Under heavy load a tile request may be answered 503 and retried by the page.
  • At most 32 connections are served at once.
  • A plugin may keep at most 512 markers on a map.

Security and privacy

  • No login. The map has no accounts of its own. With the default host: 0.0.0.0 anyone who can reach the port sees it. Bind 127.0.0.1 behind a reverse proxy, or firewall the port, to keep it private. per-player fog sign-in decides only whose fog a viewer sees, not whether they may see the map.
  • A live map shows what is built, by design. Do not run one where a base on the map is a base raided; use a drawn-once map there, and leave players off.
  • Never exposed, whatever the settings: players in other worlds or outside the area, anyone's height, inventory, health, IP address or account id, who has chosen /map hide, and who is trusted in a claim. Hidden ground under fog is never sent at all.
  • Fog sign-in codes are one-time, short-lived, and kept out of request lines and logs; session cookies are HttpOnly and Secure, and sessions live only in memory.
  • The page loads nothing from elsewhere but players' heads from mc-heads.net, and only when players are shown.

For plugin authors

render_web_map(world) -> Result<bool, String>, in the guest SDK as rustmc_guest::host::render_web_map(world), asks an on-plugin-signal map to be drawn:

  • Ok(true): drawing started;
  • Ok(false): the map is drawn or being drawn already, or the server draws maps itself (on-create or live);
  • Err: the web map is off, or it does not draw that world (only the overworld is drawn).

Call it once everything that belongs on the map stands, before players have built anything: the map is never redrawn. Calling it again is harmless.

web_map_marker(world, id, label, x, z, kind) -> Result<bool, String>, in the guest SDK as rustmc_guest::host::web_map_marker(world, id, label, x, z, kind), with kind one of landmark, monument and point:

  • Ok(true): the marker is new, or moved, renamed or of another kind;
  • Ok(false): it was so already, and nothing was written, so a plugin may set its markers again at every start;
  • Err: the web map is off, it does not draw that world, or the marker is refused: an id that is not 1 to 64 of [A-Za-z0-9_.:-], a label blank, longer than 64 characters or holding a control character, a position more than 30,000,000 blocks out, or a 513th marker of this plugin's on the map.

web_map_remove_marker(world, id) -> Result<bool, String> takes one off: Ok(true) when the plugin had it. A plugin's markers are its own: another plugin may use the same id.

The claims layer is read from the claims plugin's claims service; see the plugin API.

Without a server

render-web-map draws a drawn-once map's images from a world on disk:

cargo run --release -p rustmc-server --features generation-rusty-island --bin render-web-map -- \
    <world> <out> [--generator NAME] [--source terrain|world] [--radius BLOCKS] [--centre X Z] [--threads N]

Run it on a copy of a world a server is using, never the world itself.

From docs/web-map.md in the RustMC repository at bbd81209ad.