A tiny sleep/wake wrapper for Minecraft Java servers, on Pterodactyl or in Docker.
Sleepy listens on the public port and keeps the server turned off while nobody is playing. It starts the server when a player connects and stops it again after a period of inactivity. While the server sleeps, only Sleepy runs: about 4 MB of RAM (6 MB measured in Docker), for a ~4 MB static binary.
It works with every Java client: a server-list ping never wakes the server up, and the server never shows up as incompatible while it sleeps. Modded servers (NeoForge, Forge, Fabric, Arclight...) are supported: players are kept waiting through a start of several minutes.
A small server for a group of friends is empty most of the day, yet a Java server idles at several gigabytes of RAM. On a VPS or a Pterodactyl panel, that memory is paid for 24/7. Sleepy gives it back whenever nobody is online, without players having to do anything special: they just connect, wait, and play.
Coming soon.
backend.type in sleepy.toml says how Sleepy runs the Minecraft server:
| Type | Use it when | Sleepy... |
|---|---|---|
process |
Pterodactyl, systemd, screen, any machine where Sleepy can run Java itself | runs backend.command, writes server-port/server-ip in server.properties, stops with stop then SIGTERM |
docker |
the server already runs in its own container (itzg/minecraft-server...) | starts and stops that container through the Docker API, never touches its files |
On first launch Sleepy writes a commented sleepy.toml and exits. The type is detected then:
process on Pterodactyl, docker when a Docker API is reachable (DOCKER_HOST or
/var/run/docker.sock), process otherwise. Check the file, then start Sleepy again.
Written in Go. If Go is not installed, the scripts fall back to the golang Docker image.
./test.sh # go vet + tests (Linux)
./build.sh # -> dist/sleepy-v<VERSION>: static Linux x64 binary, runs on any x86-64 CPU
docker build -t sleepy . # the Docker imageDesigned for the standard Minecraft Java egg (image ghcr.io/pterodactyl/yolks:java_25).
-
Upload the binary
dist/sleepy-v0.x.yto the server root, rename it tosleepy, then make it executable: in the file manager, open the file's⋯menu → Permissions →755. Uploading clears the executable bit, so repeat this step after every new upload. -
Egg → Configuration Files: replace the content with
{}. Otherwise Pterodactyl rewritesserver-portinserver.propertieson every start, while Sleepy sets it to the local port. -
Egg → Start Configuration:
{"done": "[sleepy] Listening on "} -
Egg → Stop Command:
^C -
Server startup command:
./sleepyDo not chain other commands (for example
chmod +x sleepy && ./sleepy): the yolks entrypoint does not run the command through a shell, and&&breaks the console and the "done" detection. -
First start: Sleepy writes
sleepy.tomlwith commented defaults, then exits. Checkbackend.command(jar name, memory), then start again.
When public_address is empty, the port allocated by Pterodactyl is read from the SERVER_PORT
variable. The Minecraft server itself listens on backend.address (127.0.0.1:25566) and is not
reachable from outside.
Sleepy runs in its own container, next to the server container, on a shared Docker network.
examples/docker-compose.yml and
examples/sleepy.docker.toml are a complete setup.
- Server container: remove its
ports:block (Sleepy publishes 25565 now), put it on a network Sleepy shares (webin the example), and give itstdin_open: trueso console commands reach it. Itsserver-portstays whatever it is (25566in the example): Sleepy targetsbackend.address = "mc:25566". Avoidrestart: always, which would restart a container Sleepy put to sleep;unless-stoppedis fine. - Docker API access: the example gives Sleepy a
docker-socket-proxy allowing only inspect,
start and stop (plus attach for console commands), so Sleepy can run as an unprivileged user
(
user: "1000:1000"). Sleepy can also use the socket directly (/var/run/docker.sock:/var/run/docker.sockandgroup_addwith the socket's group). - Configuration:
/opt/sleepy(mounted as/data, owned by the container user) holdssleepy.tomlandsleepy-state.json. Start once to get the template, or copy the example. docker compose up -d, thendocker logs -f sleepy.
How it behaves:
- A player connects →
POST /containers/mc/start. Ready as soon as the server answers a status ping onbackend.address. - Idle →
POST /containers/mc/stop?t=<stop_timeout>, likedocker stop --timeout: the container gets its stop signal (itzg: a cleanstopwithSTOP_DURATION), then SIGKILL from Docker only afterstop_timeout. Sleepy never kills the server itself. - The container started by someone else (a panel,
docker start mc, Sleepy restarted while it runs) is taken over within 5 seconds: it is shown as awake and put to sleep like any other. - Stopped by someone else: Sleepy notices within 2 seconds and goes back to sleeping.
- Sleepy itself stopping or restarting leaves the container as it is.
- The server's files are never written.
backend.directoryis optional and read-only: without the server files, the icon and max players shown while asleep come from the status Sleepy remembers.
The console: docker attach sleepy (detach with Ctrl+P Ctrl+Q). Lines that are not sleepy ...
commands go to the server's stdin while it runs.
sleepy.toml in the current directory. All paths are relative to that directory.
To write it without mistakes, open tools/config-generator.html in a browser. This offline page
offers a form, an in-game preview of the MOTD, validation, import of an existing file, and a
download of the resulting sleepy.toml.
| Key | Default | Purpose |
|---|---|---|
public_address |
"" |
Listen address ip:port. Empty = 0.0.0.0:$SERVER_PORT (25565 if unset). |
kick_message |
"The server is starting, reconnect in a few seconds" | Sent if the server is not ready in time, or to a non-whitelisted player with whitelist_only. |
hold_timeout |
180 |
Seconds a connecting player is held while the server starts. See Long starts. |
sleep_after |
300 |
Seconds without players before the server is stopped. |
minimum_online_time |
60 |
Minimum running time before an idle stop. |
stop_timeout |
60 |
Seconds the server gets to stop. process: after stop, then SIGTERM, then SIGKILL 10 s later. docker: the Docker stop timeout. |
whitelist_only |
false |
Only players in whitelist.json can wake the server. |
busy_command |
"" |
Run before an idle stop; exit code 0 = busy, the stop is postponed. See below. |
busy_retry |
900 |
Seconds between two checks while busy_command says busy. |
busy_max_delay |
10800 |
The stop happens anyway after being postponed this long. |
backend.type |
"process" |
"process" or "docker", see above. |
backend.address |
"127.0.0.1:25566" |
Where the server is reached. process: written to server.properties. docker: e.g. "mc:25566". |
backend.directory |
"." |
Server directory (server.properties, whitelist.json, server-icon.png). Read only with docker. |
backend.proxy_protocol |
"auto" |
Forward the players' real IP (PROXY protocol): auto, on or off. See below. |
backend.command |
java -Xms128M -XX:MaxRAMPercentage=85.0 -jar server.jar nogui |
process: start command, run through sh (variables such as ${SERVER_MEMORY} are expanded). |
backend.container |
"mc" |
docker: name or ID of the server container. |
backend.docker_endpoint |
"" |
docker: unix:///path or tcp://host:port. Empty = $DOCKER_HOST, else /var/run/docker.sock. |
motd.source |
"sleepy" |
MOTD shown while asleep and starting: "sleepy" (the messages below) or "server" (the real server MOTD, see below). |
motd.sleeping |
"Server is asleep - join to wake it up" | MOTD shown while the server sleeps. Supports § codes and \n. |
motd.starting |
"Server is starting..." | MOTD shown while the server starts. |
motd.stopped |
"Server stopped by an administrator" | MOTD shown while [power] keeps the server stopped. |
motd.version_name |
"Sleepy" |
Version name shown in the server list. |
power.directory |
"" |
State directory shared with a panel and scripts. Empty = off. See below. |
power.stopped_message, power.backup_message, power.restore_message |
English messages | Sent to players who cannot wake the server for that reason. |
With process, before each start Sleepy writes server-port and server-ip from
backend.address to server.properties, leaving every other line untouched. max-players is
read from it for the server list.
A Minecraft client gives up on a connection that stays silent for 30 seconds, which is shorter than
the start of most modded servers. While a 1.13+ player waits, Sleepy sends it a login plugin request
every 10 seconds on a channel nobody knows (sleepy:wait); the client answers "not understood",
and its timeout starts over. Once the server is ready, Sleepy drops those answers and hands the
connection over as if nothing happened. The player sees "Logging in..." for the whole start.
Clients before 1.13 do not have this packet: they are held for at most 25 seconds, then asked to reconnect.
With motd.source = "server", the server list shows the Minecraft server's own MOTD, even while it
sleeps. Sleepy reads it from the server while it runs (as soon as it is ready, then every minute),
with its colors (hex included) and any changes made by plugins, and stores it in
sleepy-state.json together with the server icon and max players. Before the very first start, it
uses motd= from server.properties.
power.directory is a directory Sleepy shares with a control panel and maintenance scripts. Files
written by others are read on every check; writes are expected to be atomic (write, then rename).
| File | Written by | Effect |
|---|---|---|
admin.json |
panel, admin | {"mode": "stopped"}: an administrator stopped the server. Sleepy never wakes it, shows motd.stopped and sends stopped_message. {"mode": "auto"} or no file: normal. |
backup.lock |
backup script | While present, Sleepy does not start the server (backup_message). |
restore.lock |
restore job | Same, with restore_message. Locks older than 6 hours are ignored (crashed script). |
keep-awake |
panel, scripts | {"until": "2026-10-28T04:00:00+01:00", "reason": "map render"}: no idle stop before that date. |
sleepy.json |
Sleepy | {"state": "awake" | "sleeping" | "waking", "since", "heartbeat", "version"}, rewritten on every change and at least every 30 seconds. waking is a start triggered by a player or the console. |
The console commands start and sleepy wake respect the locks and admin.json too.
When the server is about to sleep, Sleepy runs busy_command through sh. Exit code 0 means busy
(a map render, a long task): the stop is retried every busy_retry seconds, and happens anyway once
it has been postponed for busy_max_delay. The first line the command prints is logged as the
reason. A command running for more than a minute counts as "not busy".
Without it, the server sees every player coming from Sleepy's address (127.0.0.1, or the Sleepy
container's IP with docker). Logs, /ban-ip and IP-based plugins then stop working, and
Bukkit-based servers (Spigot, Arclight...) may throttle players joining close together: set
connection-throttle: -1 in bukkit.yml there.
auto(default): Paper and its forks (config/paper-global.yml). Withprocess, Sleepy setsproxies.proxy-protocoltotruethere before each start, then forwards the IP; the file only exists after Paper's first start, so the IP is forwarded from the following start onwards. Withdocker, Sleepy forwards the IP if the file (inbackend.directory) enables it. On other servers (vanilla, Spigot, Fabric, NeoForge...), nothing changes.on: always forward the IP. The server must expect it (for example through a mod), otherwise every connection fails.off: never forward the IP. Withprocess, also setsproxy-protocolback tofalseinpaper-global.ymlif present.
Only expose the server to Sleepy (127.0.0.1, or a Docker network without published ports): a
server that accepts the PROXY header from anyone accepts spoofed IPs.
sleepy ... commands are handled by Sleepy and never forwarded to the server, whether it is
running or not:
| Command | Effect |
|---|---|
sleepy status |
State, connected players, auto-sleep, what blocks a wake-up, IP forwarding, backend. |
sleepy off |
Disables auto-sleep: the server stays on. Survives restarts (sleepy-state.json). |
sleepy on |
Re-enables auto-sleep. |
sleepy sleep |
Stops the server now. |
sleepy wake |
Starts the server now. |
sleepy reload |
Re-reads sleepy.toml. Backend settings apply on the next server start; public_address and power.directory require restarting Sleepy. |
sleepy help |
Lists the commands. |
Otherwise:
- Server running: the console behaves exactly as without Sleepy. Everything goes to the
server;
stopgoes through Sleepy to keep its stop sequence. - Server stopped:
startstarts the server,stopexits Sleepy,statusshows the state,helplists the commands.
Sleepy's own lines are prefixed with [sleepy].
./test.sh -race # unit tests and integration tests (fake server, fake Docker API)
./test.sh -run Proxy # a subsetIntegration tests re-run the test binary itself as a fake Minecraft server, and serve a fake Docker
Engine API whose container is that fake server (internal/testutil).
Layout: internal/config (TOML), internal/protocol (VarInt, packets, legacy ping, PROXY v2
header), internal/proxy (listener, status, hold, relay), internal/backend (states, process and
container instances, server.properties, paper-global.yml, whitelist), internal/docker
(minimal Docker API client), internal/power (shared state directory), internal/settings
(reloadable settings, persistent state), internal/console, internal/idle, main.go.
The version is defined only in the VERSION file.
MIT © JonesJugHead