Skip to content
Basalt Server

Installation

Downloading, verifying and starting Basalt Server, its command line flags, first-start files, systemd and Docker.

Basalt ships as a single native binary per platform. It needs no runtime beside it.

Download#

Releases are published on GitHub at BasaltProject/Server. Each asset is the executable itself, named after the build:

BasaltServer-<version>-<Platform>-<arch>
Platform Architectures
Linux x64, arm64
macOS x64, arm64 (Apple silicon)
Windows x64, arm64
Linux-musl (Alpine) x64, arm64
Android (Android 10+, API 29) x64, arm64
FreeBSD (14.5+) x64, arm64

The musl builds are fully static and need neither glibc nor C++ runtime packages. Android and FreeBSD builds include the C++ runtime statically. Android, FreeBSD and musl builds include the native core and Lua plugins, but not Bukkit/Shim.

There are two channels:

Channel When it is published
Release Weekly, on Monday at 09:00 in Madrid, when something changed since the last one
nightly prerelease On every push to main. It is one moving release that the next push replaces

Every release carries a SHA256SUMS file next to the binaries. Download the binary for your platform and the checksum file, then verify before running:

bash
sha256sum --check --ignore-missing SHA256SUMS

On macOS use shasum -a 256 -c SHA256SUMS --ignore-missing.

Start it#

Put the binary in an empty directory of its own: everything the server writes goes into the directory you start it from.

bash
chmod +x BasaltServer-*
./BasaltServer-26.3.0-Linux-x64

The first start does not launch a server, see the EULA below.

Android#

The Android download is a native NDK executable, not an APK and not a Linux/glibc binary. It needs no Java runtime or libc++_shared.so alongside it.

Use an executable directory, such as a compatible terminal's private home directory or /data/local/tmp through adb shell. Shared storage such as /sdcard is mounted without execution permission.

For an arm64 device, rename the downloaded Android-arm64 executable to basalt on your computer, then:

bash
adb push basalt /data/local/tmp/basalt
adb shell 'cd /data/local/tmp && chmod +x basalt && ./basalt'

Edit the generated eula.toml to accept the EULA before starting again. For this adb shell deployment, neither root access nor an APK is required. Keep the working directory consistent: configuration and worlds are written there.

Android's battery and background-process restrictions still apply. A command-line executable does not become an Android foreground service or keep the device awake automatically.

Command line flags#

There are exactly two flags. Everything else is configuration, in config/.

Flag Meaning
--accept-eula Counts as accepting the Minecraft EULA for this run, skipping the eula.toml check
--safeMode Starts with vanilla's data packs only, ignoring the ones enabled for the world. Use it to recover from a data pack that stops the server loading

If a data pack fails validation at startup, the server logs the errors and falls back to vanilla alone by itself, as if --safeMode had been passed.

Exit codes#

Stable, so a supervisor can tell them apart from a crash.

Code Meaning
0 Normal stop
2 A configuration file is malformed or holds an invalid value. The log names the file
3 The server could not start, for example a port that is already in use
4 players.db could not be opened
78 The EULA has not been accepted

The EULA#

The server never prompts. It behaves the same on a terminal, under systemd and in a container.

  1. On start it looks for eula.toml in the working directory.
  2. If there is none but an accepted eula.txt exists (from vanilla, Paper or a hosting panel), it records the acceptance in eula.toml once and carries on. eula.txt is never read again.
  3. Otherwise it writes eula.toml with eula = false, logs a message and exits with code 78.
  4. With eula = true in eula.toml it continues. eula.toml always wins over eula.txt.
toml
# eula.toml
eula = true

--accept-eula skips all of that. Typing it is a deliberate act and counts as accepting the Minecraft EULA; it exists for immutable or non-interactive environments where editing a file is awkward.

First start#

Once the EULA is accepted the server downloads the official server archive from Mojang, verifies its SHA-1 and unpacks the data it needs into .basalt/assets/<version>/. Later starts of the same version do not touch the network for this. Then it writes its configuration and creates the default world.

eula.toml        the EULA acceptance
config/          one TOML file per domain, generated with comments
worlds/          one directory per world: worlds/<name>/region/, settings.toml, mobs.toml
plugins/         .bspkg packages
players.db       player data, permission group membership, whitelist, bans
.basalt/         state the server owns:
  assets/          the unpacked Mojang data
  logs/            latest.log and archive/ (see logging in Server settings)
  remote-token     the bearer token of the management API

config/ is what you edit. players.db and .basalt/ are state: back them up, but do not hand-edit them. Player data deliberately lives outside the worlds, so a world can be deleted, copied or reset without touching anyone's inventory. See Worlds.

Coming from vanilla or Paper

If there is a server.properties and no config/server.toml on the first start, the values Basalt recognises are imported once, with a line in the log, and the file is not read again. The motd goes to the server list profile. The view distance is not imported: it is a per-world setting in worlds.toml. An Anvil world copied into worlds/ is read as it is, see Worlds.

Ports#

Port Protocol Used for Default
25565 TCP Java Edition on
19132 UDP Bedrock Edition on
25565 UDP GameSpy 4 query off
25575 TCP RCON, plaintext off
25580 TCP Management API on, bound to loopback only

Open only what you use. All of them are set in server.toml.

Running under systemd#

ini
# /etc/systemd/system/basalt.service
[Unit]
Description=Basalt Server
After=network-online.target
Wants=network-online.target

[Service]
User=basalt
WorkingDirectory=/srv/basalt
ExecStart=/srv/basalt/BasaltServer-26.3.0-Linux-x64
Restart=on-failure
RestartPreventExitStatus=2 3 4 78
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

RestartPreventExitStatus keeps systemd from restart-looping on a configuration mistake or a missing EULA. Accept the EULA once with --accept-eula in ExecStart, or by editing eula.toml. A stop signal saves and shuts the server down like /stop.

With no terminal there is no console. Run commands through the management API or RCON (Operations).

Running in Docker#

The binary is a single file, so the image is a small base with CA certificates (the first start talks HTTPS to Mojang) and the binary. Use a glibc-based base image for Linux assets, or an Alpine base image for the fully static Linux-musl assets.

dockerfile
FROM debian:stable-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates \
 && rm -rf /var/lib/apt/lists/* \
 && useradd --system --create-home --home-dir /data basalt
COPY BasaltServer-26.3.0-Linux-x64 /usr/local/bin/basalt
RUN chmod +x /usr/local/bin/basalt
USER basalt
WORKDIR /data
VOLUME /data
EXPOSE 25565/tcp 19132/udp
ENTRYPOINT ["/usr/local/bin/basalt", "--accept-eula"]
yaml
# compose.yaml
services:
  basalt:
    build: .
    restart: unless-stopped
    ports:
      - "25565:25565"
      - "19132:19132/udp"
    volumes:
      - ./data:/data
    stdin_open: true
    tty: true

Everything the server writes goes to /data, so that one volume holds the configuration, worlds, plugins and state. stdin_open and tty let you reach the console with docker attach; leave them out if you drive the server through the API.

The management API in a container

The API listens on loopback inside the container, so publishing its port reaches nothing. To use it from outside, set bind in [network.api] and keep it on a private network or behind TLS. See Operations.

Updating#

  1. Stop the server with /stop.
  2. Back up the directory (see Worlds).
  3. Replace the binary with the new one and start it.

Configuration carries over. Options an update introduces are appended to the end of the existing file as commented-out lines with their defaults, and the log lists them, so nothing changes until you uncomment one. See Configuration.

Building from source#

You need CMake 3.28 or newer, Ninja and Clang. Every dependency is vendored.

bash
git clone --recurse-submodules https://github.com/BasaltProject/Server.git
cd Server
cmake --preset release
cmake --build build/release

The shared network layer lives in a submodule, so clone with --recurse-submodules (or run git submodule update --init in an existing clone). The binary lands in build/release/bin/ with its download name.

Cross-compile for Android#

Install Android NDK r29 (29.0.14206865) or newer and set ANDROID_NDK_HOME to its directory. NDK r28's libc++ lacks floating-point std::from_chars, which the server requires.

bash
cmake --preset android-arm64
cmake --build --preset android-arm64 --target basalt-server

Use android-x64 instead for an x86_64 device or emulator. The presets select the official NDK toolchain, API 29 and static libc++; they do not inherit the host compiler. The executable lands in build/android-arm64/bin/ or build/android-x64/bin/. The native asynchronous system resolver requires API 29 and works without a Java ConnectivityManager.

Build on Alpine#

On Alpine 3.22, install the compiler, headers and static C++ runtime:

bash
apk add cmake ninja clang20 lld20 libc++-dev libc++-static \
  llvm-libunwind-dev llvm-libunwind-static gcc g++ musl-dev linux-headers
cmake --preset linux-musl -DCMAKE_C_COMPILER=clang-20 \
  -DCMAKE_CXX_COMPILER=clang++-20 -DCMAKE_CXX_FLAGS=-stdlib=libc++ \
  "-DCMAKE_EXE_LINKER_FLAGS=-nostdlib++ -fuse-ld=lld" \
  "-DCMAKE_CXX_STANDARD_LIBRARIES=-lc++ -lc++abi -lunwind" \
  -DCMAKE_TRY_COMPILE_PLATFORM_VARIABLES=CMAKE_CXX_STANDARD_LIBRARIES
cmake --build --preset linux-musl
ctest --preset linux-musl

The result is in build/linux-musl/bin/. BASALT_LINUX_MUSL selects fully static linkage and a distinct filename; it does not turn a glibc compiler into a musl cross-compiler.

Build on FreeBSD#

FreeBSD 14.5 includes Clang and libc++ 21, which support the C++23 features Basalt requires. Install cmake and ninja through pkg, then build natively. These flags link the base system's libc++ and libcxxrt statically, matching the release:

bash
cmake --preset freebsd -DCMAKE_EXE_LINKER_FLAGS=-nostdlib++ \
  "-DCMAKE_CXX_STANDARD_LIBRARIES=/usr/lib/libc++.a /usr/lib/libcxxrt.a -lpthread" \
  -DCMAKE_TRY_COMPILE_PLATFORM_VARIABLES=CMAKE_CXX_STANDARD_LIBRARIES
cmake --build --preset freebsd
ctest --preset freebsd

The result is in build/freebsd/bin/. For arm64, CI cross-compiles against an official FreeBSD sysroot and then runs the full test suite and executable in a FreeBSD arm64 VM. The exact flags are in the build workflow.

Edit this page