dstack 0.19.31__py3-none-any.whl → 0.19.33__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dstack might be problematic. Click here for more details.

Files changed (53) hide show
  1. dstack/_internal/cli/commands/offer.py +1 -1
  2. dstack/_internal/cli/services/configurators/run.py +1 -5
  3. dstack/_internal/core/backends/aws/compute.py +8 -5
  4. dstack/_internal/core/backends/azure/compute.py +9 -6
  5. dstack/_internal/core/backends/base/compute.py +40 -17
  6. dstack/_internal/core/backends/base/offers.py +5 -1
  7. dstack/_internal/core/backends/datacrunch/compute.py +9 -6
  8. dstack/_internal/core/backends/gcp/compute.py +137 -7
  9. dstack/_internal/core/backends/gcp/models.py +7 -0
  10. dstack/_internal/core/backends/gcp/resources.py +87 -5
  11. dstack/_internal/core/backends/hotaisle/compute.py +30 -0
  12. dstack/_internal/core/backends/kubernetes/compute.py +218 -77
  13. dstack/_internal/core/backends/kubernetes/models.py +4 -2
  14. dstack/_internal/core/backends/nebius/compute.py +24 -6
  15. dstack/_internal/core/backends/nebius/configurator.py +15 -0
  16. dstack/_internal/core/backends/nebius/models.py +57 -5
  17. dstack/_internal/core/backends/nebius/resources.py +45 -2
  18. dstack/_internal/core/backends/oci/compute.py +9 -6
  19. dstack/_internal/core/backends/runpod/compute.py +10 -6
  20. dstack/_internal/core/backends/vastai/compute.py +3 -1
  21. dstack/_internal/core/backends/vastai/configurator.py +0 -1
  22. dstack/_internal/core/compatibility/runs.py +8 -0
  23. dstack/_internal/core/models/fleets.py +1 -1
  24. dstack/_internal/core/models/profiles.py +12 -5
  25. dstack/_internal/core/models/runs.py +3 -2
  26. dstack/_internal/core/models/users.py +10 -0
  27. dstack/_internal/core/services/configs/__init__.py +1 -0
  28. dstack/_internal/server/background/tasks/process_fleets.py +75 -17
  29. dstack/_internal/server/background/tasks/process_instances.py +6 -4
  30. dstack/_internal/server/background/tasks/process_running_jobs.py +1 -0
  31. dstack/_internal/server/background/tasks/process_runs.py +27 -23
  32. dstack/_internal/server/background/tasks/process_submitted_jobs.py +63 -20
  33. dstack/_internal/server/migrations/versions/ff1d94f65b08_user_ssh_key.py +34 -0
  34. dstack/_internal/server/models.py +3 -0
  35. dstack/_internal/server/routers/runs.py +5 -1
  36. dstack/_internal/server/routers/users.py +14 -2
  37. dstack/_internal/server/services/runs.py +9 -4
  38. dstack/_internal/server/services/users.py +35 -2
  39. dstack/_internal/server/statics/index.html +1 -1
  40. dstack/_internal/server/statics/main-720ce3a11140daa480cc.css +3 -0
  41. dstack/_internal/server/statics/{main-c51afa7f243e24d3e446.js → main-97c7e184573ca23f9fe4.js} +12218 -7625
  42. dstack/_internal/server/statics/{main-c51afa7f243e24d3e446.js.map → main-97c7e184573ca23f9fe4.js.map} +1 -1
  43. dstack/api/_public/__init__.py +9 -12
  44. dstack/api/_public/repos.py +0 -21
  45. dstack/api/_public/runs.py +64 -9
  46. dstack/api/server/_users.py +17 -2
  47. dstack/version.py +2 -2
  48. {dstack-0.19.31.dist-info → dstack-0.19.33.dist-info}/METADATA +12 -14
  49. {dstack-0.19.31.dist-info → dstack-0.19.33.dist-info}/RECORD +52 -51
  50. dstack/_internal/server/statics/main-56191fbfe77f49b251de.css +0 -3
  51. {dstack-0.19.31.dist-info → dstack-0.19.33.dist-info}/WHEEL +0 -0
  52. {dstack-0.19.31.dist-info → dstack-0.19.33.dist-info}/entry_points.txt +0 -0
  53. {dstack-0.19.31.dist-info → dstack-0.19.33.dist-info}/licenses/LICENSE.md +0 -0
@@ -2,12 +2,11 @@ from typing import Optional
2
2
 
3
3
  import dstack._internal.core.services.api_client as api_client_service
4
4
  from dstack._internal.core.errors import ConfigurationError
5
- from dstack._internal.core.services.configs import ConfigManager
6
5
  from dstack._internal.utils.logging import get_logger
7
- from dstack._internal.utils.path import PathLike
6
+ from dstack._internal.utils.path import PathLike as PathLike
8
7
  from dstack.api._public.backends import BackendCollection
9
- from dstack.api._public.repos import RepoCollection, get_ssh_keypair
10
- from dstack.api._public.runs import RunCollection
8
+ from dstack.api._public.repos import RepoCollection
9
+ from dstack.api._public.runs import RunCollection, warn
11
10
  from dstack.api.server import APIClient
12
11
 
13
12
  logger = get_logger(__name__)
@@ -35,24 +34,24 @@ class Client:
35
34
  # Args:
36
35
  # api_client: low-level server API client
37
36
  # project_name: project name used for runs
38
- # ssh_identity_file: SSH keypair to access instances
37
+ # ssh_identity_file: deprecated and will be removed in 0.19.40
39
38
  # """
40
39
  self._client = api_client
41
40
  self._project = project_name
42
41
  self._repos = RepoCollection(api_client, project_name)
43
42
  self._backends = BackendCollection(api_client, project_name)
44
43
  self._runs = RunCollection(api_client, project_name, self)
45
- if ssh_identity_file:
46
- self.ssh_identity_file = str(ssh_identity_file)
47
- else:
48
- self.ssh_identity_file = get_ssh_keypair(None, ConfigManager().dstack_key_path)
44
+ if ssh_identity_file is not None:
45
+ warn(
46
+ "[code]ssh_identity_file[/code] in [code]Client[/code] is deprecated and ignored; will be removed"
47
+ " since 0.19.40"
48
+ )
49
49
 
50
50
  @staticmethod
51
51
  def from_config(
52
52
  project_name: Optional[str] = None,
53
53
  server_url: Optional[str] = None,
54
54
  user_token: Optional[str] = None,
55
- ssh_identity_file: Optional[PathLike] = None,
56
55
  ) -> "Client":
57
56
  """
58
57
  Creates a Client using the default configuration from `~/.dstack/config.yml` if it exists.
@@ -61,7 +60,6 @@ class Client:
61
60
  project_name: The name of the project. required if `server_url` and `user_token` are specified.
62
61
  server_url: The dstack server URL (e.g. `http://localhost:3000/` or `https://sky.dstack.ai`).
63
62
  user_token: The dstack user token.
64
- ssh_identity_file: The private SSH key path for SSH tunneling.
65
63
 
66
64
  Returns:
67
65
  A client instance.
@@ -75,7 +73,6 @@ class Client:
75
73
  return Client(
76
74
  api_client=api_client,
77
75
  project_name=project_name,
78
- ssh_identity_file=ssh_identity_file,
79
76
  )
80
77
 
81
78
  @property
@@ -1,4 +1,3 @@
1
- from pathlib import Path
2
1
  from typing import Literal, Optional, Union, overload
3
2
 
4
3
  from git import InvalidGitRepositoryError
@@ -18,7 +17,6 @@ from dstack._internal.core.services.repos import (
18
17
  get_repo_creds_and_default_branch,
19
18
  load_repo,
20
19
  )
21
- from dstack._internal.utils.crypto import generate_rsa_key_pair
22
20
  from dstack._internal.utils.logging import get_logger
23
21
  from dstack._internal.utils.path import PathLike
24
22
  from dstack.api.server import APIClient
@@ -209,22 +207,3 @@ class RepoCollection:
209
207
  return method(self._project, repo_id)
210
208
  except ResourceNotExistsError:
211
209
  return None
212
-
213
-
214
- def get_ssh_keypair(key_path: Optional[PathLike], dstack_key_path: Path) -> str:
215
- """Returns a path to the private key"""
216
- if key_path is not None:
217
- key_path = Path(key_path).expanduser().resolve()
218
- pub_key = (
219
- key_path
220
- if key_path.suffix == ".pub"
221
- else key_path.with_suffix(key_path.suffix + ".pub")
222
- )
223
- private_key = pub_key.with_suffix("")
224
- if pub_key.exists() and private_key.exists():
225
- return str(private_key)
226
- raise ConfigurationError(f"Make sure valid keypair exists: {private_key}(.pub)")
227
-
228
- if not dstack_key_path.exists():
229
- generate_rsa_key_pair(private_key_path=dstack_key_path)
230
- return str(dstack_key_path)
@@ -1,4 +1,6 @@
1
1
  import base64
2
+ import hashlib
3
+ import os
2
4
  import queue
3
5
  import tempfile
4
6
  import threading
@@ -15,6 +17,7 @@ from urllib.parse import urlparse
15
17
  from websocket import WebSocketApp
16
18
 
17
19
  import dstack.api as api
20
+ from dstack._internal.cli.utils.common import warn
18
21
  from dstack._internal.core.consts import DSTACK_RUNNER_HTTP_PORT, DSTACK_RUNNER_SSH_PORT
19
22
  from dstack._internal.core.errors import ClientError, ConfigurationError, ResourceNotExistsError
20
23
  from dstack._internal.core.models.backends.base import BackendType
@@ -45,11 +48,14 @@ from dstack._internal.core.models.runs import (
45
48
  get_service_port,
46
49
  )
47
50
  from dstack._internal.core.models.runs import Run as RunModel
51
+ from dstack._internal.core.models.users import UserWithCreds
52
+ from dstack._internal.core.services.configs import ConfigManager
48
53
  from dstack._internal.core.services.logs import URLReplacer
49
54
  from dstack._internal.core.services.ssh.attach import SSHAttach
50
55
  from dstack._internal.core.services.ssh.ports import PortsLock
51
56
  from dstack._internal.server.schemas.logs import PollLogsRequest
52
57
  from dstack._internal.utils.common import get_or_error, make_proxy_url
58
+ from dstack._internal.utils.crypto import generate_rsa_key_pair
53
59
  from dstack._internal.utils.files import create_file_archive
54
60
  from dstack._internal.utils.logging import get_logger
55
61
  from dstack._internal.utils.path import PathLike, path_in_dir
@@ -72,16 +78,20 @@ class Run(ABC):
72
78
  self,
73
79
  api_client: APIClient,
74
80
  project: str,
75
- ssh_identity_file: Optional[PathLike],
76
81
  run: RunModel,
77
82
  ports_lock: Optional[PortsLock] = None,
83
+ ssh_identity_file: Optional[PathLike] = None,
78
84
  ):
79
85
  self._api_client = api_client
80
86
  self._project = project
81
- self._ssh_identity_file = ssh_identity_file
82
87
  self._run = run
83
88
  self._ports_lock: Optional[PortsLock] = ports_lock
84
89
  self._ssh_attach: Optional[SSHAttach] = None
90
+ if ssh_identity_file is not None:
91
+ warn(
92
+ "[code]ssh_identity_file[/code] in [code]Run[/code] is deprecated and ignored; will be removed"
93
+ " since 0.19.40"
94
+ )
85
95
 
86
96
  @property
87
97
  def name(self) -> str:
@@ -270,9 +280,33 @@ class Run(ABC):
270
280
  Raises:
271
281
  dstack.api.PortUsedError: If ports are in use or the run is attached by another process.
272
282
  """
273
- ssh_identity_file = ssh_identity_file or self._ssh_identity_file
274
- if ssh_identity_file is None:
275
- raise ConfigurationError("SSH identity file is required to attach to the run")
283
+ if not ssh_identity_file:
284
+ user = self._api_client.users.get_my_user()
285
+ run_ssh_key_pub = self._run.run_spec.ssh_key_pub
286
+ config_manager = ConfigManager()
287
+ if isinstance(user, UserWithCreds) and user.ssh_public_key == run_ssh_key_pub:
288
+ token_hash = hashlib.sha1(user.creds.token.encode()).hexdigest()[:8]
289
+ config_manager.dstack_ssh_dir.mkdir(parents=True, exist_ok=True)
290
+ ssh_identity_file = config_manager.dstack_ssh_dir / token_hash
291
+
292
+ def key_opener(path, flags):
293
+ return os.open(path, flags, 0o600)
294
+
295
+ with open(ssh_identity_file, "wb", opener=key_opener) as f:
296
+ assert user.ssh_private_key
297
+ f.write(user.ssh_private_key.encode())
298
+ else:
299
+ if config_manager.dstack_key_path.exists():
300
+ # TODO: Remove since 0.19.40
301
+ warn(
302
+ f"Using legacy [code]{config_manager.dstack_key_path}[/code]."
303
+ " Future versions will use the user SSH key from the server.",
304
+ )
305
+ ssh_identity_file = config_manager.dstack_key_path
306
+ else:
307
+ raise ConfigurationError(
308
+ f"User SSH key doen't match; default SSH key ({config_manager.dstack_key_path}) doesn't exist"
309
+ )
276
310
  ssh_identity_file = str(ssh_identity_file)
277
311
 
278
312
  job = self._find_job(replica_num=replica_num, job_num=job_num)
@@ -434,6 +468,7 @@ class RunCollection:
434
468
  profile: Optional[Profile] = None,
435
469
  configuration_path: Optional[str] = None,
436
470
  repo_dir: Optional[str] = None,
471
+ ssh_identity_file: Optional[PathLike] = None,
437
472
  ) -> RunPlan:
438
473
  """
439
474
  Get a run plan.
@@ -465,6 +500,19 @@ class RunCollection:
465
500
  if repo_dir is None and configuration.repos:
466
501
  repo_dir = configuration.repos[0].path
467
502
 
503
+ if ssh_identity_file:
504
+ ssh_key_pub = Path(ssh_identity_file).with_suffix(".pub").read_text()
505
+ else:
506
+ config_manager = ConfigManager()
507
+ if not config_manager.dstack_key_path.exists():
508
+ generate_rsa_key_pair(private_key_path=config_manager.dstack_key_path)
509
+ warn(
510
+ f"Using legacy [code]{config_manager.dstack_key_path.with_suffix('.pub')}[/code]."
511
+ " Future versions will use the user SSH key from the server.",
512
+ )
513
+ ssh_key_pub = config_manager.dstack_key_path.with_suffix(".pub").read_text()
514
+ # TODO: Uncomment after 0.19.40
515
+ # ssh_key_pub = None
468
516
  run_spec = RunSpec(
469
517
  run_name=configuration.name,
470
518
  repo_id=repo.repo_id,
@@ -477,7 +525,7 @@ class RunCollection:
477
525
  configuration_path=configuration_path,
478
526
  configuration=configuration,
479
527
  profile=profile,
480
- ssh_key_pub=Path(self._client.ssh_identity_file + ".pub").read_text().strip(),
528
+ ssh_key_pub=ssh_key_pub,
481
529
  )
482
530
  logger.debug("Getting run plan")
483
531
  run_plan = self._api_client.runs.get_plan(self._project, run_spec)
@@ -546,6 +594,7 @@ class RunCollection:
546
594
  profile: Optional[Profile] = None,
547
595
  configuration_path: Optional[str] = None,
548
596
  reserve_ports: bool = True,
597
+ ssh_identity_file: Optional[PathLike] = None,
549
598
  ) -> Run:
550
599
  """
551
600
  Apply the run configuration.
@@ -567,6 +616,7 @@ class RunCollection:
567
616
  repo=repo,
568
617
  profile=profile,
569
618
  configuration_path=configuration_path,
619
+ ssh_identity_file=ssh_identity_file,
570
620
  )
571
621
  run = self.apply_plan(
572
622
  run_plan=run_plan,
@@ -709,6 +759,13 @@ class RunCollection:
709
759
  creation_policy=creation_policy,
710
760
  idle_duration=idle_duration, # type: ignore[assignment]
711
761
  )
762
+ config_manager = ConfigManager()
763
+ if not config_manager.dstack_key_path.exists():
764
+ generate_rsa_key_pair(private_key_path=config_manager.dstack_key_path)
765
+ warn(
766
+ f"Using legacy [code]{config_manager.dstack_key_path.with_suffix('.pub')}[/code]."
767
+ " Future versions will use the user SSH key from the server.",
768
+ )
712
769
  run_spec = RunSpec(
713
770
  run_name=run_name,
714
771
  repo_id=repo.repo_id,
@@ -718,7 +775,7 @@ class RunCollection:
718
775
  configuration_path=configuration_path,
719
776
  configuration=configuration,
720
777
  profile=profile,
721
- ssh_key_pub=Path(self._client.ssh_identity_file + ".pub").read_text().strip(),
778
+ ssh_key_pub=config_manager.dstack_key_path.with_suffix(".pub").read_text(),
722
779
  )
723
780
  logger.debug("Getting run plan")
724
781
  run_plan = self._api_client.runs.get_plan(self._project, run_spec)
@@ -800,7 +857,6 @@ class RunCollection:
800
857
  return Run(
801
858
  self._api_client,
802
859
  self._project,
803
- self._client.ssh_identity_file,
804
860
  run,
805
861
  )
806
862
 
@@ -808,7 +864,6 @@ class RunCollection:
808
864
  return Run(
809
865
  self._api_client,
810
866
  self._project,
811
- self._client.ssh_identity_file,
812
867
  run,
813
868
  ports_lock,
814
869
  )
@@ -1,6 +1,6 @@
1
1
  from typing import List
2
2
 
3
- from pydantic import parse_obj_as
3
+ from pydantic import ValidationError, parse_obj_as
4
4
 
5
5
  from dstack._internal.core.models.users import GlobalRole, User, UserWithCreds
6
6
  from dstack._internal.server.schemas.users import (
@@ -18,8 +18,23 @@ class UsersAPIClient(APIClientGroup):
18
18
  return parse_obj_as(List[User.__response__], resp.json())
19
19
 
20
20
  def get_my_user(self) -> User:
21
+ """
22
+ Returns `User` with pre-0.19.33 servers, or `UserWithCreds` with newer servers.
23
+ """
24
+
21
25
  resp = self._request("/api/users/get_my_user")
22
- return parse_obj_as(User.__response__, resp.json())
26
+ try:
27
+ return parse_obj_as(UserWithCreds.__response__, resp.json())
28
+ except ValidationError as e:
29
+ # Compatibility with pre-0.19.33 server
30
+ if (
31
+ len(e.errors()) == 1
32
+ and e.errors()[0]["loc"] == ("__root__", "creds")
33
+ and e.errors()[0]["type"] == "value_error.missing"
34
+ ):
35
+ return parse_obj_as(User.__response__, resp.json())
36
+ else:
37
+ raise
23
38
 
24
39
  def get_user(self, username: str) -> User:
25
40
  body = GetUserRequest(username=username)
dstack/version.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.19.31"
1
+ __version__ = "0.19.33"
2
2
  __is_release__ = True
3
- base_image = "0.11rc2"
3
+ base_image = "0.11"
4
4
  base_image_ubuntu_version = "22.04"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dstack
3
- Version: 0.19.31
3
+ Version: 0.19.33
4
4
  Summary: dstack is an open-source orchestration engine for running AI workloads on any cloud or on-premises.
5
5
  Project-URL: Homepage, https://dstack.ai
6
6
  Project-URL: Source, https://github.com/dstackai/dstack
@@ -22,7 +22,7 @@ Requires-Dist: cryptography
22
22
  Requires-Dist: cursor
23
23
  Requires-Dist: filelock
24
24
  Requires-Dist: gitpython
25
- Requires-Dist: gpuhunt==0.1.8
25
+ Requires-Dist: gpuhunt==0.1.10
26
26
  Requires-Dist: ignore-python>=0.2.0
27
27
  Requires-Dist: jsonschema
28
28
  Requires-Dist: orjson
@@ -73,7 +73,7 @@ Requires-Dist: grpcio>=1.50; extra == 'all'
73
73
  Requires-Dist: httpx; extra == 'all'
74
74
  Requires-Dist: jinja2; extra == 'all'
75
75
  Requires-Dist: kubernetes; extra == 'all'
76
- Requires-Dist: nebius<0.3,>=0.2.40; (python_version >= '3.10') and extra == 'all'
76
+ Requires-Dist: nebius<=0.2.72,>=0.2.40; (python_version >= '3.10') and extra == 'all'
77
77
  Requires-Dist: oci>=2.150.0; extra == 'all'
78
78
  Requires-Dist: prometheus-client; extra == 'all'
79
79
  Requires-Dist: pyopenssl>=23.2.0; extra == 'all'
@@ -259,7 +259,7 @@ Requires-Dist: fastapi; extra == 'nebius'
259
259
  Requires-Dist: grpcio>=1.50; extra == 'nebius'
260
260
  Requires-Dist: httpx; extra == 'nebius'
261
261
  Requires-Dist: jinja2; extra == 'nebius'
262
- Requires-Dist: nebius<0.3,>=0.2.40; (python_version >= '3.10') and extra == 'nebius'
262
+ Requires-Dist: nebius<=0.2.72,>=0.2.40; (python_version >= '3.10') and extra == 'nebius'
263
263
  Requires-Dist: prometheus-client; extra == 'nebius'
264
264
  Requires-Dist: python-dxf==12.1.0; extra == 'nebius'
265
265
  Requires-Dist: python-json-logger>=3.1.0; extra == 'nebius'
@@ -340,15 +340,13 @@ It streamlines development, training, and inference, and is compatible with any
340
340
  `dstack` supports `NVIDIA`, `AMD`, `Google TPU`, `Intel Gaudi`, and `Tenstorrent` accelerators out of the box.
341
341
 
342
342
  ## Latest news ✨
343
- - [2025/09] [dstack 0.19.27: Offers UI, Digital Ocean and AMD Developer Cloud](https://github.com/dstackai/dstack/releases/tag/0.19.27)
344
- - [2025/08] [dstack 0.19.26: Repos – explicit repo configuration via YAML](https://github.com/dstackai/dstack/releases/tag/0.19.26)
345
- - [2025/08] [dstack 0.19.25: `dstack offer` CLI command](https://github.com/dstackai/dstack/releases/tag/0.19.25)
346
- - [2025/08] [dstack 0.19.22: Service probes, GPU health-checks, Tenstorrent Galaxy, Secrets UI](https://github.com/dstackai/dstack/releases/tag/0.19.22)
343
+ - [2025/10] [dstack 0.19.31: Kubernetes, GCP A4 spot](https://github.com/dstackai/dstack/releases/tag/0.19.31)
344
+ - [2025/08] [dstack 0.19.26: Repos](https://github.com/dstackai/dstack/releases/tag/0.19.26)
345
+ - [2025/08] [dstack 0.19.22: Service probes, GPU health-checks, Tenstorrent Galaxy](https://github.com/dstackai/dstack/releases/tag/0.19.22)
347
346
  - [2025/07] [dstack 0.19.21: Scheduled tasks](https://github.com/dstackai/dstack/releases/tag/0.19.21)
348
347
  - [2025/07] [dstack 0.19.17: Secrets, Files, Rolling deployment](https://github.com/dstackai/dstack/releases/tag/0.19.17)
349
- - [2025/06] [dstack 0.19.16: Docker in Docker, CloudRift](https://github.com/dstackai/dstack/releases/tag/0.19.16)
350
- - [2025/06] [dstack 0.19.13: InfiniBand support in default images](https://github.com/dstackai/dstack/releases/tag/0.19.13)
351
- - [2025/06] [dstack 0.19.12: Simplified use of MPI](https://github.com/dstackai/dstack/releases/tag/0.19.12)
348
+ - [2025/06] [dstack 0.19.16: Docker in Docker](https://github.com/dstackai/dstack/releases/tag/0.19.16)
349
+ - [2025/06] [dstack 0.19.13: Default images with InfiniBand support](https://github.com/dstackai/dstack/releases/tag/0.19.13)
352
350
 
353
351
  ## How does it work?
354
352
 
@@ -364,11 +362,11 @@ It streamlines development, training, and inference, and is compatible with any
364
362
 
365
363
  To orchestrate compute across cloud providers or existing Kubernetes clusters, you need to configure backends.
366
364
 
367
- Backends can be set up in `~/.dstack/server/config.yml` or through the [project settings page](../concepts/projects.md#backends) in the UI.
365
+ Backends can be set up in `~/.dstack/server/config.yml` or through the [project settings page](https://dstack.ai/docs/concepts/projects#backends) in the UI.
368
366
 
369
- For more details, see [Backends](../concepts/backends.md).
367
+ For more details, see [Backends](https://dstack.ai/docs/concepts/backends).
370
368
 
371
- > When using `dstack` with on-prem servers, backend configuration isn’t required. Simply create [SSH fleets](../concepts/fleets.md#ssh) once the server is up.
369
+ > When using `dstack` with on-prem servers, backend configuration isn’t required. Simply create [SSH fleets](https://dstack.ai/docs/concepts/fleets#ssh) once the server is up.
372
370
 
373
371
  ##### Start the server
374
372