Skip to content

Docker.Swarm reference

Source: packages/docker/src/Service.ts

A Docker Swarm service: N replicas of a container kept alive by the swarm, deployed through the active (or a named) Docker context.

The target engine must be a swarm manager — Service wraps docker service, swarm mode’s orchestration API. Declare the swarm with Docker.Swarm and pass it as context so the service deploys after the swarm exists; for a plain single container on a non-swarm daemon use Docker.Container instead.

The service’s image comes from one of two sources:

  • image — run a pre-built reference (a registry ref, or a Docker.Image / Docker.RemoteImage resource).
  • main — bundle an inline Effect program into a generated bun image, built directly against the service’s Docker context. The impl returns { fetch } and may register background loops via ServerHost.run — the same effectful platform shape as AWS.ECS.Service.

The bundled image is content-addressed and only rebuilt when the program (or its generated Dockerfile) changes. It is built on the target engine’s local store — single-node swarms run it as-is; multi-node swarms need the image on a registry every node can reach (build with Docker.Image + registry and pass the pushed ref as image instead).

Only replicated services are supported. Changes to the image, replicas, environment, labels, networks, ports, mounts, secrets, configs, constraints, healthcheck, and rollout or restart policies are applied in place with docker service update, so swarm rolls tasks over following updateConfig. An update that swarm rolls back or pauses fails the deploy with DockerServiceUpdateFailed. Changing the name, context, command, args, endpoint mode, placement preferences, or read-only root filesystem, or removing a policy setting, replaces the service (delete-then-create).

Replicated Nginx

const swarm = yield* Docker.Swarm("swarm");
const web = yield* Docker.Service("web", {
context: swarm,
image: "nginx:alpine",
replicas: 3,
ports: [{ external: 8080, internal: 80 }],
});

Run a Built Image

const image = yield* Docker.Image("app-image", {
build: { context: "./app" },
});
const app = yield* Docker.Service("app", {
context: swarm,
image,
replicas: 2,
});

Inline Effect Server

const swarm = yield* Docker.Swarm("swarm");
const api = yield* Docker.Service(
"Api",
{
context: swarm,
main: import.meta.url,
port: 3000,
ports: [{ external: 8080, internal: 3000 }],
replicas: 2,
},
Effect.gen(function* () {
return {
fetch: Effect.gen(function* () {
return yield* HttpServerResponse.json({ ok: true });
}),
};
}),
);

Background Loops with ServerHost

// Class props may be an Effect, so the service can yield the swarm it
// deploys into (declared once at module level).
const Swarm = Docker.Swarm("swarm");
export default class Worker extends Docker.Service<Worker>()(
"Worker",
Effect.gen(function* () {
const swarm = yield* Swarm;
return { context: swarm, main: import.meta.url, port: 3000 };
}),
Effect.gen(function* () {
const host = yield* ServerHost;
yield* host.run(
pollQueue.pipe(Effect.repeat(Schedule.spaced("5 seconds")), Effect.asVoid),
);
return {
fetch: Effect.succeed(HttpServerResponse.text("ok")),
};
}),
) {}

main is bundled with rolldown at deploy time. Unused code is tree-shaken. effect, alchemy, and @distilled.cloud are marked pure so unused parts prune more aggressively. Your app is not marked pure.

Mark additional packages as pure

Only list packages with no top-level side effects.

{
main: import.meta.url,
build: {
pure: { packages: ["my-lib", "@my-scope/*"] },
},
}

Turn it off

{
main: import.meta.url,
build: { pure: false },
}
const vps = yield* Docker.Context("vps", {
docker: "host=ssh://deploy@example.com",
});
const swarm = yield* Docker.Swarm("swarm", {
context: vps,
advertiseAddr: "10.0.0.1",
});
const app = yield* Docker.Service("app", {
context: swarm,
image: "nginx:alpine",
replicas: 3,
});
const network = yield* Docker.Network("app-net", {
context: swarm,
driver: "overlay",
});
const db = yield* Docker.Service("db", {
context: swarm,
image: "postgres:18-alpine",
networks: [{ name: network.name, aliases: ["postgres"] }],
volumes: [{ hostPath: "pg-data", containerPath: "/var/lib/postgresql/data" }],
});
const app = yield* Docker.Service("app", {
image: "ghcr.io/acme/app:latest",
replicas: 4,
updateConfig: {
parallelism: 1,
delay: "10s",
failureAction: "rollback",
order: "start-first",
},
placement: {
constraints: ["node.role==worker"],
maxReplicasPerNode: 2,
},
});
const app = yield* Docker.Service("app", {
image: "ghcr.io/acme/app:latest",
secrets: [{ source: "db-password", target: "db_password", mode: 0o400 }],
configs: [{ source: "app-config", target: "/etc/app/config.yaml" }],
});

Source: packages/docker/src/Swarm.ts

Swarm mode on a Docker engine — an idempotent docker swarm init.

Turns the engine behind the given context (or the local engine) into a single-node swarm: the node becomes a manager that can run Docker.Service workloads. Regular (non-swarm) docker usage of the engine is unaffected.

Pass the swarm as the context of a Docker.Service or overlay Docker.Network: the workload then deploys after the swarm exists and inherits its Docker context.

An engine that is already in swarm mode is treated as foreign — adopt it with adopt(true) (or --adopt) to manage it. Destroying the resource dissolves the node’s swarm membership (docker swarm leave --force), which stops every service running on it — services managed by the same stack are destroyed first via their dependency edges.

Growing the cluster beyond one node is host-level setup: run docker swarm join --token <token> <manager-ip>:2377 on each additional machine (docker swarm join-token worker on the manager prints the command). Alchemy manages the swarm’s workloads through the manager.

Local single-node swarm

const swarm = yield* Docker.Swarm("swarm");

Remote engine over SSH

const vps = yield* Docker.Context("vps", {
docker: "host=ssh://deploy@example.com",
});
const swarm = yield* Docker.Swarm("swarm", {
context: vps,
advertiseAddr: "10.0.0.1",
});

Reference without owning

// Services don't require a Swarm resource — point them at an engine that
// is already a manager and the swarm's lifecycle stays external: destroy
// removes the services, never the swarm.
const web = yield* Docker.Service("web", {
context: vps,
image: "nginx:alpine",
});

Adopt an already-initialized engine

// Adoption makes the swarm part of the stack — destroy then dissolves
// the node's membership.
const swarm = yield* Docker.Swarm("swarm").pipe(adopt(true));
const swarm = yield* Docker.Swarm("swarm");
const web = yield* Docker.Service("web", {
context: swarm,
image: "nginx:alpine",
replicas: 2,
ports: [{ external: 8080, internal: 80 }],
});