Fix Docker deployment connectivity issues (#4795)

* Fix Docker deployment connectivity issues

- Shardok: Listen on 0.0.0.0:40042 instead of localhost for container networking
- Shardok: Add env var fallback for resource paths (SHARDOK_RESOURCES_PATH, SHARDOK_MAPS_PATH)
- Eagle: Use plaintext gRPC for internal container-to-container communication
- docker-compose: Pass CLI args directly instead of env vars, default to gpt-5.1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Document Docker networking and resource configuration

Added section explaining:
- Why Shardok binds to 0.0.0.0 instead of localhost for container networking
- Why Eagle uses .usePlaintext() for internal gRPC
- How env var fallbacks replace Bazel runfiles in Docker
- Future considerations for multi-host deployment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-24 10:41:53 -08:00
committed by GitHub
co-authored by Claude Opus 4.5
parent 331702bf3c
commit 1433f3d29c
5 changed files with 87 additions and 7 deletions
+5 -5
View File
@@ -11,15 +11,15 @@ services:
eagle:
image: ${EAGLE_IMAGE:-registry.digitalocean.com/eagle0/eagle-server:latest}
container_name: eagle-server
command:
- "--gpt-model-name"
- "${GPT_MODEL_NAME:-gpt-5.1}"
- "--shardok-interface-remote-address"
- "shardok:40042"
ports:
- "40032:40032"
environment:
EAGLE_GRPC_PORT: "40032"
SHARDOK_HOST: "shardok"
SHARDOK_PORT: "40042"
EAGLE_RESOURCES_PATH: "/app/resources"
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
GPT_MODEL_NAME: "${GPT_MODEL_NAME:-gpt-4o}"
volumes:
- ./saves:/app/saves
depends_on:
+62
View File
@@ -348,6 +348,68 @@ services:
---
## Docker Networking & Resource Configuration
### Container Network Binding
In Docker, each container has its own network namespace. When a server binds to `localhost` (127.0.0.1), it only accepts connections from within the same container. Other containers on the Docker bridge network cannot connect, even if they can resolve the hostname.
**Shardok must bind to `0.0.0.0:40042`** to accept connections from Eagle running in a separate container. This is configured in `ServerConfiguration.cpp`:
```cpp
// Default to 0.0.0.0 for container networking (accepts connections from any interface)
{ServerConfiguration::kEagleInterfaceGrpcAddress, "0.0.0.0:40042"}
```
This still works on the local Mac because `0.0.0.0` means "all interfaces", which includes the loopback interface that localhost uses.
### gRPC Plaintext for Internal Communication
By default, gRPC's `ManagedChannelBuilder` attempts TLS connections. Since Shardok runs in plaintext mode, Eagle must use `.usePlaintext()` for internal container-to-container communication:
```scala
// ServerSetupHelpers.scala
val channelBuilder = ManagedChannelBuilder
.forAddress(shardokInterfaceAddress, shardokInterfacePort)
.usePlaintext() // Required for internal Docker communication
```
TLS termination happens at nginx for external clients. Internal traffic between Eagle and Shardok stays within the Docker network and doesn't need encryption.
**Future consideration:** If Shardok moves to a separate droplet, traffic would traverse the network. Options:
1. Enable TLS between Eagle and Shardok (configure Shardok with certificates)
2. Use DigitalOcean VPC (private network, still unencrypted but isolated)
3. Use WireGuard/VPN tunnel between hosts
### Bazel Runfiles in Docker
Bazel's runfiles system locates resources relative to the executable using a manifest or directory structure that Bazel sets up at runtime. This doesn't exist in Docker containers.
**Solution:** Environment variable fallbacks in `FilesystemUtils.cpp`:
```cpp
// Check for Docker deployment env vars first
const char* resourcesPath = getenv("SHARDOK_RESOURCES_PATH");
if (resourcesPath != nullptr) {
return string(resourcesPath) + "/";
}
// Fall back to Bazel runfiles for local development
return rLocation + "/src/main/resources/net/eagle0/shardok/";
```
The Docker Compose configuration sets these:
```yaml
environment:
SHARDOK_RESOURCES_PATH: "/app/resources"
SHARDOK_MAPS_PATH: "/app/resources/maps"
```
This allows the same binary to work in both environments:
- **Local dev:** No env var → uses Bazel runfiles
- **Docker:** Env var set → uses `/app/resources/` directly
---
## Build Pipeline
### Build Artifacts
@@ -26,6 +26,13 @@ namespace fs = std::filesystem;
static string rLocation;
auto rloc(const string& execPath) -> string {
// First check for environment variable override for Docker deployment
const char* resourcesPath = getenv("SHARDOK_RESOURCES_PATH");
if (resourcesPath != nullptr) {
return ""; // Return empty so StaticShardokFilesDirectory uses env var directly
}
// Fall back to Bazel runfiles for development
string error;
const std::unique_ptr<Runfiles> runfiles(Runfiles::Create(execPath, &error));
@@ -58,10 +65,14 @@ auto FilesystemUtils::FileExistsAtPath(const string& path) -> bool { return fs::
auto FilesystemUtils::StaticEagle0FilesDirectory() -> string { return "/usr/local/share/eagle0/"; }
auto FilesystemUtils::StaticShardokFilesDirectory() -> string {
const char* resourcesPath = getenv("SHARDOK_RESOURCES_PATH");
if (resourcesPath != nullptr) { return string(resourcesPath) + "/"; }
return rLocation + "/src/main/resources/net/eagle0/shardok/";
}
auto FilesystemUtils::MapFilesDirectory() -> string {
const char* mapsPath = getenv("SHARDOK_MAPS_PATH");
if (mapsPath != nullptr) { return string(mapsPath) + "/"; }
return StaticShardokFilesDirectory() + "maps/";
}
@@ -8,6 +8,7 @@
#include "ServerConfiguration.hpp"
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
@@ -30,9 +31,14 @@ using std::unordered_map;
unordered_map<string, string> DefaultValues();
unordered_map<string, string> DefaultValues() {
// Check environment variables for Docker deployment
const char* eagleInterfaceAddr = getenv("SHARDOK_EAGLE_INTERFACE_ADDRESS");
const char* shardokAddr = getenv("SHARDOK_GRPC_ADDRESS");
return unordered_map<string, string>{
{ServerConfiguration::kShardokGrpcAddress, "localhost"},
{ServerConfiguration::kEagleInterfaceGrpcAddress, "localhost"}};
{ServerConfiguration::kShardokGrpcAddress, shardokAddr ? shardokAddr : "localhost"},
{ServerConfiguration::kEagleInterfaceGrpcAddress,
eagleInterfaceAddr ? eagleInterfaceAddr : "0.0.0.0:40042"}};
}
ServerConfiguration::ServerConfiguration(const string& filePath) {
@@ -93,6 +93,7 @@ object ServerSetupHelpers {
): Channel = {
val channelBuilder = ManagedChannelBuilder
.forAddress(shardokInterfaceAddress, shardokInterfacePort)
.usePlaintext()
channelBuilder.build
}