Skip to content

Docker auto-scale and asleep motd status#488

Merged
itzg merged 19 commits intoitzg:mainfrom
Lenart12:main
Dec 20, 2025
Merged

Docker auto-scale and asleep motd status#488
itzg merged 19 commits intoitzg:mainfrom
Lenart12:main

Conversation

@Lenart12
Copy link
Contributor

@Lenart12 Lenart12 commented Dec 9, 2025

No description provided.

Copilot AI review requested due to automatic review settings December 9, 2025 05:19
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Docker auto-scale functionality to enable scale-to-zero behavior for Docker containers, similar to the existing Kubernetes auto-scale feature. The implementation allows mc-router to automatically start/unpause stopped containers when clients connect and gracefully stop containers after a configurable idle period.

Key changes:

  • Refactored ScalerFunc into WakerFunc (returns endpoint string) and SleeperFunc to support dynamic endpoint resolution after container startup
  • Implemented Docker container start/stop/unpause operations with health check waiting
  • Added per-container auto-scale label overrides (mc-router.auto-scale-up and mc-router.auto-scale-down)

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
server/routes.go Refactored type signatures from ScalerFunc to WakerFunc/SleeperFunc; added guard to prevent downscaler activation on empty backends
server/k8s.go Updated K8s integration to wrap scale-up function and return endpoint; updated type signatures
server/docker_swarm.go Updated Docker Swarm signatures to match new types (auto-scale not yet implemented for Swarm)
server/docker.go Implemented full Docker auto-scale with container start/stop/unpause, health checking, and route updates; added global state for container tracking
server/connector.go Updated waker invocation to handle returned endpoint and improved error handling/logging
server/server.go Extended downscaler enablement check to include Docker environments
server/api_server.go Updated API route creation to use new empty function names
README.md Added comprehensive documentation for Docker auto-scale feature with examples

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Lenart12 Lenart12 requested a review from Copilot December 9, 2025 14:04
@Lenart12 Lenart12 changed the title Docker auto-scale Docker auto-scale and asleep motd status Dec 9, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Lenart12
Copy link
Contributor Author

Lenart12 commented Dec 9, 2025

This is ready for review. I didn't notice #406 is trying to merge about the same functionality.

@itzg
Copy link
Owner

itzg commented Dec 9, 2025

Thanks. I was about to ask if you were finished addressing the copilot feedback.

Don't worry about overlap with the other PR. Yours has a more focused scope that addresses a common and immediate need. The other PR grew a bit broad, which is partly why it is parked there (and because I keep forgetting about it).

@Lenart12
Copy link
Contributor Author

Added a fix that showed default asleep motd on nonexistent routes.

@itzg
Copy link
Owner

itzg commented Dec 14, 2025

Sorry for the delay with me reviewing this. I've set a goal for myself to look over it today. When I skimmed the changes they looked good, but wanted to take a better look while looking on a computer monitor.

Copy link
Owner

@itzg itzg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great contribution. Just some nitpicky things.

mcproto/types.go Outdated
PacketIdLogin = 0x00 // during StateLogin
PacketIdLegacyServerListPing = 0xFE
PacketIdStatusRequest = 0x00
PacketIdStatusPing = 0x01
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please call this PacketIdPingRequest to match the docs naming convention:

https://minecraft.wiki/w/Java_Edition_protocol/Packets#Ping_Request_(status)

I wasn't able to find a "status ping" when searching the page as is.

mcproto/types.go Outdated

// PingPayload represents the status ping payload (packet 0x01)
type PingPayload struct {
Value int64
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field name should be Timestamp, right? According to

https://minecraft.wiki/w/Java_Edition_protocol/Packets#Ping_Request_(status)

mcproto/read.go Outdated
case PacketIdStatusPing:
// read 8-byte long from remainder
if remainder.Len() >= 8 {
val := int64(binary.BigEndian.Uint64(remainder.Next(8)))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the existing ReadLong function in this package

mcproto/write.go Outdated
Comment on lines 54 to 71
// StatusResponse is a minimal structure for the status JSON
type StatusResponse struct {
Version struct {
Name string `json:"name"`
Protocol int `json:"protocol"`
} `json:"version"`
Players struct {
Max int `json:"max"`
Online int `json:"online"`
Sample []struct {
Name string `json:"name"`
ID string `json:"id"`
} `json:"sample,omitempty"`
} `json:"players"`
Description map[string]interface{} `json:"description"`
Favicon string `json:"favicon,omitempty"`
EnforcesSecureChat *bool `json:"enforcesSecureChat,omitempty"`
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to types.go

mcproto/write.go Outdated
if err := WriteString(&payload, jsonString); err != nil {
return err
}
pkt := buildPacket(0x00, payload.Bytes())
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declare const for the packet ID 0x00 here

mcproto/write.go Outdated
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], uint64(payload))
pl.Write(buf[:])
pkt := buildPacket(0x01, pl.Bytes())
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declare packet ID const

logrus.WithFields(logrus.Fields{
"client": frontendConn.RemoteAddr(),
"error": err,
}).Debug("Predefined status: error reading initial status packet")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps should be Warn level

server/docker.go Outdated
client *client.Client
config dockerWatcherConfig
client *client.Client
dockerRoutesLock sync.RWMutex
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering why the extra mutex? It looks like the mutex field on L64 isn't even used, so perhaps removing that is the cleaner solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover from initial development... whoops

server/docker.go Outdated
}
if key == DockerRouterLabelAutoScaleUp {
lowerValue := strings.TrimSpace(strings.ToLower(value))
data.autoScaleUp = lowerValue != "" && lowerValue != "0" && lowerValue != "false" && lowerValue != "no"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to create a helper function for this and then use the same at

mc-router/server/k8s.go

Lines 287 to 291 in 22ec39b

enabled, exists := service.Annotations[AnnotationAutoScaleUp]
if exists {
if enabled == "false" {
return nil
}

and below at L385

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used strconv.ParseBool instead of a new helper function.

@Lenart12 Lenart12 requested a review from itzg December 16, 2025 16:05
@itzg
Copy link
Owner

itzg commented Dec 20, 2025

While doing some sanity testing, using examples/docker-discovery/compose.yml, I thought I'd change the mc-router.host to a usable hostname and did a docker compose up. That recreated/started both servers there. It seems like after that it detects the new container instance but isn't able to verify connectivity after "waking it up"

time="2025-12-20T08:18:55-06:00" level=debug msg="Got connection" client="127.0.0.1:57509"
time="2025-12-20T08:18:55-06:00" level=debug msg="Read frame length" client="127.0.0.1:57509" length=24
time="2025-12-20T08:18:55-06:00" level=debug msg="Reading frame content" client="127.0.0.1:57509" length=24 total=24
time="2025-12-20T08:18:55-06:00" level=debug msg="Read frame" client="127.0.0.1:57509" frame="Frame:[len=24, payload=0X008606116C6F63616C686F73742E69747A672E6D6563DD02]"
time="2025-12-20T08:18:55-06:00" level=debug msg="Read packet" client="127.0.0.1:57509" packet="Frame:[len=25, packetId=0, data=0X8606116C6F63616C686F73742E69747A672E6D6563DD02]"
time="2025-12-20T08:18:55-06:00" level=debug msg="Got packet" client="127.0.0.1:57509" length=25 packetID=0
time="2025-12-20T08:18:55-06:00" level=debug msg="Got handshake" client="127.0.0.1:57509" handshake="&{774 localhost.itzg.me 25565 2}"
time="2025-12-20T08:18:55-06:00" level=debug msg="Read frame length" client="127.0.0.1:57509" length=22
time="2025-12-20T08:18:55-06:00" level=debug msg="Reading frame content" client="127.0.0.1:57509" length=22 total=22
time="2025-12-20T08:18:55-06:00" level=debug msg="Read frame" client="127.0.0.1:57509" frame="Frame:[len=22, payload=0X000469747A675CDDFD26FC864981B52EC42BB10BFDEF]"
time="2025-12-20T08:18:55-06:00" level=debug msg="Read packet" client="127.0.0.1:57509" packet="Frame:[len=23, packetId=0, data=0X0469747A675CDDFD26FC864981B52EC42BB10BFDEF]"
time="2025-12-20T08:18:55-06:00" level=debug msg="Got user info" client="127.0.0.1:57509" player=itzg/5cddfd26-fc86-4981-b52e-c42bb10bfdef
time="2025-12-20T08:18:55-06:00" level=debug msg="Finding backend for server address" serverAddress=localhost.itzg.me
time="2025-12-20T08:18:55-06:00" level=debug msg="checked if player is allowed to wake up the server" client="127.0.0.1:57509" player=itzg/5cddfd26-fc86-4981-b52e-c42bb10bfdef server=localhost.itzg.me serverAllowsPlayer=true
time="2025-12-20T08:18:55-06:00" level=info msg="Waking up backend server" serverAddress=localhost.itzg.me
time="2025-12-20T08:19:17-06:00" level=error msg="failed to wake up backend" error="timeout waiting for container to become reachable at 172.18.0.2:25565" serverAddress=localhost.itzg.me
time="2025-12-20T08:19:17-06:00" level=info msg="Closed connection to backend" backendHostPort="172.18.0.2:25565" client="127.0.0.1:57475" connectionCount=0
time="2025-12-20T08:19:17-06:00" level=debug msg="Beginning scale down" backendEndpoint="172.18.0.2:25565"
time="2025-12-20T08:19:17-06:00" level=debug msg="Closing frontend connection" client="127.0.0.1:57475"
time="2025-12-20T08:19:56-06:00" level=error msg="failed to wake up backend" error="timeout waiting for container to become reachable at 172.18.0.2:25565" serverAddress=localhost.itzg.me
time="2025-12-20T08:19:56-06:00" level=info msg="Closed connection to backend" backendHostPort="172.18.0.2:25565" client="127.0.0.1:57509" connectionCount=0
time="2025-12-20T08:19:56-06:00" level=debug msg="Beginning scale down" backendEndpoint="172.18.0.2:25565"
time="2025-12-20T08:19:56-06:00" level=debug msg="Closing frontend connection" client="127.0.0.1:57509"

@itzg
Copy link
Owner

itzg commented Dec 20, 2025

Disregard that comment. My testing scenario was flawed since I'm running mc-router outside of docker.

itzg
itzg previously approved these changes Dec 20, 2025
Copy link
Owner

@itzg itzg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent! Thanks.

@itzg
Copy link
Owner

itzg commented Dec 20, 2025

Can you take a look at the unit test failure:

https://github.com/itzg/mc-router/actions/runs/20274437941/job/58612175899?pr=488

@Lenart12
Copy link
Contributor Author

Tests were failing because some method signatures changed and were used for running k8s tests. I checked all tests now pass and I have also added some Docker compose examples.

@Lenart12 Lenart12 requested a review from itzg December 20, 2025 16:19
@itzg itzg merged commit 4dff00d into itzg:main Dec 20, 2025
4 checks passed
@itzg
Copy link
Owner

itzg commented Dec 20, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants