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:
sha256sum --check --ignore-missing SHA256SUMSOn 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.
chmod +x BasaltServer-*
./BasaltServer-26.3.0-Linux-x64The 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:
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.
- On start it looks for
eula.tomlin the working directory. - If there is none but an accepted
eula.txtexists (from vanilla, Paper or a hosting panel), it records the acceptance ineula.tomlonce and carries on.eula.txtis never read again. - Otherwise it writes
eula.tomlwitheula = false, logs a message and exits with code78. - With
eula = trueineula.tomlit continues.eula.tomlalways wins overeula.txt.
# 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 APIconfig/ 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#
# /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.targetRestartPreventExitStatus 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.
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"]# compose.yaml
services:
basalt:
build: .
restart: unless-stopped
ports:
- "25565:25565"
- "19132:19132/udp"
volumes:
- ./data:/data
stdin_open: true
tty: trueEverything 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#
- Stop the server with
/stop. - Back up the directory (see Worlds).
- 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.
git clone --recurse-submodules https://github.com/BasaltProject/Server.git
cd Server
cmake --preset release
cmake --build build/releaseThe 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.
cmake --preset android-arm64
cmake --build --preset android-arm64 --target basalt-serverUse 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:
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-muslThe 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:
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 freebsdThe 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.