diff --git a/Dockerfile b/Dockerfile index 164ea03..a5f13c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Use the official Nginx base image FROM nginx:1.25.3 -ENV PORT_IP_MAP "" +ENV PORT_MAP "" ENV PYTHONUNBUFFERED=1 ENV PLACEHOLDER_SERVER_SLEEPING_IP "" ENV PLACEHOLDER_SERVER_STARTING_IP "" diff --git a/app/app.py b/app/app.py index a07fe15..eacfff4 100644 --- a/app/app.py +++ b/app/app.py @@ -1,4 +1,5 @@ import logging + from requestHandler import RequestHandler from utils import docker_container_mapping from dockerHandler import DockerHandler @@ -9,24 +10,45 @@ logging.basicConfig(level=logging.INFO) def main(): try: - port_ip_map = docker_container_mapping() + port_map = docker_container_mapping() # Create a DockerHandler instance docker_handler = DockerHandler( - 'unix://var/run/docker.sock', port_ip_map) + 'unix://var/run/docker.sock', port_map) # Create an NginxHandler instance nginx_handler = NginxHandler('/etc/nginx/nginx.conf') + print(1) + docker_handler.get_ip_by_dns_name( + docker_handler.get_current_container_name()) + print(2) + nginx_handler.setup_config_file( - docker_container_mapping(), docker_handler.get_current_container_ip()) + port_map, docker_handler.get_ip_by_dns_name(docker_handler.get_current_container_name()), docker_handler) + print(3) + nginx_handler.print_config() # Create a RequestHandler instance for each port - for port in port_ip_map.keys(): + for port in port_map.keys(): logging.info(f'Creating request handler for port {port}') request_handler = RequestHandler(int(port), docker_handler) request_handler.start() + # DEBUG + logging.info( + '-----------------------------DEBUG--------------------------------') + logging.info( + f'Current container: {docker_handler.get_current_container_name()}') + logging.info( + f'Current container ip: {docker_handler.get_ip_by_dns_name(docker_handler.get_current_container_name())}') + logging.info( + f'Current network: {docker_handler.get_current_network()}') + + docker_handler.print_all_container_names() + logging.info( + '-----------------------------DEBUG END--------------------------------') + except Exception as e: logging.error(f'An error occurred: {e}') diff --git a/app/dockerHandler.py b/app/dockerHandler.py index a5388f0..5b6f497 100644 --- a/app/dockerHandler.py +++ b/app/dockerHandler.py @@ -1,16 +1,20 @@ +from math import log +import socket import docker import os import logging class DockerHandler: - def __init__(self, base_url, port_ip_map): + def __init__(self, base_url, port_map): logging.info( - f'Initializing docker handler with base url {base_url} and port ip map: {port_ip_map}') + f'Initializing docker handler with base url {base_url} and port ip map: {port_map}') self.base_url = base_url self.client = docker.DockerClient(base_url=base_url) - self.port_ip_map = port_ip_map + self.port_map = port_map self.current_network = self.get_current_network() + logging.info('Docker handler initialized') + logging.info(f'Current network: {self.current_network}') def get_current_container(self): current_container_name = os.environ.get('HOSTNAME') @@ -20,13 +24,15 @@ class DockerHandler: logging.error(f'Container {current_container_name} not found') return None - def get_current_container_ip(self): - # Get IP of current container + def get_current_container_name(self): + # Get DNS name of current container current_container = self.get_current_container() if current_container: networks = current_container.attrs['NetworkSettings']['Networks'] current_network = list(networks.keys())[0] - return networks[current_network]['IPAddress'] + if 'Aliases' in networks[current_network]: + dns_name = networks[current_network]['Aliases'][0] + return dns_name return None def get_current_network(self): @@ -54,7 +60,67 @@ class DockerHandler: f'No docker container found with ip {ip} in network {self.current_network}') return None + def get_container_by_name(self, name): + try: + return self.client.containers.get(name) + except docker.errors.NotFound: + logging.error(f'Container {name} not found') + return None + def is_container_starting(self, container): if container: return container.attrs['State']['Health']['Status'] == 'starting' return False + + def print_all_container_names(self): + try: + containers = self.client.containers.list( + all=True, filters={"network": self.current_network}) + + if containers is None: + logging.info('No containers found') + return None + + for container in containers: + logging.info(f'Container name: {container.name}') + # get docker compose dns name + networks = container.attrs['NetworkSettings']['Networks'] + if self.current_network in networks: + logging.info( + f'Container ip: {networks[self.current_network]["IPAMConfig"]["IPv4Address"]}') + else: + logging.info(f'Container ip: None') + + except docker.errors.APIError as e: + logging.error(f'Error getting container list: {e}') + return None + + def get_ip_by_dns_name(self, dns_name): + # dEBUG Print all containers with their network ip and dns name + for container in self.client.containers.list(all=True): + networks = container.attrs['NetworkSettings']['Networks'] + if self.current_network in networks: + logging.info( + f'Container {container.name} ip: {networks[self.current_network]["IPAMConfig"]["IPv4Address"]}, dns name: {networks[self.current_network]["Aliases"]}') + + try: + containers = self.client.containers.list( + all=True, filters={"network": self.current_network}) + + if containers is None: + logging.info('No containers found') + return None + + for container in containers: + networks = container.attrs['NetworkSettings']['Networks'] + if self.current_network in networks and dns_name in networks[self.current_network]['Aliases']: + logging.info( + f'Found container {container.name} with dns name {dns_name} in network {self.current_network}') + return networks[self.current_network]['IPAMConfig']['IPv4Address'] + logging.info( + f'No docker container found with dns name {dns_name} in network {self.current_network}') + return None + + except docker.errors.APIError as e: + logging.error(f'Error getting container list: {e}') + return None diff --git a/app/nginxHandler.py b/app/nginxHandler.py index cc92201..5c79cd7 100644 --- a/app/nginxHandler.py +++ b/app/nginxHandler.py @@ -24,11 +24,16 @@ class NginxHandler: with open(self.config_path, 'r') as f: logging.info(f.read()) - def setup_config_file(self, port_ip_map, current_container_ip): + def setup_config_file(self, port_map, current_container_ip, docker_handler): + print(f'-------------------> {port_map}') + if port_map is None: + logging.error('port_map is None') + return + proxy_timeout = "5s" self.stop() logging.info('Setting up NGINX config file...') - logging.info('port_ip_map: {}'.format(port_ip_map)) + logging.info('port_map: {}'.format(port_map)) nginx_conf = open(self.config_path, 'w+') nginx_conf.truncate() nginx_conf.write('worker_processes 5;\n') @@ -40,22 +45,30 @@ class NginxHandler: # This looks confusing, but the nginx.conf looks good when it's done # Example for the nginx-example.conf file is in the repo root directory - for port in port_ip_map: - nginx_conf.write(f' upstream upstream_{port} {{\n') - nginx_conf.write(f' server {port_ip_map[port]}:25565;\n') - nginx_conf.write(f' server 127.0.0.1:{port} backup;\n') - nginx_conf.write(' }\n') + if isinstance(port_map, dict): + for port in port_map: + print(f'-------------------> {port_map}') + ip = docker_handler.get_ip_by_dns_name(port_map[port]) + print(f'-------------------> {port_map[port]}, {ip}') - nginx_conf.write(' server {\n') - nginx_conf.write( - f' listen {current_container_ip}:{port};\n') + nginx_conf.write(f' upstream upstream_{port} {{\n') + nginx_conf.write(f' server {ip}:25565;\n') + nginx_conf.write(f' server 127.0.0.1:{port} backup;\n') + nginx_conf.write(' }\n') - nginx_conf.write( - f' proxy_connect_timeout {proxy_timeout};\n') - nginx_conf.write(f' proxy_timeout {proxy_timeout};\n') + nginx_conf.write(' server {\n') + nginx_conf.write( + f' listen {current_container_ip}:{port};\n') + + nginx_conf.write( + f' proxy_connect_timeout {proxy_timeout};\n') + nginx_conf.write(f' proxy_timeout {proxy_timeout};\n') + + nginx_conf.write(f' proxy_pass upstream_{port};\n') + nginx_conf.write(' }\n') + else: + logging.error('port_map is not a dictionary') - nginx_conf.write(f' proxy_pass upstream_{port};\n') - nginx_conf.write(' }\n') nginx_conf.write('}\n') nginx_conf.close() logging.info('NGINX config file setup complete') diff --git a/app/requestHandler.py b/app/requestHandler.py index 9a83059..abec88d 100644 --- a/app/requestHandler.py +++ b/app/requestHandler.py @@ -47,7 +47,8 @@ class RequestHandler(threading.Thread): def handle_request(self): logging.info(f'Handling request on port {self.port}') - container_ip = docker_container_mapping().get(str(self.port)) + container_ip = self.docker_handler.get_ip_by_dns_name( + docker_container_mapping().get(str(self.port))) if container_ip: container = self.docker_handler.get_container_by_ip( container_ip) @@ -85,11 +86,13 @@ class RequestHandler(threading.Thread): def forward_request_to_placeholder(self, request, isStarting=False): logging.info('Forwarding request to placeholder server') with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket: - ip = os.environ.get('PLACEHOLDER_SERVER_SLEEPING_IP') + ip = self.docker_handler.get_ip_by_dns_name( + os.environ.get('PLACEHOLDER_SERVER_SLEEPING_SERVICE')) if isStarting: logging.info( 'Container is starting. Using starting placeholder IP') - ip = os.environ.get('PLACEHOLDER_SERVER_STARTING_IP') + ip = self.docker_handler.get_ip_by_dns_name( + os.environ.get('PLACEHOLDER_SERVER_STARTING_SERVICE')) if not ip: logging.info('No placeholder server IP found') diff --git a/app/utils.py b/app/utils.py index 05cc7b0..e57db5c 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,8 +1,11 @@ +import logging import os +import socket +import socket def docker_container_mapping(): - port_ip_map_str = os.environ.get('PORT_IP_MAP') + port_ip_map_str = os.environ.get('PORT_MAP') # Convert the environment variable to a Python dictionary port_ip_map = {} for line in port_ip_map_str.split('\n'): @@ -11,3 +14,11 @@ def docker_container_mapping(): port_ip_map[port.strip()] = ip.strip() return port_ip_map + + +def get_ip_by_dns_name(dns_name): + try: + return socket.gethostbyname(dns_name, resolver='127.0.0.1') + except socket.gaierror: + logging.error(f'Could not resolve dns name {dns_name}') + return None diff --git a/docker-compose.yml b/docker-compose.yml index c31b4f9..e0c2e6d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,5 @@ version: "3.9" -networks: - # You need to have a network with static ips for the servers - mc_network: - driver: bridge - ipam: - driver: default - config: - - subnet: 172.20.0.0/16 - services: auto_starter: container_name: mc_auto_starter @@ -19,19 +10,16 @@ services: - 25566:25566 environment: # The ip of the placeholder servers below - PLACEHOLDER_SERVER_SLEEPING_IP: "172.20.0.3" - PLACEHOLDER_SERVER_STARTING_IP: "172.20.0.4" + PLACEHOLDER_SERVER_SLEEPING_SERVICE: "mc_placeholder_server_sleeping" + PLACEHOLDER_SERVER_STARTING_SERVICE: "mc_placeholder_server_starting" # Port mapping for the servers # The key is the external port of the placeholder server # The value is the internal ip of the actual server # Don't change the server port in the actual server. Use this instead - PORT_IP_MAP: | - 25565: 172.20.0.5 - 25566: 172.20.0.6 - networks: - mc_network: - ipv4_address: 172.20.0.2 + PORT_MAP: | + 25565: "mc" + 25566: "mc2" volumes: - /var/run/docker.sock:/var/run/docker.sock @@ -60,15 +48,37 @@ services: GENERATE_STRUCTURES: "false" ALLOW_NETHER: "false" ALLOW_END: "false" - networks: - mc_network: - ipv4_address: 172.20.0.3 mc_placeholder_server_starting: container_name: mc_placeholder_server_starting restart: always image: itzg/minecraft-server:java8 environment: + EULA: "TRUE" + MOTD: "Starting, please wait..." + + # The placeholder servers should be as lightweight as possible + MAX_PLAYERS: "0" + MAX_MEMORY: "512M" + INIT_MEMORY: "512M" + LEVEL_TYPE: "FLAT" + JVM_XX_OPTS: "-XX:+UseConcMarkSweepGC -XX:+DisableExplicitGC -XX:+UseCompressedOops" + VIEW_DISTANCE: "1" + SPAWN_ANIMALS: "false" + SPAWN_MONSTERS: "false" + SNOOPER_ENABLED: "false" + GENERATE_STRUCTURES: "false" + ALLOW_NETHER: "false" + ALLOW_END: "false" + + # These are the actual servers + # For itzg/minecraft-server you can find the documentation here: https://docker-minecraft-server.readthedocs.io/en/latest/variables/ + mc: + container_name: example_mc_server_1 + image: itzg/minecraft-server + restart: no #! This is important. If you restart the server automatically, the auto_starter will not work + environment: + type: "PAPER" VERSION: "1.12.2" EULA: "TRUE" MOTD: "Starting, please wait..." @@ -86,31 +96,7 @@ services: GENERATE_STRUCTURES: "false" ALLOW_NETHER: "false" ALLOW_END: "false" - networks: - mc_network: - ipv4_address: 172.20.0.4 - # These are the actual servers - # For itzg/minecraft-server you can find the documentation here: https://docker-minecraft-server.readthedocs.io/en/latest/variables/ - mc: - container_name: example_mc_server_1 - image: itzg/minecraft-server - restart: no #! This is important. If you restart the server automatically, the auto_starter will not work - environment: - type: "PAPER" - EULA: "TRUE" - MOTD: "Example Server 1" - MAX_PLAYERS: "1" - - # Enable autostop, so the auto_starter makes sense - ENABLE_AUTOSTOP: "TRUE" - AUTOSTOP_TIMEOUT_EST: "10" - AUTOSTOP_TIMEOUT_INIT: "10" - #! Dont change SERVER_PORT. Use PORT_IP_MAP in auto_starter instead. - #! SERVER_PORT default is "25565" - networks: - mc_network: - ipv4_address: 172.20.0.5 mc2: container_name: example_mc_server_2 image: itzg/minecraft-server @@ -122,11 +108,8 @@ services: MAX_PLAYERS: "1" # Enable autostop, so the auto_starter makes sense - ENABLE_AUTOSTOP: "TRUE" + ENABLE_AUTOSTOP: "FALSE" AUTOSTOP_TIMEOUT_EST: "10" AUTOSTOP_TIMEOUT_INIT: "10" #! Dont change SERVER_PORT. Use PORT_IP_MAP in auto_starter instead. # SERVER_PORT default is "25565" - networks: - mc_network: - ipv4_address: 172.20.0.6 diff --git a/start.sh b/start.sh index e92332e..2351c33 100644 --- a/start.sh +++ b/start.sh @@ -1 +1,2 @@ -docker compose up -d \ No newline at end of file +docker compose down +docker compose up -d --remove-orphans \ No newline at end of file