Skip to content

Service

Source: src/Docker/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. Configuration changes replace the service (delete-then-create); swarm tasks are stateless, so replacement is cheap and avoids partially-applied service update drift.

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")),
};
}),
) {}
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" }],
});