From 2ead63ae5e0784cb19729a3982fe6202cacaa24c Mon Sep 17 00:00:00 2001 From: Jvst Me Date: Tue, 1 Sep 2026 08:39:29 +0200 Subject: [PATCH] Tune Nginx config on the gateway Bring our own config, based on the default Ubuntu config, but with increased `worker_rlimit_nofile`, `worker_connections`, `server_names_hash_bucket_size`. --- .../_internal/core/backends/base/compute.py | 6 --- .../proxy/gateway/resources/nginx/nginx.conf | 32 +++++++++++ .../_internal/proxy/gateway/services/nginx.py | 22 ++++---- .../proxy/gateway/routers/test_registry.py | 53 +++++++++---------- src/tests/_internal/proxy/gateway/test_app.py | 12 +++-- 5 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 src/dstack/_internal/proxy/gateway/resources/nginx/nginx.conf diff --git a/src/dstack/_internal/core/backends/base/compute.py b/src/dstack/_internal/core/backends/base/compute.py index 673f396429..234b3a3097 100644 --- a/src/dstack/_internal/core/backends/base/compute.py +++ b/src/dstack/_internal/core/backends/base/compute.py @@ -1098,12 +1098,6 @@ def get_gateway_user_data(authorized_key: str) -> str: snap={"commands": [["install", "--classic", "certbot"]]}, runcmd=[ ["ln", "-s", "/snap/bin/certbot", "/usr/bin/certbot"], - [ - "sed", - "-i", - "s/# server_names_hash_bucket_size 64;/server_names_hash_bucket_size 128;/", - "/etc/nginx/nginx.conf", - ], ["su", "ubuntu", "-c", " && ".join(get_dstack_gateway_commands())], ], ssh_authorized_keys=[authorized_key], diff --git a/src/dstack/_internal/proxy/gateway/resources/nginx/nginx.conf b/src/dstack/_internal/proxy/gateway/resources/nginx/nginx.conf new file mode 100644 index 0000000000..0e4820f58c --- /dev/null +++ b/src/dstack/_internal/proxy/gateway/resources/nginx/nginx.conf @@ -0,0 +1,32 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +worker_rlimit_nofile 65535; + +events { + worker_connections 16384; +} + +http { + sendfile on; + tcp_nopush on; + types_hash_max_size 2048; + + server_names_hash_bucket_size 128; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + gzip on; + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} diff --git a/src/dstack/_internal/proxy/gateway/services/nginx.py b/src/dstack/_internal/proxy/gateway/services/nginx.py index 29562c5556..74581c6477 100644 --- a/src/dstack/_internal/proxy/gateway/services/nginx.py +++ b/src/dstack/_internal/proxy/gateway/services/nginx.py @@ -18,7 +18,7 @@ CERTBOT_TIMEOUT = 40 CERTBOT_2ND_TIMEOUT = 5 -CONFIGS_DIR = Path("/etc/nginx/sites-enabled") +NGINX_DIR = Path("/etc/nginx") logger = get_logger(__name__) @@ -78,8 +78,10 @@ class ModelEntrypointConfig(SiteConfig): class Nginx: """Updates nginx config and issues SSL certificates.""" - def __init__(self, conf_dir: Path = Path("/etc/nginx/sites-enabled")) -> None: - self._conf_dir = conf_dir + def __init__(self, nginx_dir: Path = NGINX_DIR) -> None: + self._nginx_dir = nginx_dir + self._sites_enabled_dir = nginx_dir / "sites-enabled" + self._nginx_conf_path = nginx_dir / "nginx.conf" self._lock: Lock = Lock() async def register(self, conf: SiteConfig, acme: ACMESettings) -> None: @@ -89,14 +91,14 @@ async def register(self, conf: SiteConfig, acme: ACMESettings) -> None: if conf.https: await run_async(self.run_certbot, conf.domain, acme) - await run_async(self.write_conf, conf.render(), conf_name) + await run_async(self.write_conf, conf.render(), self._sites_enabled_dir / conf_name) logger.info("Registered %s domain %s", conf.type, conf.domain) async def unregister(self, service: models.Service) -> None: domain = service.domain_safe logger.debug("Unregistering domain %s", domain) - conf_path = self._conf_dir / self.get_config_name(domain) + conf_path = self._sites_enabled_dir / self.get_config_name(domain) if not conf_path.exists(): return async with self._lock: @@ -111,9 +113,8 @@ def reload() -> None: if r.returncode != 0: raise UnexpectedProxyError("Failed to reload nginx") - def write_conf(self, conf: str, conf_name: str) -> None: + def write_conf(self, conf: str, conf_path: Path) -> None: """Update config and reload nginx. Rollback changes on error.""" - conf_path = self._conf_dir / conf_name old_conf = conf_path.read_text() if conf_path.exists() else None if conf == old_conf: return @@ -176,8 +177,11 @@ def get_config_name(domain: str) -> str: return f"443-{domain}.conf" def write_global_conf(self) -> None: - conf = read_package_resource("00-log-format.conf") - self.write_conf(conf, "00-log-format.conf") + log_format_conf = read_package_resource("00-log-format.conf") + self.write_conf(log_format_conf, self._sites_enabled_dir / "00-log-format.conf") + + nginx_conf = read_package_resource("nginx.conf") + self.write_conf(nginx_conf, self._nginx_conf_path) def read_package_resource(file: str) -> str: diff --git a/src/tests/_internal/proxy/gateway/routers/test_registry.py b/src/tests/_internal/proxy/gateway/routers/test_registry.py index b7a5b2e2e8..20a0e148ce 100644 --- a/src/tests/_internal/proxy/gateway/routers/test_registry.py +++ b/src/tests/_internal/proxy/gateway/routers/test_registry.py @@ -17,10 +17,9 @@ from dstack._internal.proxy.lib.testing.common import make_project, make_service -def make_client( - nginx_conf_dir: Path, repo: Optional[GatewayProxyRepo] = None -) -> httpx.AsyncClient: - app = make_app(repo=repo or GatewayProxyRepo(), nginx=Nginx(conf_dir=nginx_conf_dir)) +def make_client(nginx_dir: Path, repo: Optional[GatewayProxyRepo] = None) -> httpx.AsyncClient: + (nginx_dir / "sites-enabled").mkdir(exist_ok=True) + app = make_app(repo=repo or GatewayProxyRepo(), nginx=Nginx(nginx_dir=nginx_dir)) return httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test/") @@ -103,7 +102,7 @@ async def test_register(self, tmp_path: Path, system_mocks: Mocks) -> None: ) assert resp.status_code == 200 assert resp.json() == {"status": "ok"} - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() # general assert system_mocks.reload_nginx.call_count == 1 assert "server_name test-run.gtw.test;" in conf @@ -136,7 +135,7 @@ async def test_register_with_https(self, tmp_path: Path, system_mocks: Mocks) -> json=register_service_payload(domain="test-run.gtw.test", https=True), ) assert resp.status_code == 200 - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert "listen 80;" in conf assert "listen 443 ssl;" in conf assert "ssl_certificate /etc/letsencrypt/live/test-run.gtw.test/fullchain.pem;" in conf @@ -150,7 +149,7 @@ async def test_register_with_auth(self, tmp_path: Path, system_mocks: Mocks) -> json=register_service_payload(domain="test-run.gtw.test", auth=True), ) assert resp.status_code == 200 - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert "auth_request /_dstack_auth;" in conf assert "proxy_pass http://localhost:8000/api/auth/test-proj;" in conf @@ -167,8 +166,8 @@ async def test_register_same_name_error(self, tmp_path: Path, system_mocks: Mock ) assert resp.status_code == 400 assert resp.json() == {"detail": "Service test-proj/test-run is already registered"} - assert (tmp_path / "443-test-run-1.gtw.test.conf").exists() - assert not (tmp_path / "443-test-run-2.gtw.test.conf").exists() + assert (tmp_path / "sites-enabled" / "443-test-run-1.gtw.test.conf").exists() + assert not (tmp_path / "sites-enabled" / "443-test-run-2.gtw.test.conf").exists() assert system_mocks.reload_nginx.call_count == 1 async def test_register_same_name_in_different_projects( @@ -185,8 +184,8 @@ async def test_register_same_name_in_different_projects( json=register_service_payload(run_name="test-run", domain="test-run.proj-2.gtw.test"), ) assert resp.status_code == 200 - assert (tmp_path / "443-test-run.proj-1.gtw.test.conf").exists() - assert (tmp_path / "443-test-run.proj-2.gtw.test.conf").exists() + assert (tmp_path / "sites-enabled" / "443-test-run.proj-1.gtw.test.conf").exists() + assert (tmp_path / "sites-enabled" / "443-test-run.proj-2.gtw.test.conf").exists() async def test_register_same_domain_error(self, tmp_path: Path, system_mocks: Mocks) -> None: client = make_client(tmp_path) @@ -203,7 +202,7 @@ async def test_register_same_domain_error(self, tmp_path: Path, system_mocks: Mo assert resp.json() == { "detail": "Domain name 'test-run.gtw.test' is already taken by another service" } - assert (tmp_path / "443-test-run.gtw.test.conf").exists() + assert (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").exists() assert system_mocks.reload_nginx.call_count == 1 @freeze_time(datetime(2024, 12, 12, 0, 30)) @@ -251,7 +250,7 @@ async def test_register_with_rate_limits(self, tmp_path: Path, system_mocks: Moc ), ) assert resp.status_code == 200 - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert ( "limit_req_zone $binary_remote_addr zone=0.test-run.gtw.test:10m rate=150r/m;" in conf ) @@ -276,7 +275,7 @@ async def test_register_with_root_rate_limit( ), ) assert resp.status_code == 200 - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert ( "limit_req_zone $binary_remote_addr zone=0.test-run.gtw.test:10m rate=60r/m;" in conf ) @@ -290,7 +289,7 @@ async def test_register_without_rate_limits(self, tmp_path: Path, system_mocks: json=register_service_payload(domain="test-run.gtw.test", rate_limits=[]), ) assert resp.status_code == 200 - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert "limit_req_zone" not in conf assert "limit_req zone=" not in conf assert "location / {" in conf @@ -306,7 +305,7 @@ async def test_register(self, tmp_path: Path, system_mocks: Mocks) -> None: json=register_service_payload(run_name="test-run", domain="test-run.gtw.test"), ) assert resp.status_code == 200 - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert "upstream" not in conf # register 2 replicas resp = await client.post( @@ -321,7 +320,7 @@ async def test_register(self, tmp_path: Path, system_mocks: Mocks) -> None: ) assert resp.status_code == 200 assert resp.json() == {"status": "ok"} - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert "upstream test-run.gtw.test.upstream" in conf assert (m1 := re.search(r"server unix:/(.+)/replica.sock; # replica xxx-xxx", conf)) assert (m2 := re.search(r"server unix:/(.+)/replica.sock; # replica yyy-yyy", conf)) @@ -376,7 +375,7 @@ async def test_register_connection_error(self, tmp_path: Path, system_mocks: Moc json=register_service_payload(run_name="test-run", domain="test-run.gtw.test"), ) assert resp.status_code == 200 - conf_before = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf_before = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() # register invalid replica system_mocks.open_conn.side_effect = SSHError("test error") resp = await client.post( @@ -387,7 +386,7 @@ async def test_register_connection_error(self, tmp_path: Path, system_mocks: Moc assert resp.json() == { "detail": "Cannot register replica abc-def in service test-proj/test-run: test error" } - conf_after = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf_after = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert conf_after == conf_before @@ -450,12 +449,12 @@ async def test_unregister(self, tmp_path: Path, system_mocks: Mocks) -> None: json=register_service_payload(run_name="test-run", domain="test-run.gtw.test"), ) assert resp.status_code == 200 - assert (tmp_path / "443-test-run.gtw.test.conf").exists() + assert (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").exists() # unregister service resp = await client.post("/api/registry/test-proj/services/test-run/unregister") assert resp.status_code == 200 assert resp.json() == {"status": "ok"} - assert not (tmp_path / "443-test-run.gtw.test.conf").exists() + assert not (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").exists() assert system_mocks.reload_nginx.call_count == 2 async def test_unregister_not_registered_error( @@ -484,11 +483,11 @@ async def test_unregister_with_replicas(self, tmp_path: Path, system_mocks: Mock json=register_replica_payload(job_id=job_id), ) assert resp.status_code == 200 - assert (tmp_path / "443-test-run.gtw.test.conf").exists() + assert (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").exists() # unregister service resp = await client.post("/api/registry/test-proj/services/test-run/unregister") assert resp.status_code == 200 - assert not (tmp_path / "443-test-run.gtw.test.conf").exists() + assert not (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").exists() assert system_mocks.reload_nginx.call_count == 4 assert system_mocks.close_conn.call_count == 2 @@ -526,7 +525,7 @@ async def test_unregister(self, tmp_path: Path, system_mocks: Mocks) -> None: json=register_replica_payload(job_id=job_id), ) assert resp.status_code == 200 - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert "replica xxx-xxx" in conf assert "replica yyy-yyy" in conf # unregister 1 replica @@ -535,7 +534,7 @@ async def test_unregister(self, tmp_path: Path, system_mocks: Mocks) -> None: ) assert resp.status_code == 200 assert resp.json() == {"status": "ok"} - conf = (tmp_path / "443-test-run.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-test-run.gtw.test.conf").read_text() assert "replica xxx-xxx" in conf assert "replica yyy-yyy" not in conf assert system_mocks.reload_nginx.call_count == 4 @@ -585,7 +584,7 @@ async def test_register(self, tmp_path: Path, system_mocks: Mocks) -> None: ) assert resp.status_code == 200 assert resp.json() == {"status": "ok"} - conf = (tmp_path / "443-gateway.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-gateway.gtw.test.conf").read_text() assert "proxy_pass http://localhost:8000/api/models/test-proj/;" in conf assert "listen 80;" in conf assert "listen 443" not in conf @@ -599,7 +598,7 @@ async def test_register_with_https(self, tmp_path: Path, system_mocks: Mocks) -> json={"domain": "gateway.gtw.test", "https": True}, ) assert resp.status_code == 200 - conf = (tmp_path / "443-gateway.gtw.test.conf").read_text() + conf = (tmp_path / "sites-enabled" / "443-gateway.gtw.test.conf").read_text() assert "proxy_pass http://localhost:8000/api/models/test-proj/;" in conf assert "listen 80;" in conf assert "listen 443 ssl;" in conf diff --git a/src/tests/_internal/proxy/gateway/test_app.py b/src/tests/_internal/proxy/gateway/test_app.py index d1bff6d2ad..335be84fa1 100644 --- a/src/tests/_internal/proxy/gateway/test_app.py +++ b/src/tests/_internal/proxy/gateway/test_app.py @@ -21,12 +21,14 @@ async def test_lifespan(tmp_path: Path, system_mocks: Mocks) -> None: make_service("test-proj", "test-run", domain="test-run.gtw.test", https=True) ) nginx_dir = tmp_path / "nginx" - nginx_dir.mkdir() - app = make_app(repo=repo, nginx=Nginx(conf_dir=nginx_dir)) + conf_dir = nginx_dir / "sites-enabled" + conf_dir.mkdir(parents=True) + app = make_app(repo=repo, nginx=Nginx(nginx_dir=nginx_dir)) async with lifespan(app): - assert (nginx_dir / "00-log-format.conf").exists() - assert (nginx_dir / "443-gateway.gtw.test.conf").exists() - assert (nginx_dir / "443-test-run.gtw.test.conf").exists() + assert (conf_dir / "00-log-format.conf").exists() + assert (conf_dir / "443-gateway.gtw.test.conf").exists() + assert (conf_dir / "443-test-run.gtw.test.conf").exists() + assert (nginx_dir / "nginx.conf").exists() assert system_mocks.open_conn.call_count == 1 assert system_mocks.close_conn.call_count == 0 assert system_mocks.close_conn.call_count == 1