Skip to content

Commit 768bdc1

Browse files
Allow https: auto for services (#3600)
* Add https: auto mode for services Allow setting `https` to `auto` in service configuration. When set to `auto`, the effective HTTPS setting is resolved at registration time based on the gateway's certificate configuration: enabled for Let's Encrypt, disabled for no certificate or ACM. The default remains `true` for backward compatibility. Co-authored-by: Cursor <cursoragent@cursor.com> * Update gateway docs: add Domain and Certificate sections, document private gateways - Add "Domain" section explaining domain requirement and DNS setup - Add "Certificate" section covering lets-encrypt, acm, and null options - Expand "Public IP" with private gateway example - Move "Instance type" after "Public IP" - Update certificate field description to mention null - Add null note to gateway reference page
1 parent 41079ff commit 768bdc1

File tree

6 files changed

+181
-39
lines changed

6 files changed

+181
-39
lines changed

docs/docs/concepts/gateways.md

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ domain: example.com
3232
3333
</div>
3434
35-
A domain name is required to create a gateway.
36-
3735
To create or update the gateway, simply call the [`dstack apply`](../reference/cli/dstack/apply.md) command:
3836

3937
<div class="termy">
@@ -53,6 +51,12 @@ Provisioning...
5351

5452
## Configuration options
5553

54+
### Domain
55+
56+
A gateway requires a `domain` to be specified in the configuration before creation. The domain is used to generate service endpoints (e.g. `<run name>.<gateway domain>`).
57+
58+
Once the gateway is created and assigned a hostname, configure your DNS by adding a wildcard record for `*.<gateway domain>` (e.g. `*.example.com`). The record should point to the gateway's hostname and should be of type `A` if the hostname is an IP address (most cases), or of type `CNAME` if the hostname is another domain (some private gateways and Kubernetes).
59+
5660
### Backend
5761

5862
You can create gateways with the `aws`, `azure`, `gcp`, or `kubernetes` backends, but that does not limit where services run. A gateway can use one backend while services run on any other backend supported by dstack, including backends where gateways themselves cannot be created.
@@ -61,27 +65,6 @@ You can create gateways with the `aws`, `azure`, `gcp`, or `kubernetes` backends
6165
Gateways in `kubernetes` backend require an external load balancer. Managed Kubernetes solutions usually include a load balancer.
6266
For self-hosted Kubernetes, you must provide a load balancer by yourself.
6367

64-
### Instance type
65-
66-
By default, `dstack` provisions a small, low-cost instance for the gateway. If you expect to run high-traffic services, you can configure a larger instance type using the `instance_type` property.
67-
68-
<div editor-title="gateway.dstack.yml">
69-
70-
```yaml
71-
type: gateway
72-
name: example-gateway
73-
74-
backend: aws
75-
region: eu-west-1
76-
77-
# (Optional) Override the gateway instance type
78-
instance_type: t3.large
79-
80-
domain: example.com
81-
```
82-
83-
</div>
84-
8568
### Router
8669

8770
By default, the gateway uses its own load balancer to route traffic between replicas. However, you can delegate this responsibility to a specific router by setting the `router` property. Currently, the only supported external router is `sglang`.
@@ -124,21 +107,65 @@ If you configure the `sglang` router, [services](../concepts/services.md) can ru
124107

125108

126109

110+
### Certificate
111+
112+
By default, when you run a service with a gateway, `dstack` provisions an SSL certificate via Let's Encrypt for the configured domain. This automatically enables HTTPS for the service endpoint.
113+
114+
If you disable [public IP](#public-ip) (e.g. to make the gateway private) or if you simply don't need HTTPS, you can set `certificate` to `null`.
115+
116+
> Note, by default services set [`https`](../reference/dstack.yml/service.md#https) to `true` which requires a certificate. You can set `https` to `auto` to detect if the gateway supports HTTPS or not automatically.
117+
118+
??? info "Certificate types"
119+
`dstack` supports the following certificate types:
120+
121+
* `lets-encrypt` (default) — Automatic certificates via [Let's Encrypt](https://letsencrypt.org/). Requires a [public IP](#public-ip).
122+
* `acm` — Certificates managed by [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/). AWS-only. TLS is terminated at the load balancer, not at the gateway.
123+
* `null` — No certificate. Services will use HTTP.
124+
127125
### Public IP
128126

129-
If you don't need/want a public IP for the gateway, you can set the `public_ip` to `false` (the default value is `true`), making the gateway private.
127+
If you don't need a public IP for the gateway, you can set `public_ip` to `false` (the default is `true`), making the gateway private.
128+
130129
Private gateways are currently supported in `aws` and `gcp` backends.
131130

132-
!!! info "Reference"
133-
For all gateway configuration options, refer to the [reference](../reference/dstack.yml/gateway.md).
131+
<div editor-title="gateway.dstack.yml">
134132

135-
## Update DNS records
133+
```yaml
134+
type: gateway
135+
name: private-gateway
136+
137+
backend: aws
138+
region: eu-west-1
139+
domain: example.com
140+
141+
public_ip: false
142+
certificate: null
143+
```
144+
145+
</div>
146+
147+
### Instance type
148+
149+
By default, `dstack` provisions a small, low-cost instance for the gateway. If you expect to run high-traffic services, you can configure a larger instance type using the `instance_type` property.
150+
151+
<div editor-title="gateway.dstack.yml">
136152

137-
Once the gateway is assigned a hostname, go to your domain's DNS settings
138-
and add a DNS record for `*.<gateway domain>`, e.g. `*.example.com`.
139-
The record should point to the gateway's hostname shown in `dstack`
140-
and should be of type `A` if the hostname is an IP address (most cases),
141-
or of type `CNAME` if the hostname is another domain (some private gateways and Kubernetes).
153+
```yaml
154+
type: gateway
155+
name: example-gateway
156+
157+
backend: aws
158+
region: eu-west-1
159+
160+
instance_type: t3.large
161+
162+
domain: example.com
163+
```
164+
165+
</div>
166+
167+
!!! info "Reference"
168+
For all gateway configuration options, refer to the [reference](../reference/dstack.yml/gateway.md).
142169

143170
## Manage gateways
144171

docs/docs/reference/dstack.yml/gateway.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ The `gateway` configuration type allows creating and updating [gateways](../../c
2222

2323
### `certificate`
2424

25+
Set to `null` to disable certificates (e.g. for [private gateways](../../concepts/gateways.md#public-ip)).
26+
2527
=== "Let's encrypt"
2628

2729
#SCHEMA# dstack._internal.core.models.gateways.LetsEncryptGatewayCertificate

src/dstack/_internal/core/models/configurations.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -856,9 +856,13 @@ class ServiceConfigurationParams(CoreModel):
856856
)
857857
),
858858
] = None
859-
https: Annotated[bool, Field(description="Enable HTTPS if running with a gateway")] = (
860-
SERVICE_HTTPS_DEFAULT
861-
)
859+
https: Annotated[
860+
Union[bool, Literal["auto"]],
861+
Field(
862+
description="Enable HTTPS if running with a gateway."
863+
" Set to `auto` to determine automatically based on gateway configuration"
864+
),
865+
] = SERVICE_HTTPS_DEFAULT
862866
auth: Annotated[bool, Field(description="Enable the authorization")] = True
863867

864868
scaling: Annotated[

src/dstack/_internal/core/models/gateways.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ class GatewayConfiguration(CoreModel):
7777
public_ip: Annotated[bool, Field(description="Allocate public IP for the gateway")] = True
7878
certificate: Annotated[
7979
Optional[AnyGatewayCertificate],
80-
Field(description="The SSL certificate configuration. Defaults to `type: lets-encrypt`"),
80+
Field(
81+
description="The SSL certificate configuration."
82+
" Set to `null` to disable. Defaults to `type: lets-encrypt`"
83+
),
8184
] = LetsEncryptGatewayCertificate()
8285
tags: Annotated[
8386
Optional[Dict[str, str]],

src/dstack/_internal/server/services/services/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
)
2323
from dstack._internal.core.models.configurations import (
2424
DEFAULT_REPLICA_GROUP_NAME,
25-
SERVICE_HTTPS_DEFAULT,
2625
ServiceConfiguration,
2726
)
2827
from dstack._internal.core.models.gateways import GatewayConfiguration, GatewayStatus
@@ -241,7 +240,7 @@ def _register_service_in_server(run_model: RunModel, run_spec: RunSpec) -> Servi
241240
"Service with SGLang router configuration requires a gateway. "
242241
"Please configure a gateway with the SGLang router enabled."
243242
)
244-
if run_spec.configuration.https != SERVICE_HTTPS_DEFAULT:
243+
if run_spec.configuration.https is False:
245244
# Note: if the user sets `https: <default-value>`, it will be ignored silently
246245
# TODO: in 0.19, make `https` Optional to be able to tell if it was set or omitted
247246
raise ServerClientError(
@@ -416,7 +415,14 @@ async def unregister_replica(session: AsyncSession, job_model: JobModel):
416415

417416
def _get_service_https(run_spec: RunSpec, configuration: GatewayConfiguration) -> bool:
418417
assert run_spec.configuration.type == "service"
419-
if not run_spec.configuration.https:
418+
https = run_spec.configuration.https
419+
if https == "auto":
420+
if configuration.certificate is None:
421+
return False
422+
if configuration.certificate.type == "acm":
423+
return False
424+
return True
425+
if not https:
420426
return False
421427
if configuration.certificate is not None and configuration.certificate.type == "acm":
422428
return False
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from typing import Literal, Optional, Union
2+
from unittest.mock import MagicMock
3+
4+
import pytest
5+
6+
from dstack._internal.core.errors import ServerClientError
7+
from dstack._internal.core.models.backends.base import BackendType
8+
from dstack._internal.core.models.configurations import ServiceConfiguration
9+
from dstack._internal.core.models.gateways import (
10+
ACMGatewayCertificate,
11+
AnyGatewayCertificate,
12+
GatewayConfiguration,
13+
LetsEncryptGatewayCertificate,
14+
)
15+
from dstack._internal.core.models.runs import RunSpec
16+
from dstack._internal.server.services.services import (
17+
_get_service_https,
18+
_register_service_in_server,
19+
)
20+
from dstack._internal.server.testing.common import get_run_spec
21+
22+
23+
def _service_run_spec(https: Union[bool, Literal["auto"]] = "auto") -> RunSpec:
24+
return get_run_spec(
25+
repo_id="test-repo",
26+
configuration=ServiceConfiguration(commands=["python serve.py"], port=8000, https=https),
27+
)
28+
29+
30+
def _gateway_config(
31+
certificate: Optional[AnyGatewayCertificate] = LetsEncryptGatewayCertificate(),
32+
) -> GatewayConfiguration:
33+
return GatewayConfiguration(
34+
backend=BackendType.AWS,
35+
region="us-east-1",
36+
certificate=certificate,
37+
)
38+
39+
40+
def _mock_run_model() -> MagicMock:
41+
run_model = MagicMock()
42+
run_model.project.name = "test-project"
43+
run_model.run_name = "test-run"
44+
return run_model
45+
46+
47+
class TestServiceConfigurationHttps:
48+
def test_default_is_true(self) -> None:
49+
conf = ServiceConfiguration(commands=["python serve.py"], port=8000)
50+
assert conf.https is True
51+
52+
def test_accepts_auto(self) -> None:
53+
conf = ServiceConfiguration(commands=["python serve.py"], port=8000, https="auto")
54+
assert conf.https == "auto"
55+
56+
57+
class TestGetServiceHttps:
58+
def test_auto_resolves_to_true_with_lets_encrypt_gateway(self) -> None:
59+
run_spec = _service_run_spec(https="auto")
60+
gw = _gateway_config(certificate=LetsEncryptGatewayCertificate())
61+
assert _get_service_https(run_spec, gw) is True
62+
63+
def test_auto_resolves_to_false_when_gateway_has_no_certificate(self) -> None:
64+
run_spec = _service_run_spec(https="auto")
65+
gw = _gateway_config(certificate=None)
66+
assert _get_service_https(run_spec, gw) is False
67+
68+
def test_auto_resolves_to_false_with_acm_gateway(self) -> None:
69+
run_spec = _service_run_spec(https="auto")
70+
gw = _gateway_config(
71+
certificate=ACMGatewayCertificate(arn="arn:aws:acm:us-east-1:123:cert/abc")
72+
)
73+
assert _get_service_https(run_spec, gw) is False
74+
75+
def test_true_enables_https_regardless_of_gateway_certificate(self) -> None:
76+
run_spec = _service_run_spec(https=True)
77+
gw = _gateway_config(certificate=None)
78+
assert _get_service_https(run_spec, gw) is True
79+
80+
def test_false_disables_https_regardless_of_gateway_certificate(self) -> None:
81+
run_spec = _service_run_spec(https=False)
82+
gw = _gateway_config(certificate=LetsEncryptGatewayCertificate())
83+
assert _get_service_https(run_spec, gw) is False
84+
85+
86+
class TestRegisterServiceInServerHttps:
87+
def test_allows_default_true_without_gateway(self) -> None:
88+
run_spec = _service_run_spec(https=True)
89+
result = _register_service_in_server(_mock_run_model(), run_spec)
90+
assert result is not None
91+
92+
def test_allows_auto_without_gateway(self) -> None:
93+
run_spec = _service_run_spec(https="auto")
94+
result = _register_service_in_server(_mock_run_model(), run_spec)
95+
assert result is not None
96+
97+
def test_rejects_explicit_false_without_gateway(self) -> None:
98+
run_spec = _service_run_spec(https=False)
99+
with pytest.raises(ServerClientError, match="not applicable"):
100+
_register_service_in_server(_mock_run_model(), run_spec)

0 commit comments

Comments
 (0)