[{"data":1,"prerenderedAt":2159},["ShallowReactive",2],{"docs-nav":3,"docs:\u002Fdocs\u002Fhow-to\u002Fswarm-cluster":139},[4,7,10,13,16,19,22,25,28,31,34,37,40,43,46,49,52,55,58,61,64,67,70,73,76,79,82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,127,130,133,136],{"path":5,"title":6},"\u002Fdocs\u002Faccess-gating","Protect a local route",{"path":8,"title":9},"\u002Fdocs\u002Farchitecture\u002Fsecurity-and-trust","Security and trust",{"path":11,"title":12},"\u002Fdocs\u002Fconcepts\u002Fbuilds-and-deployments","Build and deploy",{"path":14,"title":15},"\u002Fdocs\u002Fconcepts\u002Fconfiguration-ownership","Where configuration belongs",{"path":17,"title":18},"\u002Fdocs\u002Fconcepts\u002Fenvironments-and-secrets","Environment values and secrets",{"path":20,"title":21},"\u002Fdocs\u002Fconcepts\u002Fmental-model","How projects fit together",{"path":23,"title":24},"\u002Fdocs\u002Fconcepts\u002Fplans-and-apply","Plans and apply",{"path":26,"title":27},"\u002Fdocs\u002Fconcepts\u002Ftargets-and-runtimes","Choose a target and runtime",{"path":29,"title":30},"\u002Fdocs\u002Fcontribute\u002Freleases","Publish a release",{"path":32,"title":33},"\u002Fdocs\u002Fdevelopment-mounts","Development mounts",{"path":35,"title":36},"\u002Fdocs\u002Fgetting-started","Getting started",{"path":38,"title":39},"\u002Fdocs\u002Fhelp\u002Ffaq","FAQ",{"path":41,"title":42},"\u002Fdocs\u002Fhelp\u002Ftroubleshooting","Troubleshooting",{"path":44,"title":45},"\u002Fdocs\u002Fhow-to\u002Fdeploy-production","Prepare a deployment environment",{"path":47,"title":48},"\u002Fdocs\u002Fhow-to\u002Fswarm-cluster","Run on a multi-node Swarm",{"path":50,"title":51},"\u002Fdocs","mm documentation",{"path":53,"title":54},"\u002Fdocs\u002Fjobs-and-schedules","Jobs and schedules",{"path":56,"title":57},"\u002Fdocs\u002Flocal-dashboard","Local dashboard",{"path":59,"title":60},"\u002Fdocs\u002Flocal-https","Local HTTPS",{"path":62,"title":63},"\u002Fdocs\u002Flocal-services","Local services",{"path":65,"title":66},"\u002Fdocs\u002Fmcp","Let an AI agent work on your project",{"path":68,"title":69},"\u002Fdocs\u002Fmulti-project-hosts","Run multiple projects on one host",{"path":71,"title":72},"\u002Fdocs\u002Fonboarding","Set up a project",{"path":74,"title":75},"\u002Fdocs\u002Fopenbao","Use OpenBao",{"path":77,"title":78},"\u002Fdocs\u002Fproject\u002Fcontributing","Contributing",{"path":80,"title":81},"\u002Fdocs\u002Fproject\u002Flicense","License",{"path":83,"title":84},"\u002Fdocs\u002Fproject\u002Fsecurity","Security",{"path":86,"title":87},"\u002Fdocs\u002Freference\u002Fcapability-matrix","Supported features and platforms",{"path":89,"title":90},"\u002Fdocs\u002Freference\u002Fcli","CLI command reference",{"path":92,"title":93},"\u002Fdocs\u002Freference\u002Fconfiguration","Configuration",{"path":95,"title":96},"\u002Fdocs\u002Freference\u002Fenvironment-variables","Environment variables",{"path":98,"title":99},"\u002Fdocs\u002Freference\u002Ferrors-and-exit-codes","Errors and exit codes",{"path":101,"title":102},"\u002Fdocs\u002Freference\u002Fknown-issues","Known limitations",{"path":104,"title":105},"\u002Fdocs\u002Fremote-access","Manage remote access",{"path":107,"title":108},"\u002Fdocs\u002Fremote-daemon","Set up a remote host",{"path":110,"title":111},"\u002Fdocs\u002Fruntime-observability","Status, logs, and commands",{"path":113,"title":114},"\u002Fdocs\u002Fsecret-delivery","Pass secrets to your app",{"path":116,"title":117},"\u002Fdocs\u002Fsecret-providers","Choose a secret store",{"path":119,"title":120},"\u002Fdocs\u002Fstart\u002Finstall","Install mm",{"path":122,"title":123},"\u002Fdocs\u002Fterminal-ui","Terminal UI",{"path":125,"title":126},"\u002Fdocs\u002Ftutorials\u002Fdotnet-web-api",".NET web API example",{"path":128,"title":129},"\u002Fdocs\u002Ftutorials\u002Ffirst-local-app","First local app",{"path":131,"title":132},"\u002Fdocs\u002Ftutorials\u002Flaravel-rust","Laravel and Rust example",{"path":134,"title":135},"\u002Fdocs\u002Ftutorials\u002Fnode-fullstack","Node full stack example",{"path":137,"title":138},"\u002Fdocs\u002Fvault","Use HashiCorp Vault",{"id":140,"title":48,"body":141,"description":2153,"extension":2154,"meta":2155,"navigation":1477,"path":47,"seo":2156,"stem":2157,"__hash__":2158},"docs\u002Fdocs\u002Fhow-to\u002Fswarm-cluster.md",{"type":142,"value":143,"toc":2132},"minimark",[144,153,156,161,195,199,217,356,359,362,444,447,463,471,474,478,499,527,573,591,610,613,626,631,644,648,655,722,727,791,797,801,804,827,835,866,880,884,891,928,1189,1195,1219,1235,1239,1242,1291,1309,1325,1328,1349,1354,1368,1372,1383,1386,1390,1393,1522,1585,1588,1591,1628,1631,1698,1707,1711,1714,1747,1750,1756,1763,1767,1773,1788,1794,1872,1900,1904,1907,1918,1938,1944,1953,1956,1959,1986,1990,1997,2001,2025,2029,2128],[145,146,147,148,152],"p",{},"mm can deploy to a Docker Swarm cluster with several managers. You choose where\neach service runs with one word in ",[149,150,151],"code",{},".config\u002Fmm.yaml",". mm writes the Swarm\nconstraints, keeps the node labels, and runs a built-in NFS server for volumes\nthat every node must see.",[145,154,155],{},"Use a test cluster first.",[157,158,160],"h2",{"id":159},"requirements","Requirements",[162,163,164,178,181,192],"ul",{},[165,166,167,168,171,172,177],"li",{},"An active Swarm. Run mm on a manager, or from your machine through a\nmanager's ",[149,169,170],{},"mmd"," (see ",[173,174,176],"a",{"href":175},"..\u002Fremote-daemon","remote hosts",").",[165,179,180],{},"Published images, as for any deployment. Every node pulls them.",[165,182,183,184,187,188,191],{},"For shared volumes or HTTP routes: the ",[149,185,186],{},"nfsd"," kernel module on the storage\nnode, and ",[149,189,190],{},"\u002Fvar\u002Flib\u002Fmm\u002Fstorage"," on a normal filesystem such as ext4, xfs, or\nbtrfs. Other nodes need nothing installed.",[165,193,194],{},"For HTTP routes: ports 80 and 443 free on every ingress node.",[157,196,198],{"id":197},"set-up-the-nodes","Set up the nodes",[145,200,201,202,205,206,209,210,212,213,216],{},"Install Docker Engine on every node with ",[149,203,204],{},"mm target setup"," and the\n",[149,207,208],{},"docker-swarm"," runtime. On managers that developers reach through ",[149,211,170],{},", add\nthe remote access flags; see ",[173,214,176],{"href":215},"..\u002Fremote-daemon#set-up-the-host",".\nSetup logs in through a private target for each node's admin account:",[218,219,224],"pre",{"className":220,"code":221,"language":222,"meta":223,"style":223},"language-sh shiki shiki-themes mm","mm target add mgr-1-setup --host 10.0.0.11 --user admin \\\n  --host-key-fingerprint SHA256:REVIEWED_HOST_KEY_FINGERPRINT\nmm --environment production --target mgr-1-setup --runtime docker-swarm \\\n  target setup plan \\\n  --origin git@github.com:acme\u002Fshop.git \\\n  --origin-host-key 'ssh-ed25519 AAAA…' \\\n  --break-glass \"ops-recovery=$(cat ops-recovery.pub)\"\n","sh","",[149,225,226,262,271,295,309,320,331],{"__ignoreMap":223},[227,228,231,235,239,242,245,249,253,256,259],"span",{"class":229,"line":230},"line",1,[227,232,234],{"class":233},"svrjQ","mm",[227,236,238],{"class":237},"sv8ni"," target",[227,240,241],{"class":237}," add",[227,243,244],{"class":237}," mgr-1-setup",[227,246,248],{"class":247},"sbRf1"," --host",[227,250,252],{"class":251},"s1Wbj"," 10.0.0.11",[227,254,255],{"class":247}," --user",[227,257,258],{"class":237}," admin",[227,260,261],{"class":251}," \\\n",[227,263,265,268],{"class":229,"line":264},2,[227,266,267],{"class":247},"  --host-key-fingerprint",[227,269,270],{"class":237}," SHA256:REVIEWED_HOST_KEY_FINGERPRINT\n",[227,272,274,276,279,282,285,287,290,293],{"class":229,"line":273},3,[227,275,234],{"class":233},[227,277,278],{"class":247}," --environment",[227,280,281],{"class":237}," production",[227,283,284],{"class":247}," --target",[227,286,244],{"class":237},[227,288,289],{"class":247}," --runtime",[227,291,292],{"class":237}," docker-swarm",[227,294,261],{"class":251},[227,296,298,301,304,307],{"class":229,"line":297},4,[227,299,300],{"class":237},"  target",[227,302,303],{"class":237}," setup",[227,305,306],{"class":237}," plan",[227,308,261],{"class":251},[227,310,312,315,318],{"class":229,"line":311},5,[227,313,314],{"class":247},"  --origin",[227,316,317],{"class":237}," git@github.com:acme\u002Fshop.git",[227,319,261],{"class":251},[227,321,323,326,329],{"class":229,"line":322},6,[227,324,325],{"class":247},"  --origin-host-key",[227,327,328],{"class":237}," 'ssh-ed25519 AAAA…'",[227,330,261],{"class":251},[227,332,334,337,340,344,347,350,353],{"class":229,"line":333},7,[227,335,336],{"class":247},"  --break-glass",[227,338,339],{"class":237}," \"ops-recovery=",[227,341,343],{"class":342},"sIbZ0","$(",[227,345,346],{"class":233},"cat",[227,348,349],{"class":237}," ops-recovery.pub",[227,351,352],{"class":342},")",[227,354,355],{"class":237},"\"\n",[145,357,358],{},"Apply each plan with the command it prints, and repeat for every node. Setup\ninstalls the same packages as for Docker Compose and no host proxy, because\nSwarm runs Traefik itself.",[145,360,361],{},"Setup does not create the Swarm. Initialise it on the first manager and join\nthe others by hand, on private addresses:",[218,363,365],{"className":220,"code":364,"language":222,"meta":223,"style":223},"# On mgr-1:\ndocker swarm init --advertise-addr 10.0.0.11 --data-path-addr 10.0.0.11\ndocker swarm join-token manager\n# On mgr-2 and mgr-3, with the token printed above:\ndocker swarm join --token TOKEN \\\n  --advertise-addr 10.0.0.12 --data-path-addr 10.0.0.12 10.0.0.11:2377\n",[149,366,367,373,395,407,412,429],{"__ignoreMap":223},[227,368,369],{"class":229,"line":230},[227,370,372],{"class":371},"sP7nD","# On mgr-1:\n",[227,374,375,378,381,384,387,389,392],{"class":229,"line":264},[227,376,377],{"class":233},"docker",[227,379,380],{"class":237}," swarm",[227,382,383],{"class":237}," init",[227,385,386],{"class":247}," --advertise-addr",[227,388,252],{"class":251},[227,390,391],{"class":247}," --data-path-addr",[227,393,394],{"class":251}," 10.0.0.11\n",[227,396,397,399,401,404],{"class":229,"line":273},[227,398,377],{"class":233},[227,400,380],{"class":237},[227,402,403],{"class":237}," join-token",[227,405,406],{"class":237}," manager\n",[227,408,409],{"class":229,"line":297},[227,410,411],{"class":371},"# On mgr-2 and mgr-3, with the token printed above:\n",[227,413,414,416,418,421,424,427],{"class":229,"line":311},[227,415,377],{"class":233},[227,417,380],{"class":237},[227,419,420],{"class":237}," join",[227,422,423],{"class":247}," --token",[227,425,426],{"class":237}," TOKEN",[227,428,261],{"class":251},[227,430,431,434,437,439,441],{"class":229,"line":322},[227,432,433],{"class":247},"  --advertise-addr",[227,435,436],{"class":251}," 10.0.0.12",[227,438,391],{"class":247},[227,440,436],{"class":251},[227,442,443],{"class":237}," 10.0.0.11:2377\n",[145,445,446],{},"Check the cluster:",[218,448,450],{"className":220,"code":449,"language":222,"meta":223,"style":223},"mm target swarm nodes\n",[149,451,452],{"__ignoreMap":223},[227,453,454,456,458,460],{"class":229,"line":230},[227,455,234],{"class":233},[227,457,238],{"class":237},[227,459,380],{"class":237},[227,461,462],{"class":237}," nodes\n",[218,464,469],{"className":465,"code":467,"language":468,"meta":223},[466],"language-text","Swarm cluster m0lv578bbrlwkzwufcgiuwfnu (3 nodes, 3\u002F3 managers reachable, quorum 2)\n  mgr-1 (this node) leader active 10.0.0.11 ingress\n  mgr-2 manager active 10.0.0.12 ingress\n  mgr-3 manager active 10.0.0.13 ingress\n  storage: not chosen yet; the first deployment with shared volumes uses the node it runs on\n  ingress: every manager (default)\n","text",[149,470,467],{"__ignoreMap":223},[145,472,473],{},"The command warns when a manager is down, when losing one more manager would\nbreak quorum, or when the cluster has an even number of managers.",[157,475,477],{"id":476},"where-cluster-commands-run","Where cluster commands run",[145,479,480,483,484,483,487,490,491,494,495,498],{},[149,481,482],{},"mm target swarm nodes",", ",[149,485,486],{},"group",[149,488,489],{},"storage",", and ",[149,492,493],{},"ingress"," follow the\nselected target, like ",[149,496,497],{},"mm deploy",":",[162,500,501,518],{},[165,502,503,510,511,514,515,517],{},[504,505,506,507,509],"strong",{},"Through ",[149,508,170],{},"."," From a project checkout, when the environment's target\n(or ",[149,512,513],{},"--target",") is an SSH target, mm sends the command to that manager's\n",[149,516,170],{},". Nobody needs a shell on the manager.",[165,519,520,523,524,526],{},[504,521,522],{},"On the manager itself."," With a local target, or outside a project with\nno ",[149,525,513],{},", mm uses this machine's Docker engine.",[218,528,530],{"className":220,"code":529,"language":222,"meta":223,"style":223},"mm --environment production target swarm nodes\nmm --environment production target swarm group set edge mgr-2 mgr-3\n",[149,531,532,546],{"__ignoreMap":223},[227,533,534,536,538,540,542,544],{"class":229,"line":230},[227,535,234],{"class":233},[227,537,278],{"class":247},[227,539,281],{"class":237},[227,541,238],{"class":237},[227,543,380],{"class":237},[227,545,462],{"class":237},[227,547,548,550,552,554,556,558,561,564,567,570],{"class":229,"line":264},[227,549,234],{"class":233},[227,551,278],{"class":247},[227,553,281],{"class":237},[227,555,238],{"class":237},[227,557,380],{"class":237},[227,559,560],{"class":237}," group",[227,562,563],{"class":237}," set",[227,565,566],{"class":237}," edge",[227,568,569],{"class":237}," mgr-2",[227,571,572],{"class":237}," mgr-3\n",[145,574,506,575,577,578,581,582,585,586,590],{},[149,576,170],{},", reading nodes needs the ",[149,579,580],{},"view"," level in that environment, and\nchanging groups, storage, or ingress needs ",[149,583,584],{},"admin"," (see\n",[173,587,589],{"href":588},"..\u002Fremote-access#levels","remote access","). A change is a reviewed plan, like\na deploy:",[592,593,594,600,607],"ol",{},[165,595,596,597,509],{},"The manager plans the label changes against the live cluster, stores the\nplan, and records it in ",[149,598,599],{},"\u002Fvar\u002Flib\u002Fmm\u002Faudit.jsonl",[165,601,602,603,606],{},"mm shows the changes and asks before applying them (",[149,604,605],{},"--yes"," skips the\nquestion).",[165,608,609],{},"The manager applies that stored plan once, within ten minutes, and only if\nthe cluster still matches it. Otherwise it refuses, and you run the command\nagain to review the new plan.",[145,611,612],{},"The audit log records who planned and applied each change, the plan hash, the\nrequest, and the label changes.",[145,614,615,616,618,619,622,623,625],{},"Node labels belong to the whole cluster, not to one environment. Anyone with\n",[149,617,584],{}," in any environment a manager runs can change them, and the change\naffects every project on the cluster. Use the host's ",[149,620,621],{},"ceiling"," to keep ",[149,624,584],{},"\nfor the people who run the cluster.",[627,628,630],"h3",{"id":629},"run-a-command-in-a-service","Run a command in a service",[145,632,633,636,637,639,640,643],{},[149,634,635],{},"mm service exec SERVICE -- COMMAND"," runs in that service's container wherever\nSwarm placed it. When the manager mm reached does not run the container, it\nnames the node that does, and mm connects to that node's ",[149,638,170],{}," (a target member\nof that name, checked against its pinned keys) and runs the command there. This\nneeds the node to be listed in the target, as every node of a ",[149,641,642],{},"nodes:"," target\nis.",[157,645,647],{"id":646},"pin-a-service-to-a-node","Pin a service to a node",[145,649,650,651,654],{},"Add ",[149,652,653],{},"placement"," to the service:",[218,656,660],{"className":657,"code":658,"language":659,"meta":223,"style":223},"language-yaml shiki shiki-themes mm","modules:\n  db:\n    path: db\n    services:\n      postgres:\n        stack: app\n        placement: mgr-1\n","yaml",[149,661,662,671,678,688,695,702,712],{"__ignoreMap":223},[227,663,664,668],{"class":229,"line":230},[227,665,667],{"class":666},"sWZmh","modules",[227,669,670],{"class":342},":\n",[227,672,673,676],{"class":229,"line":264},[227,674,675],{"class":666},"  db",[227,677,670],{"class":342},[227,679,680,683,685],{"class":229,"line":273},[227,681,682],{"class":666},"    path",[227,684,498],{"class":342},[227,686,687],{"class":237}," db\n",[227,689,690,693],{"class":229,"line":297},[227,691,692],{"class":666},"    services",[227,694,670],{"class":342},[227,696,697,700],{"class":229,"line":311},[227,698,699],{"class":666},"      postgres",[227,701,670],{"class":342},[227,703,704,707,709],{"class":229,"line":322},[227,705,706],{"class":666},"        stack",[227,708,498],{"class":342},[227,710,711],{"class":237}," app\n",[227,713,714,717,719],{"class":229,"line":333},[227,715,716],{"class":666},"        placement",[227,718,498],{"class":342},[227,720,721],{"class":237}," mgr-1\n",[145,723,724,726],{},[149,725,653],{}," accepts:",[728,729,730,743],"table",{},[731,732,733],"thead",{},[734,735,736,740],"tr",{},[737,738,739],"th",{},"Value",[737,741,742],{},"Runs on",[744,745,746,755,763,773,783],"tbody",{},[734,747,748,752],{},[749,750,751],"td",{},"A hostname",[749,753,754],{},"That node only.",[734,756,757,760],{},[749,758,759],{},"A group name",[749,761,762],{},"Any node in the group.",[734,764,765,770],{},[749,766,767],{},[149,768,769],{},"managers",[749,771,772],{},"Any manager.",[734,774,775,780],{},[749,776,777],{},[149,778,779],{},"any",[749,781,782],{},"Any node.",[734,784,785,788],{},[749,786,787],{},"A list",[749,789,790],{},"Nodes that match every entry in the list.",[145,792,793,794,796],{},"Docker Compose, Podman Compose, and single-node Swarm ignore ",[149,795,653],{},", so\nthe same file still works on a laptop.",[157,798,800],{"id":799},"group-nodes","Group nodes",[145,802,803],{},"A group lets a service run on any of several nodes, so Swarm can move it when a\nnode fails:",[218,805,807],{"className":220,"code":806,"language":222,"meta":223,"style":223},"mm target swarm group set edge mgr-2 mgr-3\n",[149,808,809],{"__ignoreMap":223},[227,810,811,813,815,817,819,821,823,825],{"class":229,"line":230},[227,812,234],{"class":233},[227,814,238],{"class":237},[227,816,380],{"class":237},[227,818,560],{"class":237},[227,820,563],{"class":237},[227,822,566],{"class":237},[227,824,569],{"class":237},[227,826,572],{"class":237},[145,828,829,830,832,833,498],{},"mm shows the node label changes and asks before applying them. Use ",[149,831,605],{}," in\nscripts. Then name the group in ",[149,834,653],{},[218,836,838],{"className":657,"code":837,"language":659,"meta":223,"style":223},"web:\n  stack: app\n  placement: edge\n",[149,839,840,847,856],{"__ignoreMap":223},[227,841,842,845],{"class":229,"line":230},[227,843,844],{"class":666},"web",[227,846,670],{"class":342},[227,848,849,852,854],{"class":229,"line":264},[227,850,851],{"class":666},"  stack",[227,853,498],{"class":342},[227,855,711],{"class":237},[227,857,858,861,863],{"class":229,"line":273},[227,859,860],{"class":666},"  placement",[227,862,498],{"class":342},[227,864,865],{"class":237}," edge\n",[145,867,868,869,872,873,876,877,509],{},"Groups are stored as ",[149,870,871],{},"io.mm.group.\u003Cname>"," labels on the nodes, so everyone who\nmanages the cluster sees the same groups. mm only changes labels that start with\n",[149,874,875],{},"io.mm.",". Remove a group with ",[149,878,879],{},"mm target swarm group rm edge",[157,881,883],{"id":882},"set-every-label-at-once-on-a-manager","Set every label at once on a manager",[145,885,886,887,890],{},"A host administrator can set the groups, storage node, and ingress nodes in\none step with ",[149,888,889],{},"mmd swarm converge",". Run it as root on a manager that runs the\nproject's environment, and pass the whole label set as JSON on standard input.\nName nodes by hostname:",[218,892,894],{"className":220,"code":893,"language":222,"meta":223,"style":223},"echo '{\"storage\": \"mgr-1\", \"groups\": {\"db\": [\"mgr-1\"]}, \"ingress\": [\"mgr-2\", \"mgr-3\"]}' |\n  mmd swarm converge --project PROJECT_ID --environment production\n",[149,895,896,907],{"__ignoreMap":223},[227,897,898,901,904],{"class":229,"line":230},[227,899,900],{"class":233},"echo",[227,902,903],{"class":237}," '{\"storage\": \"mgr-1\", \"groups\": {\"db\": [\"mgr-1\"]}, \"ingress\": [\"mgr-2\", \"mgr-3\"]}'",[227,905,906],{"class":342}," |\n",[227,908,909,912,914,917,920,923,925],{"class":229,"line":264},[227,910,911],{"class":233},"  mmd",[227,913,380],{"class":237},[227,915,916],{"class":237}," converge",[227,918,919],{"class":247}," --project",[227,921,922],{"class":237}," PROJECT_ID",[227,924,278],{"class":247},[227,926,927],{"class":237}," production\n",[218,929,933],{"className":930,"code":931,"language":932,"meta":223,"style":223},"language-json shiki shiki-themes mm","{\n  \"cluster-id\": \"m0lv578bbrlwkzwufcgiuwfnu\",\n  \"changed\": true,\n  \"changes\": [\n    { \"node-id\": \"…\", \"hostname\": \"mgr-1\", \"add\": { \"io.mm.group.db\": \"true\" } },\n    { \"node-id\": \"…\", \"hostname\": \"mgr-2\", \"add\": { \"io.mm.ingress\": \"true\" } },\n    { \"node-id\": \"…\", \"hostname\": \"mgr-3\", \"add\": { \"io.mm.ingress\": \"true\" } }\n  ],\n  \"warnings\": []\n}\n","json",[149,934,935,940,959,975,989,1054,1108,1162,1168,1183],{"__ignoreMap":223},[227,936,937],{"class":229,"line":230},[227,938,939],{"class":342},"{\n",[227,941,942,945,948,951,953,956],{"class":229,"line":264},[227,943,944],{"class":342},"  \"",[227,946,947],{"class":666},"cluster-id",[227,949,950],{"class":342},"\"",[227,952,498],{"class":342},[227,954,955],{"class":237}," \"m0lv578bbrlwkzwufcgiuwfnu\"",[227,957,958],{"class":342},",\n",[227,960,961,963,966,968,970,973],{"class":229,"line":273},[227,962,944],{"class":342},[227,964,965],{"class":666},"changed",[227,967,950],{"class":342},[227,969,498],{"class":342},[227,971,972],{"class":251}," true",[227,974,958],{"class":342},[227,976,977,979,982,984,986],{"class":229,"line":297},[227,978,944],{"class":342},[227,980,981],{"class":666},"changes",[227,983,950],{"class":342},[227,985,498],{"class":342},[227,987,988],{"class":342}," [\n",[227,990,991,994,997,1000,1002,1004,1007,1010,1012,1015,1017,1019,1022,1024,1026,1029,1031,1033,1036,1038,1041,1043,1045,1048,1051],{"class":229,"line":311},[227,992,993],{"class":342},"    {",[227,995,996],{"class":342}," \"",[227,998,999],{"class":666},"node-id",[227,1001,950],{"class":342},[227,1003,498],{"class":342},[227,1005,1006],{"class":237}," \"…\"",[227,1008,1009],{"class":342},",",[227,1011,996],{"class":342},[227,1013,1014],{"class":666},"hostname",[227,1016,950],{"class":342},[227,1018,498],{"class":342},[227,1020,1021],{"class":237}," \"mgr-1\"",[227,1023,1009],{"class":342},[227,1025,996],{"class":342},[227,1027,1028],{"class":666},"add",[227,1030,950],{"class":342},[227,1032,498],{"class":342},[227,1034,1035],{"class":342}," {",[227,1037,996],{"class":342},[227,1039,1040],{"class":666},"io.mm.group.db",[227,1042,950],{"class":342},[227,1044,498],{"class":342},[227,1046,1047],{"class":237}," \"true\"",[227,1049,1050],{"class":342}," }",[227,1052,1053],{"class":342}," },\n",[227,1055,1056,1058,1060,1062,1064,1066,1068,1070,1072,1074,1076,1078,1081,1083,1085,1087,1089,1091,1093,1095,1098,1100,1102,1104,1106],{"class":229,"line":322},[227,1057,993],{"class":342},[227,1059,996],{"class":342},[227,1061,999],{"class":666},[227,1063,950],{"class":342},[227,1065,498],{"class":342},[227,1067,1006],{"class":237},[227,1069,1009],{"class":342},[227,1071,996],{"class":342},[227,1073,1014],{"class":666},[227,1075,950],{"class":342},[227,1077,498],{"class":342},[227,1079,1080],{"class":237}," \"mgr-2\"",[227,1082,1009],{"class":342},[227,1084,996],{"class":342},[227,1086,1028],{"class":666},[227,1088,950],{"class":342},[227,1090,498],{"class":342},[227,1092,1035],{"class":342},[227,1094,996],{"class":342},[227,1096,1097],{"class":666},"io.mm.ingress",[227,1099,950],{"class":342},[227,1101,498],{"class":342},[227,1103,1047],{"class":237},[227,1105,1050],{"class":342},[227,1107,1053],{"class":342},[227,1109,1110,1112,1114,1116,1118,1120,1122,1124,1126,1128,1130,1132,1135,1137,1139,1141,1143,1145,1147,1149,1151,1153,1155,1157,1159],{"class":229,"line":333},[227,1111,993],{"class":342},[227,1113,996],{"class":342},[227,1115,999],{"class":666},[227,1117,950],{"class":342},[227,1119,498],{"class":342},[227,1121,1006],{"class":237},[227,1123,1009],{"class":342},[227,1125,996],{"class":342},[227,1127,1014],{"class":666},[227,1129,950],{"class":342},[227,1131,498],{"class":342},[227,1133,1134],{"class":237}," \"mgr-3\"",[227,1136,1009],{"class":342},[227,1138,996],{"class":342},[227,1140,1028],{"class":666},[227,1142,950],{"class":342},[227,1144,498],{"class":342},[227,1146,1035],{"class":342},[227,1148,996],{"class":342},[227,1150,1097],{"class":666},[227,1152,950],{"class":342},[227,1154,498],{"class":342},[227,1156,1047],{"class":237},[227,1158,1050],{"class":342},[227,1160,1161],{"class":342}," }\n",[227,1163,1165],{"class":229,"line":1164},8,[227,1166,1167],{"class":342},"  ],\n",[227,1169,1171,1173,1176,1178,1180],{"class":229,"line":1170},9,[227,1172,944],{"class":342},[227,1174,1175],{"class":666},"warnings",[227,1177,950],{"class":342},[227,1179,498],{"class":342},[227,1181,1182],{"class":342}," []\n",[227,1184,1186],{"class":229,"line":1185},10,[227,1187,1188],{"class":342},"}\n",[145,1190,1191,1192,1194],{},"The declaration is the complete set of ",[149,1193,875],{}," labels:",[162,1196,1197,1205,1212],{},[165,1198,1199,1204],{},[504,1200,1201],{},[149,1202,1203],{},"groups",": each group gets exactly the listed nodes. Groups that are not\nlisted lose their labels, and a warning names them.",[165,1206,1207,1211],{},[504,1208,1209],{},[149,1210,493],{},": the nodes that run ingress. Leave it out, or pass an empty\nlist, for every manager.",[165,1213,1214,1218],{},[504,1215,1216],{},[149,1217,489],{},": the storage node. Leave it out to keep the current one;\nmoving storage does not copy data, so the command warns when it moves.",[145,1220,1221,1222,1225,1226,1228,1229,1231,1232,509],{},"Running the command again with the same declaration prints ",[149,1223,1224],{},"\"changed\": false","\nand an empty ",[149,1227,981],{}," list. Changes are recorded in ",[149,1230,599],{},"\nand appear in ",[149,1233,1234],{},"mmd access audit",[157,1236,1238],{"id":1237},"share-a-volume-between-nodes","Share a volume between nodes",[145,1240,1241],{},"A normal named volume lives on one node. On a cluster, mm requires a service\nthat uses one to be pinned to exactly one node. If the service must run on\nseveral nodes, list the volume under its stack:",[218,1243,1245],{"className":657,"code":1244,"language":659,"meta":223,"style":223},"stacks:\n  app:\n    files: [compose.yaml]\n    shared-volumes: [uploads]\n",[149,1246,1247,1254,1261,1277],{"__ignoreMap":223},[227,1248,1249,1252],{"class":229,"line":230},[227,1250,1251],{"class":666},"stacks",[227,1253,670],{"class":342},[227,1255,1256,1259],{"class":229,"line":264},[227,1257,1258],{"class":666},"  app",[227,1260,670],{"class":342},[227,1262,1263,1266,1268,1271,1274],{"class":229,"line":273},[227,1264,1265],{"class":666},"    files",[227,1267,498],{"class":342},[227,1269,1270],{"class":342}," [",[227,1272,1273],{"class":237},"compose.yaml",[227,1275,1276],{"class":342},"]\n",[227,1278,1279,1282,1284,1286,1289],{"class":229,"line":297},[227,1280,1281],{"class":666},"    shared-volumes",[227,1283,498],{"class":342},[227,1285,1270],{"class":342},[227,1287,1288],{"class":237},"uploads",[227,1290,1276],{"class":342},[145,1292,1293,1294,1297,1298,483,1301,1304,1305,1308],{},"Keep the volume declared in Compose as usual (",[149,1295,1296],{},"volumes: { uploads: {} }",")\nwithout ",[149,1299,1300],{},"driver",[149,1302,1303],{},"driver_opts",", or ",[149,1306,1307],{},"external",". On the first deployment that\nneeds it, mm:",[592,1310,1311,1314,1317,1322],{},[165,1312,1313],{},"Chooses the node it runs on as the storage node, and says so in the plan.",[165,1315,1316],{},"Starts an NFS server there, limited to the cluster's node addresses.",[165,1318,1319,1320,509],{},"Creates a directory for each shared volume under ",[149,1321,190],{},[165,1323,1324],{},"Mounts that directory on every node that runs the service.",[145,1326,1327],{},"To use a different storage node, run this before the first deployment:",[218,1329,1331],{"className":220,"code":1330,"language":222,"meta":223,"style":223},"mm target swarm storage set mgr-2\n",[149,1332,1333],{"__ignoreMap":223},[227,1334,1335,1337,1339,1341,1344,1346],{"class":229,"line":230},[227,1336,234],{"class":233},[227,1338,238],{"class":237},[227,1340,380],{"class":237},[227,1342,1343],{"class":237}," storage",[227,1345,563],{"class":237},[227,1347,1348],{"class":237}," mgr-2\n",[145,1350,1351,1352,509],{},"Moving storage later does not copy data. Services start with empty shared\nvolumes until you copy the files from the old node's ",[149,1353,190],{},[145,1355,1356,1357,1359,1360,1363,1364,1367],{},"The NFS server mm starts is a container that needs to mount kernel\nfilesystems. Docker's default AppArmor profile forbids that, and Swarm offers\nno way to change a service's profile, so on Ubuntu (AppArmor on by default)\nthe container cannot start. A cluster target set up with ",[149,1358,204],{},"\navoids this: setup installs the storage node's own ",[149,1361,1362],{},"nfs-kernel-server","\n(NFSv4.2 only, exported to the nodes' private addresses) and labels the node\n",[149,1365,1366],{},"io.mm.storage-server=host",", and deploys then use it instead of the container.\nThe volumes and their mount options are the same either way.",[157,1369,1371],{"id":1370},"serve-https","Serve HTTPS",[145,1373,1374,1375,1377,1378,1382],{},"Routes work as on a single host: add ",[149,1376,493],{}," to a stack and a domain to the\nenvironment (see ",[173,1379,1381],{"href":1380},"..\u002Freference\u002Fconfiguration#stacks-and-ingress","configuration",").\nOn a Swarm, mm runs Traefik on every ingress node (every manager unless you\nchoose otherwise), listening directly on ports 80 and 443. Point DNS or your\nload balancer at those nodes; any of them can answer any request. No host\nproxy is installed.",[145,1384,1385],{},"Certificates come from Let's Encrypt automatically. mm runs one ACME instance\nthat requests certificates; the other nodes forward Let's Encrypt's HTTP\nchallenges to it and load the certificates it obtains. Route and certificate\nchanges reach every node within a few seconds, without restarting Traefik.\nThey live on the built-in shared storage, so the first route also starts it.",[627,1387,1389],{"id":1388},"serve-your-own-certificate","Serve your own certificate",[145,1391,1392],{},"Behind a proxy that blocks Let's Encrypt's challenges, such as Cloudflare\nAccess, serve a certificate you provide instead, for example a Cloudflare\norigin certificate. Store the PEM files as secrets, base64 on one line, and\nname them in the environment:",[218,1394,1396],{"className":657,"code":1395,"language":659,"meta":223,"style":223},"secrets:\n  tls.origin-certificate:\n    storage: {provider: age, location: local\u002Ftls-origin-certificate}\n  tls.origin-key:\n    storage: {provider: age, location: local\u002Ftls-origin-key}\n\nenvironments:\n  integration:\n    tls: {certificate: tls.origin-certificate, key: tls.origin-key}\n",[149,1397,1398,1405,1412,1441,1448,1473,1479,1486,1493],{"__ignoreMap":223},[227,1399,1400,1403],{"class":229,"line":230},[227,1401,1402],{"class":666},"secrets",[227,1404,670],{"class":342},[227,1406,1407,1410],{"class":229,"line":264},[227,1408,1409],{"class":666},"  tls.origin-certificate",[227,1411,670],{"class":342},[227,1413,1414,1417,1419,1421,1424,1426,1429,1431,1434,1436,1439],{"class":229,"line":273},[227,1415,1416],{"class":666},"    storage",[227,1418,498],{"class":342},[227,1420,1035],{"class":342},[227,1422,1423],{"class":666},"provider",[227,1425,498],{"class":342},[227,1427,1428],{"class":237}," age",[227,1430,1009],{"class":342},[227,1432,1433],{"class":666}," location",[227,1435,498],{"class":342},[227,1437,1438],{"class":237}," local\u002Ftls-origin-certificate",[227,1440,1188],{"class":342},[227,1442,1443,1446],{"class":229,"line":297},[227,1444,1445],{"class":666},"  tls.origin-key",[227,1447,670],{"class":342},[227,1449,1450,1452,1454,1456,1458,1460,1462,1464,1466,1468,1471],{"class":229,"line":311},[227,1451,1416],{"class":666},[227,1453,498],{"class":342},[227,1455,1035],{"class":342},[227,1457,1423],{"class":666},[227,1459,498],{"class":342},[227,1461,1428],{"class":237},[227,1463,1009],{"class":342},[227,1465,1433],{"class":666},[227,1467,498],{"class":342},[227,1469,1470],{"class":237}," local\u002Ftls-origin-key",[227,1472,1188],{"class":342},[227,1474,1475],{"class":229,"line":322},[227,1476,1478],{"emptyLinePlaceholder":1477},true,"\n",[227,1480,1481,1484],{"class":229,"line":333},[227,1482,1483],{"class":666},"environments",[227,1485,670],{"class":342},[227,1487,1488,1491],{"class":229,"line":1164},[227,1489,1490],{"class":666},"  integration",[227,1492,670],{"class":342},[227,1494,1495,1498,1500,1502,1505,1507,1510,1512,1515,1517,1520],{"class":229,"line":1170},[227,1496,1497],{"class":666},"    tls",[227,1499,498],{"class":342},[227,1501,1035],{"class":342},[227,1503,1504],{"class":666},"certificate",[227,1506,498],{"class":342},[227,1508,1509],{"class":237}," tls.origin-certificate",[227,1511,1009],{"class":342},[227,1513,1514],{"class":666}," key",[227,1516,498],{"class":342},[227,1518,1519],{"class":237}," tls.origin-key",[227,1521,1188],{"class":342},[218,1523,1525],{"className":220,"code":1524,"language":222,"meta":223,"style":223},"base64 -w0 cert.pem | mm -e integration secret set tls.origin-certificate --stdin\nbase64 -w0 key.pem | mm -e integration secret set tls.origin-key --stdin\n",[149,1526,1527,1560],{"__ignoreMap":223},[227,1528,1529,1532,1535,1538,1541,1544,1547,1550,1553,1555,1557],{"class":229,"line":230},[227,1530,1531],{"class":233},"base64",[227,1533,1534],{"class":247}," -w0",[227,1536,1537],{"class":237}," cert.pem",[227,1539,1540],{"class":342}," |",[227,1542,1543],{"class":233}," mm",[227,1545,1546],{"class":247}," -e",[227,1548,1549],{"class":237}," integration",[227,1551,1552],{"class":237}," secret",[227,1554,563],{"class":237},[227,1556,1509],{"class":237},[227,1558,1559],{"class":247}," --stdin\n",[227,1561,1562,1564,1566,1569,1571,1573,1575,1577,1579,1581,1583],{"class":229,"line":264},[227,1563,1531],{"class":233},[227,1565,1534],{"class":247},[227,1567,1568],{"class":237}," key.pem",[227,1570,1540],{"class":342},[227,1572,1543],{"class":233},[227,1574,1546],{"class":247},[227,1576,1549],{"class":237},[227,1578,1552],{"class":237},[227,1580,563],{"class":237},[227,1582,1519],{"class":237},[227,1584,1559],{"class":247},[145,1586,1587],{},"Each deploy writes both files to the shared ingress storage, where every edge\nloads them, and that environment's routes stop asking ACME for certificates.\nThe certificate has to cover every domain the environment routes. Provided\ncertificates need Swarm ingress.",[145,1589,1590],{},"Run ingress on fewer nodes, for example those your load balancer targets:",[218,1592,1594],{"className":220,"code":1593,"language":222,"meta":223,"style":223},"mm target swarm ingress set mgr-2 mgr-3\nmm target swarm ingress set   # every manager again\n",[149,1595,1596,1613],{"__ignoreMap":223},[227,1597,1598,1600,1602,1604,1607,1609,1611],{"class":229,"line":230},[227,1599,234],{"class":233},[227,1601,238],{"class":237},[227,1603,380],{"class":237},[227,1605,1606],{"class":237}," ingress",[227,1608,563],{"class":237},[227,1610,569],{"class":237},[227,1612,572],{"class":237},[227,1614,1615,1617,1619,1621,1623,1625],{"class":229,"line":264},[227,1616,234],{"class":233},[227,1618,238],{"class":237},[227,1620,380],{"class":237},[227,1622,1606],{"class":237},[227,1624,563],{"class":237},[227,1626,1627],{"class":371},"   # every manager again\n",[145,1629,1630],{},"To test with Let's Encrypt staging or use an internal ACME server, set its\ndirectory, and the CA that signs its HTTPS certificate if public roots do not\ntrust it:",[218,1632,1634],{"className":220,"code":1633,"language":222,"meta":223,"style":223},"mm target show local\nmm target ingress configure local --image TRAEFIK_IMAGE \\\n  --acme-email ops@example.com \\\n  --acme-ca-server https:\u002F\u002Facme-staging-v02.api.letsencrypt.org\u002Fdirectory \\\n  --expected-fingerprint TARGET_FINGERPRINT\n",[149,1635,1636,1648,1670,1680,1690],{"__ignoreMap":223},[227,1637,1638,1640,1642,1645],{"class":229,"line":230},[227,1639,234],{"class":233},[227,1641,238],{"class":237},[227,1643,1644],{"class":237}," show",[227,1646,1647],{"class":237}," local\n",[227,1649,1650,1652,1654,1656,1659,1662,1665,1668],{"class":229,"line":264},[227,1651,234],{"class":233},[227,1653,238],{"class":237},[227,1655,1606],{"class":237},[227,1657,1658],{"class":237}," configure",[227,1660,1661],{"class":237}," local",[227,1663,1664],{"class":247}," --image",[227,1666,1667],{"class":237}," TRAEFIK_IMAGE",[227,1669,261],{"class":251},[227,1671,1672,1675,1678],{"class":229,"line":273},[227,1673,1674],{"class":247},"  --acme-email",[227,1676,1677],{"class":237}," ops@example.com",[227,1679,261],{"class":251},[227,1681,1682,1685,1688],{"class":229,"line":297},[227,1683,1684],{"class":247},"  --acme-ca-server",[227,1686,1687],{"class":237}," https:\u002F\u002Facme-staging-v02.api.letsencrypt.org\u002Fdirectory",[227,1689,261],{"class":251},[227,1691,1692,1695],{"class":229,"line":311},[227,1693,1694],{"class":247},"  --expected-fingerprint",[227,1696,1697],{"class":237}," TARGET_FINGERPRINT\n",[145,1699,650,1700,1703,1704,509],{},[149,1701,1702],{},"--acme-ca-certificate ca.pem"," for an internal server. Keep the image\nshown by ",[149,1705,1706],{},"mm target show",[157,1708,1710],{"id":1709},"deploy","Deploy",[145,1712,1713],{},"Deploy as usual:",[218,1715,1717],{"className":220,"code":1716,"language":222,"meta":223,"style":223},"mm deploy plan\nmm deploy apply --plan PLAN_HASH --yes\n",[149,1718,1719,1729],{"__ignoreMap":223},[227,1720,1721,1723,1726],{"class":229,"line":230},[227,1722,234],{"class":233},[227,1724,1725],{"class":237}," deploy",[227,1727,1728],{"class":237}," plan\n",[227,1730,1731,1733,1735,1738,1741,1744],{"class":229,"line":264},[227,1732,234],{"class":233},[227,1734,1725],{"class":237},[227,1736,1737],{"class":237}," apply",[227,1739,1740],{"class":247}," --plan",[227,1742,1743],{"class":237}," PLAN_HASH",[227,1745,1746],{"class":247}," --yes\n",[145,1748,1749],{},"The plan lists where each service runs and the shared volumes:",[218,1751,1754],{"className":1752,"code":1753,"language":468,"meta":223},[466],"  cluster: 3 nodes\n    app\u002Fdb -> mgr-1 (one node: no failover)\n    app\u002Fweb -> mgr-2, mgr-3\n  shared storage: mgr-1 (10.0.0.11:\u002Fvar\u002Flib\u002Fmm\u002Fstorage) - new\n    app\u002Fuploads\n  ingress edges: mgr-1, mgr-2, mgr-3 (ports 80 and 443)\n",[149,1755,1753],{"__ignoreMap":223},[145,1757,1758,1759,1762],{},"The plan includes the nodes and mm labels it was made against. If a node joins\nor leaves, or a group, storage, or ingress label changes, ",[149,1760,1761],{},"deploy apply","\nrejects the old plan. Create a new plan.",[157,1764,1766],{"id":1765},"fail-over-between-managers","Fail over between managers",[145,1768,1769,1770,1772],{},"When you reach the cluster through ",[149,1771,170],{},", enroll the other managers as members\nof the same target. mm tries the target first, then each member in order.",[145,1774,1775,1776,1779,1780,1783,1784,509],{},"For a target the repo declares under ",[149,1777,1778],{},"targets:",", list the managers under\n",[149,1781,1782],{},"members"," so every clone and CI runner gets them, and the keys they pin are\nreviewed with the rest of the repo. See\n",[173,1785,1787],{"href":1786},"..\u002Fremote-daemon#swarm-managers-as-failover-members","Swarm managers as failover members",[145,1789,1790,1791,498],{},"For a target in your private catalog, add each member with\n",[149,1792,1793],{},"mm target swarm member add",[218,1795,1797],{"className":220,"code":1796,"language":222,"meta":223,"style":223},"mm target show production\nmm target swarm member add production mgr-2 \\\n  --host 10.0.0.12 --user mm \\\n  --host-key-fingerprint SHA256:REVIEWED --host-key 'ssh-ed25519 REVIEWED_BASE64' \\\n  --daemon-fingerprint REVIEWED_BLAKE3 \\\n  --expected-fingerprint TARGET_FINGERPRINT\n",[149,1798,1799,1809,1828,1841,1856,1866],{"__ignoreMap":223},[227,1800,1801,1803,1805,1807],{"class":229,"line":230},[227,1802,234],{"class":233},[227,1804,238],{"class":237},[227,1806,1644],{"class":237},[227,1808,927],{"class":237},[227,1810,1811,1813,1815,1817,1820,1822,1824,1826],{"class":229,"line":264},[227,1812,234],{"class":233},[227,1814,238],{"class":237},[227,1816,380],{"class":237},[227,1818,1819],{"class":237}," member",[227,1821,241],{"class":237},[227,1823,281],{"class":237},[227,1825,569],{"class":237},[227,1827,261],{"class":251},[227,1829,1830,1833,1835,1837,1839],{"class":229,"line":273},[227,1831,1832],{"class":247},"  --host",[227,1834,436],{"class":251},[227,1836,255],{"class":247},[227,1838,1543],{"class":237},[227,1840,261],{"class":251},[227,1842,1843,1845,1848,1851,1854],{"class":229,"line":297},[227,1844,267],{"class":247},[227,1846,1847],{"class":237}," SHA256:REVIEWED",[227,1849,1850],{"class":247}," --host-key",[227,1852,1853],{"class":237}," 'ssh-ed25519 REVIEWED_BASE64'",[227,1855,261],{"class":251},[227,1857,1858,1861,1864],{"class":229,"line":311},[227,1859,1860],{"class":247},"  --daemon-fingerprint",[227,1862,1863],{"class":237}," REVIEWED_BLAKE3",[227,1865,261],{"class":251},[227,1867,1868,1870],{"class":229,"line":322},[227,1869,1694],{"class":247},[227,1871,1697],{"class":237},[145,1873,1874,1875,1877,1878,1881,1882,1885,1886,1888,1889,1892,1893,1896,1897,1899],{},"Set up each member with ",[149,1876,204],{}," and the same ",[149,1879,1880],{},"--origin"," and\n",[149,1883,1884],{},"--break-glass"," flags, so every manager runs ",[149,1887,170],{}," with the same host policy.\nTake each member's ",[149,1890,1891],{},"--host-key"," and ",[149,1894,1895],{},"--daemon-fingerprint"," from its setup\noutput. ",[149,1898,170],{}," fetches the project from its origin itself. mm moves to the next\nmember only when it cannot connect.\nIt never resends a request that reached a daemon.",[157,1901,1903],{"id":1902},"access-on-every-manager","Access on every manager",[145,1905,1906],{},"Every manager enforces the same users, whichever one a command reaches.",[145,1908,1909,1910,1913,1914,1917],{},"When ",[149,1911,1912],{},"mm team sync"," or ",[149,1915,1916],{},"mm deploy apply"," changes an environment's access, the\nmanager that runs it does three things before deploying anything:",[592,1919,1920,1926,1935],{},[165,1921,1922,1923,509],{},"It stores the reconciled users file in the Swarm as a config named\n",[149,1924,1925],{},"mm-access-record-…",[165,1927,1928,1929,1931,1932,177],{},"Each other manager's ",[149,1930,170],{}," that runs the environment picks up the newest\nrecord within a few seconds and updates its SSH keys. It then records an\nacknowledgment (",[149,1933,1934],{},"mm-access-ack-…",[165,1936,1937],{},"The command waits up to 20 seconds for those acknowledgments, and reports\nwhich managers confirmed.",[218,1939,1942],{"className":1940,"code":1941,"language":468,"meta":223},[466],"Applied the users file of 3f2c9a1e5b7d4c6f8a0b2d4e6f8a1c3e5b7d9f0a to production\n  access changes in production:\n    carol: none -> deploy, +1 key(s)\n  managers: mgr-1, mgr-2, mgr-3 confirmed (shared record 4)\n",[149,1943,1941],{"__ignoreMap":223},[145,1945,1946,1947,1949,1950,1952],{},"A manager that does not confirm in time gets a warning line, and the change\nstill holds. That manager applies it on its next check or when its ",[149,1948,170],{},"\nrestarts. ",[149,1951,170],{}," checks the Swarm before every plan and apply, and at least\nevery 30 seconds for other requests. When it cannot reach Docker to check, it\nrefuses requests rather than act on an outdated copy.",[145,1954,1955],{},"If another manager changed the environment's access after you planned, apply\nrefuses with \"produce a new plan\". Plan again and review the new change.",[145,1957,1958],{},"A newly set-up manager picks up the current users of every environment it\nruns, so you need no extra sync.",[145,1960,1961,1962,1965,1966,1970,1971,1973,1974,1977,1978,1981,1982,1985],{},"For the first sync of a new cluster, run ",[149,1963,1964],{},"mmd access sync --require-shared","\nas root on one manager once the Swarm is formed (see ",[173,1967,1969],{"href":1968},"..\u002Fremote-daemon#apply-the-users-file-for-the-first-time","apply the users file for\nthe first time",").\nIt shares the record like a team sync. Every other manager needs only a\nrunning ",[149,1972,170],{}," with the project and environment enrolled in its\n",[149,1975,1976],{},"\u002Fetc\u002Fmm\u002Fhost.yaml",": it adopts the record within a few seconds. Check with\n",[149,1979,1980],{},"mmd access show"," on each manager; ",[149,1983,1984],{},"shared-record"," shows the same sequence.",[627,1987,1989],{"id":1988},"after-upgrading","After upgrading",[145,1991,1992,1993,1996],{},"Access is shared once a manager runs this release. Upgrade every manager. Then\nrun ",[149,1994,1995],{},"mm --environment ENVIRONMENT team sync"," once for each environment, so one\nshared record replaces the copies each manager kept on its own.",[627,1998,2000],{"id":1999},"what-is-shared","What is shared",[162,2002,2003,2010,2013,2016,2022],{},[165,2004,2005,2006,2009],{},"The records hold user names, public keys, levels, ",[149,2007,2008],{},"deploy-from",", the commit,\nand who applied it. They never hold secrets.",[165,2011,2012],{},"mm keeps the newest three records per environment.",[165,2014,2015],{},"When a manager leaves the Swarm, the next change drops its acknowledgments.",[165,2017,2018,2019,2021],{},"Each manager still applies its own ",[149,2020,1976],{},": break-glass keys,\nceilings, and which environments it runs.",[165,2023,2024],{},"Managers trust each other's records. Anyone who can use Docker on one manager\nalready controls every node.",[157,2026,2028],{"id":2027},"limitations","Limitations",[162,2030,2031,2041,2069,2075,2085,2091,2104,2110,2119],{},[165,2032,2033,2036,2037,2040],{},[504,2034,2035],{},"Single storage node."," Shared volumes are unavailable while the storage\nnode is down. NFS mounts are ",[149,2038,2039],{},"hard",", so writers wait instead of losing data.\nRemove application stacks before the storage node when you tear down a cluster.",[165,2042,2043,2046,2047,1892,2050,2053,2054,2056,2057,2060,2061,2064,2065,2068],{},[504,2044,2045],{},"Unencrypted NFS."," Shared storage needs every node on a private network:\ninitialise and join the Swarm with ",[149,2048,2049],{},"--advertise-addr",[149,2051,2052],{},"--data-path-addr","\nset to private addresses (or WireGuard). With more than one node, mm refuses\nto plan shared volumes or routes while any node address is public, and\n",[149,2055,482],{}," warns about it. The storage node publishes port 2049\nthrough Docker, which bypasses UFW and firewalld; to keep it off the public\ninterface, add a ",[149,2058,2059],{},"DOCKER-USER"," rule such as\n",[149,2062,2063],{},"iptables -I DOCKER-USER ! -i eth1 -p tcp -m conntrack --ctorigdstport 2049 --ctdir ORIGINAL -j DROP",",\nwith ",[149,2066,2067],{},"eth1"," the private interface.",[165,2070,2071,2074],{},[504,2072,2073],{},"No failover for single-node placement."," A service pinned to one node stops\nwhen that node stops.",[165,2076,2077,2080,2081,2084],{},[504,2078,2079],{},"Emergency blocks are per manager."," ",[149,2082,2083],{},"mmd access block"," takes effect only\non the manager where you run it. Block the key on every manager.",[165,2086,2087,2090],{},[504,2088,2089],{},"Shared storage carries ingress."," While the storage node is down, edges\nkeep serving the routes and certificates they already loaded, but new route\nchanges and certificate renewals wait for it.",[165,2092,2093,2096,2097,2100,2101,509],{},[504,2094,2095],{},"Local HTTPS."," Routes that use mm's local certificate authority (",[149,2098,2099],{},".test","\nand similar development domains) are not available on Swarm. Use a public\ndomain, or ",[149,2102,2103],{},"https: false",[165,2105,2106,2109],{},[504,2107,2108],{},"Rolling ingress updates."," Changing the Traefik image or ingress nodes\nrestarts edges one node at a time; each node is briefly unavailable, so keep\nyour load balancer's health checks on.",[165,2111,2112,2115,2116,2118],{},[504,2113,2114],{},"Per-member history."," Each ",[149,2117,170],{}," keeps its own deployment history. After\nfailing over to another member, plan again; rollback uses that member's history.",[165,2120,2121,2127],{},[504,2122,2123,2124,2126],{},"Cluster commands through ",[149,2125,170],{}," need a project."," Access is granted per\nproject and environment, so run them from the project's checkout. Outside\none, they use this machine's Docker engine.",[2129,2130,2131],"style",{},"html pre.shiki code .svrjQ, html code.shiki .svrjQ{--shiki-default:var(--mm-code-function)}html pre.shiki code .sv8ni, html code.shiki .sv8ni{--shiki-default:var(--mm-code-string)}html pre.shiki code .sbRf1, html code.shiki .sbRf1{--shiki-default:var(--mm-code-option)}html pre.shiki code .s1Wbj, html code.shiki .s1Wbj{--shiki-default:var(--mm-code-constant)}html pre.shiki code .sIbZ0, html code.shiki .sIbZ0{--shiki-default:var(--mm-code-punctuation)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sP7nD, html code.shiki .sP7nD{--shiki-default:var(--mm-code-comment)}html pre.shiki code .sWZmh, html code.shiki .sWZmh{--shiki-default:var(--mm-code-key)}",{"title":223,"searchDepth":264,"depth":264,"links":2133},[2134,2135,2136,2139,2140,2141,2142,2143,2146,2147,2148,2152],{"id":159,"depth":264,"text":160},{"id":197,"depth":264,"text":198},{"id":476,"depth":264,"text":477,"children":2137},[2138],{"id":629,"depth":273,"text":630},{"id":646,"depth":264,"text":647},{"id":799,"depth":264,"text":800},{"id":882,"depth":264,"text":883},{"id":1237,"depth":264,"text":1238},{"id":1370,"depth":264,"text":1371,"children":2144},[2145],{"id":1388,"depth":273,"text":1389},{"id":1709,"depth":264,"text":1710},{"id":1765,"depth":264,"text":1766},{"id":1902,"depth":264,"text":1903,"children":2149},[2150,2151],{"id":1988,"depth":273,"text":1989},{"id":1999,"depth":273,"text":2000},{"id":2027,"depth":264,"text":2028},"Pin services to nodes and share volumes across a Docker Swarm cluster.","md",{},{"title":48,"description":2153},"docs\u002Fhow-to\u002Fswarm-cluster","NJix_TPigaG3Em4sMAYcXBwOCEa_XaCqz78XlspW08k",1794512546197]