Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 48 additions & 1 deletion docs/STACKER_YML_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,16 @@ Additional containers deployed alongside your main application — databases, ca
| `environment` | `map<string, string>` | no | `{}` | Environment variables |
| `volumes` | `string[]` | no | `[]` | Volume mounts (`"name:/path"` or `"./host:/container"`) |
| `depends_on` | `string[]` | no | `[]` | Services this depends on (started first) |
| `command` | `string` | no | — | Override the container's default command / entrypoint |
| `command` | `string` | no | — | Override the container's default command |
| `entrypoint` | `string` | no | — | Override the image's entrypoint |
| `healthcheck` | `object` | no | — | Docker health check for this service |
| `restart` | `string` | no | `unless-stopped` | `no`, `always`, `unless-stopped` or `on-failure` |
| `cap_add`, `cap_drop` | `string[]` | no | `[]` | Linux capabilities to add or drop |
| `devices` | `string[]` | no | `[]` | Host devices to map (`/dev/dri`) |
| `profiles` | `string[]` | no | `[]` | Start the service only when one of these compose profiles is active |
| `aliases` | `string[]` | no | `[]` | Other names the service answers to, on every network it joins |
| `deploy.replicas` | `integer` | no | `1` | `0` keeps the service defined but not started |
| `deploy.resources.reservations.devices` | `object[]` | no | `[]` | Devices to reserve, e.g. a GPU |

#### `services[].command`

Expand Down Expand Up @@ -500,6 +508,45 @@ services:
> **Note:** Stacker detects port conflicts across services during validation.
> If two services bind the same host port, you'll get a warning (`W001`).

#### One-shot jobs, profiles and GPUs

`restart: "no"` keeps a job that runs once from being restarted after it
finishes. `profiles`, `aliases` and `deploy` let one stack carry a CPU and a GPU
variant of the same service: the CPU variant runs by default, the GPU variant
only under its profile, and both answer to the same name.

```yaml
services:
- name: ollama-cpu
image: ollama/ollama:0.40.4
aliases: [ollama]
deploy:
replicas: ${OLLAMA_CPU_REPLICAS} # 0 on a GPU machine

- name: ollama-gpu
image: ollama/ollama:0.40.4
profiles: [gpu-nvidia]
aliases: [ollama]
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]

- name: model-pull
image: ollama/ollama:0.40.4
restart: "no"
entrypoint: /bin/sh
command: -c "ollama pull nomic-embed-text"
```

On a GPU machine set `OLLAMA_CPU_REPLICAS=0` and `COMPOSE_PROFILES=gpu-nvidia`.
These settings reach every compose stacker writes from the service: the one the
CLI deploys, the author's own compose (`deploy.compose_file`), the one the
server renders from the project, and the one the status-panel agent applies.

---

## `proxy`
Expand Down
1 change: 1 addition & 0 deletions migrations/20261016100000_project_app_runtime.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE project_app DROP COLUMN IF EXISTS runtime;
4 changes: 4 additions & 0 deletions migrations/20261016100000_project_app_runtime.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Compose settings of an app beyond its image and wiring: cap_add, cap_drop,
-- devices, profiles, aliases, deploy (replicas, device reservations).
-- See shared-fixtures/api-contracts/stack-service-runtime-fields.json.
ALTER TABLE project_app ADD COLUMN IF NOT EXISTS runtime JSONB;
52 changes: 49 additions & 3 deletions src/cli/compose_service_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,13 +375,56 @@ fn service_to_compose_value(
serde_yaml::Value::String(platform.clone()),
);
}
if !project_networks.is_empty() {
insert_string_sequence(&mut map, "networks", project_networks);
let runtime = &service.runtime;
if runtime.aliases.is_empty() {
if !project_networks.is_empty() {
insert_string_sequence(&mut map, "networks", project_networks);
}
} else {
// Aliases need the long form, on every network the service joins.
let default_network = ["default".to_string()];
let networks = if project_networks.is_empty() {
&default_network[..]
} else {
project_networks
};
let mut settings = serde_yaml::Mapping::new();
insert_string_sequence(&mut settings, "aliases", &runtime.aliases);
let networks: serde_yaml::Mapping = networks
.iter()
.map(|n| {
(
serde_yaml::Value::String(n.clone()),
serde_yaml::Value::Mapping(settings.clone()),
)
})
.collect();
map.insert(
serde_yaml::Value::String("networks".to_string()),
serde_yaml::Value::Mapping(networks),
);
}
map.insert(
serde_yaml::Value::String("restart".to_string()),
serde_yaml::Value::String("unless-stopped".to_string()),
serde_yaml::Value::String(
runtime
.restart
.map_or("unless-stopped", |r| r.as_str())
.to_string(),
),
);
if let Some(entrypoint) = &runtime.entrypoint {
map.insert(
serde_yaml::Value::String("entrypoint".to_string()),
serde_yaml::Value::String(entrypoint.clone()),
);
}
insert_string_sequence(&mut map, "profiles", &runtime.profiles);
if let Some(deploy) = &runtime.deploy {
if let Ok(value) = serde_yaml::to_value(deploy) {
map.insert(serde_yaml::Value::String("deploy".to_string()), value);
}
}
serde_yaml::Value::Mapping(map)
}

Expand Down Expand Up @@ -732,6 +775,7 @@ mod tests {
config: None,
},
services: vec![ServiceDefinition {
runtime: Default::default(),
name: service_name.to_string(),
image: "myapp:latest".to_string(),
ports: vec!["3000:3000".to_string()],
Expand Down Expand Up @@ -787,6 +831,7 @@ mod tests {
// proxy points to "api" but we are syncing "smtp"
let mut config = npm_stacker_config(dir.path(), "api");
config.services = vec![ServiceDefinition {
runtime: Default::default(),
name: "smtp".to_string(),
image: "trydirect/smtp".to_string(),
ports: vec![],
Expand Down Expand Up @@ -852,6 +897,7 @@ volumes:
..Default::default()
},
services: vec![ServiceDefinition {
runtime: Default::default(),
name: "smtp".to_string(),
image: "trydirect/smtp".to_string(),
ports: vec!["1025:25".to_string()],
Expand Down
1 change: 1 addition & 0 deletions src/cli/compose_targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ fn collect_compose_services(
};

services.push(ServiceDefinition {
runtime: Default::default(),
name: service_name.to_string(),
image,
ports: mapping_sequence(definition, "ports")
Expand Down
Loading
Loading