Skip to content

Commit 2edd4ef

Browse files
committed
Merge remote-tracking branch 'origin/master' into codex/pr752-dstack-volume
# Conflicts: # dstack/crates/dstack-cli-core/src/compose.rs # dstack/crates/dstack-cli/src/main.rs
2 parents 517eec9 + 2c2604b commit 2edd4ef

17 files changed

Lines changed: 437 additions & 44 deletions

File tree

docs/nerdctl-compose.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# nerdctl Compose and lazy image pulling
2+
3+
dstack supports two independent Compose runners. The runner is part of
4+
`app-compose.json`, so it is included in the compose hash and attestation.
5+
6+
| `runner` | Image manager | `snapshotter` support |
7+
|---|---|---|
8+
| `docker-compose` | Docker Engine | none (Docker's overlayfs store) |
9+
| `nerdctl-compose` | containerd | `overlayfs` or `stargz` |
10+
11+
## Deploy with stargz
12+
13+
Build and push an eStargz image before deployment. For example, with Buildx:
14+
15+
```bash
16+
docker buildx build -t registry.example.com/example/app:estargz \
17+
--output type=registry,oci-mediatypes=true,compression=estargz,force-compression=true \
18+
.
19+
```
20+
21+
Reference that image from `docker-compose.yaml`, then deploy it with:
22+
23+
```bash
24+
dstack deploy -c docker-compose.yaml \
25+
--runner nerdctl-compose \
26+
--snapshotter stargz
27+
```
28+
29+
The resulting application manifest contains:
30+
31+
```json
32+
{
33+
"manifest_version": "3",
34+
"runner": "nerdctl-compose",
35+
"snapshotter": "stargz"
36+
}
37+
```
38+
39+
Use containerd without lazy pulling by selecting overlayfs:
40+
41+
```bash
42+
dstack deploy -c docker-compose.yaml \
43+
--runner nerdctl-compose \
44+
--snapshotter overlayfs
45+
```
46+
47+
If `snapshotter` is omitted for `nerdctl-compose`, it defaults to `overlayfs`.
48+
Setting `snapshotter` with `docker-compose` is rejected rather than silently
49+
ignored.
50+
51+
## Compatibility
52+
53+
`nerdctl compose` implements the commonly used Docker Compose features, but it
54+
is not a drop-in implementation of every Docker-specific extension. Test
55+
applications that use Docker socket mounts, custom runtimes, or advanced
56+
networking before switching runners. The `nerdctl-compose` runner requires
57+
pre-built images and rejects Compose `build` sections. This avoids depending
58+
on an in-guest BuildKit daemon and ensures lazy-pull images were converted
59+
before deployment.
60+
61+
Both backends keep their own image and container metadata. Changing the runner
62+
recreates the application through the selected backend; it does not migrate
63+
existing Docker containers into containerd.

dstack/crates/dstack-cli-core/src/compose.rs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,48 @@ use serde_json::json;
1212
///
1313
/// `kms_enabled` selects KMS mode (deterministic, upgradeable per-app keys);
1414
/// gateway and local-key-provider are off for the direct-port single-node flow.
15-
///
16-
/// Each `verity_volumes` entry is measured, so the CVM only mounts content
17-
/// matching the attested root. Empty for a normal deploy.
18-
pub fn build_app_compose(
15+
pub fn build_app_compose(name: &str, docker_compose_yaml: &str, kms_enabled: bool) -> String {
16+
build_app_compose_with_runtime(
17+
name,
18+
docker_compose_yaml,
19+
kms_enabled,
20+
"docker-compose",
21+
None,
22+
)
23+
}
24+
25+
/// Build an app-compose manifest with an explicitly selected compose frontend.
26+
/// `snapshotter` is meaningful only for `nerdctl-compose`.
27+
pub fn build_app_compose_with_runtime(
28+
name: &str,
29+
docker_compose_yaml: &str,
30+
kms_enabled: bool,
31+
runner: &str,
32+
snapshotter: Option<&str>,
33+
) -> String {
34+
build_app_compose_with_runtime_and_volumes(
35+
name,
36+
docker_compose_yaml,
37+
kms_enabled,
38+
runner,
39+
snapshotter,
40+
&[],
41+
)
42+
}
43+
44+
/// Build an app-compose manifest with measured verity volume declarations.
45+
pub fn build_app_compose_with_runtime_and_volumes(
1946
name: &str,
2047
docker_compose_yaml: &str,
2148
kms_enabled: bool,
49+
runner: &str,
50+
snapshotter: Option<&str>,
2251
verity_volumes: &[dstack_types::VerityVolume],
2352
) -> String {
2453
let mut manifest = json!({
25-
"manifest_version": 2,
54+
"manifest_version": if runner == "nerdctl-compose" { json!("3") } else { json!(2) },
2655
"name": name,
27-
"runner": "docker-compose",
56+
"runner": runner,
2857
"docker_compose_file": docker_compose_yaml,
2958
"kms_enabled": kms_enabled,
3059
"gateway_enabled": false,
@@ -39,6 +68,9 @@ pub fn build_app_compose(
3968
// (NTS is also currently broken in guest images — see dstack#745.)
4069
"secure_time": false,
4170
});
71+
if let Some(snapshotter) = snapshotter {
72+
manifest["snapshotter"] = json!(snapshotter);
73+
}
4274
if !verity_volumes.is_empty() {
4375
manifest["verity_volumes"] = json!(verity_volumes);
4476
}

dstack/crates/dstack-cli/src/main.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,37 @@ struct Cli {
4747
command: Command,
4848
}
4949

50+
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
51+
enum ComposeRunner {
52+
#[default]
53+
DockerCompose,
54+
NerdctlCompose,
55+
}
56+
57+
impl ComposeRunner {
58+
fn as_str(self) -> &'static str {
59+
match self {
60+
Self::DockerCompose => "docker-compose",
61+
Self::NerdctlCompose => "nerdctl-compose",
62+
}
63+
}
64+
}
65+
66+
#[derive(Clone, Copy, Debug, ValueEnum)]
67+
enum Snapshotter {
68+
Overlayfs,
69+
Stargz,
70+
}
71+
72+
impl Snapshotter {
73+
fn as_str(self) -> &'static str {
74+
match self {
75+
Self::Overlayfs => "overlayfs",
76+
Self::Stargz => "stargz",
77+
}
78+
}
79+
}
80+
5081
#[derive(Subcommand)]
5182
enum Command {
5283
/// Deploy an app from a docker-compose file.
@@ -90,6 +121,12 @@ enum Command {
90121
/// build + hash the compose and print it, without deploying.
91122
#[arg(long)]
92123
dry_run: bool,
124+
/// compose frontend used inside the guest.
125+
#[arg(long, value_enum, default_value = "docker-compose")]
126+
runner: ComposeRunner,
127+
/// containerd snapshotter (supported only with --runner nerdctl-compose).
128+
#[arg(long, value_enum)]
129+
snapshotter: Option<Snapshotter>,
93130
},
94131
/// List deployed apps.
95132
Apps,
@@ -200,6 +237,8 @@ async fn main() -> Result<()> {
200237
no_kms,
201238
allowlist,
202239
dry_run,
240+
runner,
241+
snapshotter,
203242
} => {
204243
let compose = resolve_compose_arg(compose, compose_file)?;
205244
let image = if use_local_defaults {
@@ -231,6 +270,8 @@ async fn main() -> Result<()> {
231270
allowlist.as_deref(),
232271
dry_run,
233272
json,
273+
runner,
274+
snapshotter,
234275
)
235276
.await
236277
}
@@ -408,7 +449,12 @@ async fn cmd_deploy(
408449
allowlist: Option<&str>,
409450
dry_run: bool,
410451
json: bool,
452+
runner: ComposeRunner,
453+
snapshotter: Option<Snapshotter>,
411454
) -> Result<()> {
455+
if matches!(runner, ComposeRunner::DockerCompose) && snapshotter.is_some() {
456+
bail!("--snapshotter is only supported with --runner nerdctl-compose");
457+
}
412458
let yaml = fs::read_to_string(compose_path)
413459
.with_context(|| format!("reading compose file '{compose_path}'"))?;
414460

@@ -424,7 +470,14 @@ async fn cmd_deploy(
424470

425471
// each --volume declares a measured verity_volumes entry, so the built
426472
// app-compose (and thus app_id) binds the attested roots.
427-
let app_compose = compose::build_app_compose(name, &yaml, !no_kms, &parsed_volumes);
473+
let app_compose = compose::build_app_compose_with_runtime_and_volumes(
474+
name,
475+
&yaml,
476+
!no_kms,
477+
runner.as_str(),
478+
snapshotter.map(Snapshotter::as_str),
479+
&parsed_volumes,
480+
);
428481

429482
let mut cfg = rpc::VmConfiguration {
430483
name: name.to_string(),

dstack/dstack-types/src/lib.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ pub struct AppCompose {
8080
#[serde(default)]
8181
pub features: Vec<String>,
8282
pub runner: String,
83+
/// containerd snapshotter used by the `nerdctl-compose` runner.
84+
/// The field is invalid for other runners.
85+
#[serde(default, skip_serializing_if = "Option::is_none")]
86+
pub snapshotter: Option<ContainerSnapshotter>,
8387
#[serde(default)]
8488
pub docker_compose_file: Option<String>,
8589
#[serde(default)]
@@ -193,6 +197,13 @@ mod verity_volume_tests {
193197
}
194198
}
195199

200+
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
201+
#[serde(rename_all = "kebab-case")]
202+
pub enum ContainerSnapshotter {
203+
Overlayfs,
204+
Stargz,
205+
}
206+
196207
/// Canonical source for the policy used when `requirements.gpu_policy` is
197208
/// absent. Both typed defaults and measurement are derived from this JSON.
198209
pub const DEFAULT_GPU_POLICY: &str = "{}";
@@ -517,6 +528,26 @@ mod app_compose_tests {
517528
assert_eq!(compose.manifest_version_u32(), Some(3));
518529
}
519530

531+
#[test]
532+
fn parses_supported_container_snapshotters() {
533+
let compose: AppCompose = serde_json::from_value(serde_json::json!({
534+
"manifest_version": "3",
535+
"name": "test",
536+
"runner": "nerdctl-compose",
537+
"snapshotter": "stargz"
538+
}))
539+
.unwrap();
540+
assert_eq!(compose.snapshotter, Some(ContainerSnapshotter::Stargz));
541+
542+
let invalid = serde_json::from_value::<AppCompose>(serde_json::json!({
543+
"manifest_version": "3",
544+
"name": "test",
545+
"runner": "nerdctl-compose",
546+
"snapshotter": "unknown"
547+
}));
548+
assert!(invalid.is_err());
549+
}
550+
520551
#[test]
521552
fn manifest_version_accepts_legacy_numeric_1_and_2() {
522553
assert_eq!(

dstack/dstack-util/src/system_setup.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,14 @@ fn verify_manifest_feature_requirements(app_compose: &AppCompose) -> Result<()>
839839
"requirements requires manifest_version >= {MANIFEST_VERSION_3}; use string manifest_version \"{MANIFEST_VERSION_3}\" so older guests fail closed"
840840
);
841841
}
842+
if app_compose.runner == "nerdctl-compose" && manifest_version < MANIFEST_VERSION_3 {
843+
bail!(
844+
"nerdctl-compose requires manifest_version >= {MANIFEST_VERSION_3}; use string manifest_version \"{MANIFEST_VERSION_3}\" so older guests fail closed"
845+
);
846+
}
847+
if app_compose.runner != "nerdctl-compose" && app_compose.snapshotter.is_some() {
848+
bail!("snapshotter is only supported by the nerdctl-compose runner");
849+
}
842850
Ok(())
843851
}
844852

@@ -3162,6 +3170,27 @@ fn test_os_version_requirement_requires_v3_manifest() {
31623170
assert!(err.to_string().contains("requires manifest_version"));
31633171
}
31643172

3173+
#[test]
3174+
fn test_nerdctl_compose_requires_v3_manifest() {
3175+
let mut app_compose = test_app_compose(serde_json::json!(2), None, None);
3176+
app_compose.runner = "nerdctl-compose".to_string();
3177+
let err = verify_manifest_feature_requirements(&app_compose).unwrap_err();
3178+
assert!(err.to_string().contains("nerdctl-compose requires"));
3179+
3180+
app_compose.manifest_version = "3".to_string();
3181+
verify_manifest_feature_requirements(&app_compose).unwrap();
3182+
}
3183+
3184+
#[test]
3185+
fn test_snapshotter_is_rejected_for_other_runners() {
3186+
let mut app_compose = test_app_compose(serde_json::json!("3"), None, None);
3187+
app_compose.snapshotter = Some(dstack_types::ContainerSnapshotter::Stargz);
3188+
let err = verify_manifest_feature_requirements(&app_compose).unwrap_err();
3189+
assert!(err
3190+
.to_string()
3191+
.contains("snapshotter is only supported by the nerdctl-compose runner"));
3192+
}
3193+
31653194
#[test]
31663195
fn test_os_version_requirement_rejects_too_old_os() {
31673196
let app_compose = test_app_compose(serde_json::json!("3"), Some(">=0.6.1"), None);

dstack/guest-agent/src/rpc_service.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ mod tests {
735735
name: String::new(),
736736
features: Vec::new(),
737737
runner: String::new(),
738+
snapshotter: None,
738739
docker_compose_file: None,
739740
public_logs: false,
740741
public_sysinfo: false,

os/common/rootfs/app-compose.service

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
[Unit]
22
Description=App Compose Service
3-
Wants=docker.service
4-
After=docker.service dstack-prepare.service dstack-guest-agent.service
3+
Wants=docker.service containerd.service containerd-stargz-grpc.service
4+
After=docker.service containerd.service containerd-stargz-grpc.service dstack-prepare.service dstack-guest-agent.service
55

66
[Service]
77
Type=oneshot
88
RemainAfterExit=true
99
EnvironmentFile=-/dstack/.host-shared/.decrypted-env
1010
WorkingDirectory=/dstack
1111
ExecStart=/bin/app-compose.sh
12-
ExecStop=/bin/docker compose stop
12+
ExecStop=/bin/app-compose.sh stop
1313
StandardOutput=journal+console
1414
StandardError=journal+console
1515

0 commit comments

Comments
 (0)