UNPKG

dclint

Version:

A command-line tool for validating and enforcing best practices in Docker Compose files.

1 lines 78.6 kB
"use strict";var e=require("node:fs"),t=require("yaml"),i=require("node:path"),r=require("picocolors"),s=require("node:net"),o=require("ajv"),n=require("node:crypto");const a="LINTER";class c{static instance;debugMode=!1;constructor(e){void 0!==e&&(this.debugMode=e)}static init(e){return c.instance||(c.instance=new c(e)),c.instance}static formatMessage(e,t){const i=c.getColoredLevel(e);return t?`${i} [${t}]`:i}static getColoredLevel(e){switch(e){case"DEBUG":return r.blue("[DEBUG]");case"INFO":return r.green("[INFO]");case"WARN":return r.yellow("[WARN]");case"ERROR":return r.red("[ERROR]");default:return`[${e}]`}}debug(e,...t){if(this.debugMode){const i=c.formatMessage("DEBUG",e);console.debug(i,...t)}}info(...e){const t=c.formatMessage("INFO");console.info(t,...e)}warn(...e){const t=c.formatMessage("WARN");console.warn(t,...e)}error(...e){const t=c.formatMessage("ERROR");console.error(t,...e)}}class p extends Error{constructor(e){super(),this.message=`File or directory not found: ${e}`,this.name="FileNotFoundError"}}function l(t,r,s){const o=c.init();o.debug("UTIL",`Looking for compose files in ${t.toString()}`);let n=[];const a=new Set(["node_modules",".git",".idea",".tsimp"]);s&&s.length>0&&s.forEach((e=>a.add(e)));const d=[...a];o.debug("UTIL",`Paths to exclude: ${d.toString()}`);const u=/^(docker-)?compose.*\.ya?ml$/;return t.forEach((t=>{if(!e.existsSync(t))throw o.debug("UTIL",`File or directory not found: ${t}`),new p(t);let s=[];const a=e.statSync(t);if(a.isDirectory()){try{s=e.readdirSync(i.resolve(t)).map((e=>i.join(t,e)))}catch(e){o.debug("UTIL",`Error reading directory: ${t}`,e),s=[]}s.forEach((t=>{if(d.some((e=>t.includes(e))))return void o.debug("UTIL",`Excluding ${t}`);const s=e.statSync(i.resolve(t));if(s.isDirectory()){if(r){o.debug("UTIL",`Recursive search is enabled, search within the directory: ${t}`);const e=l([t],r,d);n=[...n,...e]}}else s.isFile()&&u.test(i.basename(t))&&n.push(t)}))}else a.isFile()&&n.push(t)})),o.debug("UTIL",`Found compose files in ${t.toString()}: ${n.length>0?n.join(", "):"None"}`),n}const d={blockQuote:!0,collectionStyle:"any",defaultKeyType:null,defaultStringType:"PLAIN",directives:null,doubleQuotedAsJSON:!1,doubleQuotedMinMultiLineLength:40,falseStr:"false",flowCollectionPadding:!0,indent:2,indentSeq:!0,lineWidth:0,minContentWidth:20,nullStr:"null",simpleKeys:!1,singleQuote:null,trueStr:"true",verifyAliasOrder:!0};function u(e){return t.parseDocument(e)}function m(e){return e.toString(d)}function f(e,t){const i=e.split("\n"),r=new RegExp(`^\\s*${t}:`,"i");let s=1;for(const e of i){if(r.test(e))return s;s+=1}return 1}function g(e,t){const i=e.split("\n").findIndex((e=>e.includes(t)));return-1===i?1:i+1}function h(e,i,r,s,o){const n=e.get("services");if(!t.isMap(n))return 1;const a=n.get(r);if(!t.isMap(a))return 1;if(!s)return function(e,t){if(e.range){const[i]=e.range;return t.slice(0,i).split("\n").length-1}return 1}(a,i);const c=a.get(s,!0);if(!c)return 1;if(!o)return function(e,i){if(e.range){const[r]=e.range,s=i.slice(0,r).split("\n").length;return t.isScalar(e)?s:s-1}return 1}(c,i);if(t.isSeq(c)){let e=1;return c.items.forEach((r=>{if(t.isScalar(r)&&String(r.value)===String(o)&&r.range){const[t]=r.range;e=i.slice(0,t).split("\n").length}})),e}if(t.isMap(c)){let e=1;return c.items.forEach((r=>{const s=r.key,n=r.value;if(t.isScalar(s)&&t.isScalar(n)&&String(n.value)===String(o)&&n.range){const[t]=n.range;e=i.slice(0,t).split("\n").length}})),e}return 1}class y{static name="no-build-and-image";get name(){return y.name}type="error";category="best-practice";severity="major";meta={description:"Each service must use either `build` or `image`, not both.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-build-and-image-rule.md"};fixable=!1;options;constructor(e){this.options={checkPullPolicy:!0,...e}}getMessage({serviceName:e}){return`Service "${e}" is using both "build" and "image". Use one of them, but not both.`}check(e){const i=[],r=u(e.sourceCode),s=r.get("services");return t.isMap(s)?(s.items.forEach((s=>{if(!t.isScalar(s.key))return;const o=String(s.key.value),n=s.value;if(!t.isMap(n))return;const a=n.has("build"),c=n.has("image"),p=n.has("pull_policy");if(a&&c&&(!this.options.checkPullPolicy||!p)){const t=h(r,e.sourceCode,o,"build");i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({serviceName:o}),line:t,column:1,meta:this.meta,fixable:this.fixable,data:{serviceName:o}})}})),i):[]}}class v{static name="no-duplicate-container-names";get name(){return v.name}type="error";category="security";severity="critical";meta={description:"Container names must be unique to prevent conflicts and ensure proper container management.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-container-names-rule.md"};fixable=!1;getMessage({serviceName:e,containerName:t,anotherService:i}){return`Service "${e}" has a duplicate container name "${t}" with service "${i}". Container names MUST BE unique.`}check(e){const i=[],r=u(e.sourceCode),s=r.get("services");if(!t.isMap(s))return[];const o=new Map;return s.items.forEach((s=>{if(!t.isScalar(s.key))return;const n=String(s.key.value),a=s.value;if(t.isMap(a)&&a.has("container_name")){const t=String(a.get("container_name"));if(o.has(t)){const s=String(o.get(t)),a=h(r,e.sourceCode,n,"container_name");i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({serviceName:n,containerName:t,anotherService:s}),line:a,column:1,meta:this.meta,fixable:this.fixable,data:{serviceName:n,containerName:t,anotherService:s}})}else o.set(t,n)}})),i}}function b(e){if(t.isScalar(e)){const t=String(e.value),[i,r]=t.split("/"),o=i.split(/:(?![^[]*])/);return o[0].startsWith("[")&&o[0].endsWith("]")&&(o[0]=o[0].slice(1,-1)),s.isIP(o[0])?{port:String(o[1]),protocol:r||"tcp"}:{port:o[0],protocol:r||"tcp"}}return t.isMap(e)?{port:e.get("published")?.toString()||"",protocol:e.get("protocol")?.toString()||"tcp"}:{port:"",protocol:"tcp"}}class _{static name="no-duplicate-exported-ports";get name(){return _.name}type="error";category="security";severity="critical";meta={description:"Exported ports must be unique to avoid conflicts.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-exported-ports-rule.md"};fixable=!1;getMessage({serviceName:e,publishedPort:t,anotherService:i}){return`Service "${e}" is exporting port "${t}" which is already used by service "${i}".`}check(e){const i=[],r=u(e.sourceCode),s=r.get("services");if(!t.isMap(s))return[];const o=new Map;return s.items.forEach((s=>{if(!t.isScalar(s.key))return;const n=String(s.key.value),a=s.value;if(!t.isMap(a)||!a.has("ports"))return;const c=a.get("ports");t.isSeq(c)&&c.items.forEach((t=>{const{port:s,protocol:a}=b(t),c=function(e){const[t,i]=e.split("-").map(Number);if(Number.isNaN(t)||Number.isNaN(i))return[];if(!i)return[t.toString()];if(t>i)return[];const r=[];for(let e=t;e<=i;e++)r.push(e.toString());return r}(s);c.some((t=>{const c=`${t}/${a}`;if(o.has(c)){const t=h(r,e.sourceCode,n,"ports");return i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({serviceName:n,publishedPort:s,anotherService:String(o.get(c))}),line:t,column:1,meta:this.meta,fixable:this.fixable,data:{serviceName:n,publishedPort:s,exportedPortsMap:o}}),!0}return!1})),c.forEach((e=>{const t=`${e}/${a}`;o.has(t)||o.set(t,n)}))}))})),i}}class x{static name="no-quotes-in-volumes";get name(){return x.name}type="warning";category="style";severity="info";meta={description:"Values in the `volumes` section should not be enclosed in quotes.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-quotes-in-volumes-rule.md"};fixable=!0;getMessage(){return"Quotes should not be used in volume names."}static extractVolumes(e,i){e&&t.isMap(e)&&e.items.forEach((e=>{if(!t.isMap(e.value))return;e.value.items.forEach((e=>{if(!t.isMap(e.value))return;const r=e.value.items.find((e=>t.isScalar(e.key)&&"volumes"===e.key.value));r&&t.isSeq(r.value)&&r.value.items.forEach((e=>{t.isScalar(e)&&i(e)}))}))}))}check(e){const t=[],i=u(e.sourceCode);return x.extractVolumes(i.contents,(i=>{"PLAIN"!==i.type&&t.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage(),line:g(e.sourceCode,String(i.value)),column:1,meta:this.meta,fixable:this.fixable,data:{volume:i}})})),t}fix(e){const t=u(e);return x.extractVolumes(t.contents,(e=>{"PLAIN"!==e.type&&(e.type="PLAIN")})),m(t)}}class P{static name="no-unbound-port-interfaces";get name(){return P.name}type="error";category="security";severity="major";meta={description:"Exported ports must be bound to specific interfaces to avoid unintentional exposure.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-unbound-port-interfaces-rule.md"};fixable=!1;getMessage({serviceName:e,port:t}){return`Service "${e}" is exporting port "${t}" without specifying the interface to listen on.`}check(e){const i=[],r=u(e.sourceCode),o=r.get("services");return t.isMap(o)?(o.items.forEach((o=>{if(!t.isScalar(o.key))return;const n=String(o.key.value),a=o.value;if(!t.isMap(a)||!a.has("ports"))return;const c=a.get("ports");t.isSeq(c)&&c.items.forEach((o=>{if(""===function(e){if(t.isScalar(e)){const t=String(e.value).split(/:(?![^[]*])/);return t[0].startsWith("[")&&t[0].endsWith("]")&&(t[0]=t[0].slice(1,-1)),s.isIP(t[0])?String(t[0]):""}return t.isMap(e)&&e.get("host_ip")?.toString()||""}(o)){const s=h(r,e.sourceCode,n,"ports");i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({serviceName:n,port:t.isSeq(o)?o.toString():String(o)}),line:s,column:1,meta:this.meta,fixable:this.fixable,data:{serviceName:n,portItem:o}})}}))})),i):[]}}class S{static name="no-version-field";get name(){return S.name}type="error";category="best-practice";severity="minor";meta={description:"The `version` field must be absent in the configuration.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-version-field-rule.md"};fixable=!0;getMessage(){return'The "version" field should not be present.'}check(e){const t=[];return e.content&&"version"in e.content&&t.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage(),line:f(e.sourceCode,"version"),column:1,meta:this.meta,fixable:this.fixable,data:{}}),t}fix(e){const t=e.split("\n"),i=t.findIndex((e=>e.trim().startsWith("version:")));return-1!==i&&t.splice(i,1),t.join("\n")}}class w{static name="require-project-name-field";get name(){return w.name}type="warning";category="best-practice";severity="minor";meta={description:"The `name` field should be included in the configuration.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/require-project-name-field-rule.md"};fixable=!1;getMessage(){return'The "name" field should be present.'}check(e){const t=[];return e.content&&!("name"in e.content)&&t.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage(),line:1,column:1,meta:this.meta,fixable:this.fixable,data:{}}),t}}class k{static name="require-quotes-in-ports";get name(){return k.name}type="warning";category="best-practice";severity="minor";meta={description:"Ports in `ports` and `expose` sections should be enclosed in quotes.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/require-quotes-in-ports-rule.md"};fixable=!0;options;constructor(e){this.options={quoteType:"single",portsSections:["ports","expose"],...e}}getMessage(){return"Ports in `ports` and `expose` sections should be enclosed in quotes."}getQuoteType(){return"single"===this.options.quoteType?"QUOTE_SINGLE":"QUOTE_DOUBLE"}static extractValues(e,i,r){e&&t.isMap(e)&&e.items.forEach((e=>{if(!t.isMap(e.value))return;e.value.items.forEach((e=>{if(!t.isMap(e.value))return;const s=e.value.items.find((e=>t.isScalar(e.key)&&e.key.value===i));s&&t.isSeq(s.value)&&s.value.items.forEach((i=>{t.isScalar(i)&&r(e,i)}))}))}))}check(e){const t=[],i=u(e.sourceCode);return this.options.portsSections.forEach((r=>{k.extractValues(i.contents,r,((s,o)=>{o.type!==this.getQuoteType()&&t.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage(),line:h(i,e.sourceCode,String(s.key),r,String(o.value)),column:1,meta:this.meta,fixable:this.fixable,data:{service:s,port:o,section:r}})}))})),t}fix(e){const i=u(e);return this.options.portsSections.forEach((e=>{k.extractValues(i.contents,e,((e,i)=>{if(i.type!==this.getQuoteType()){const e=new t.Scalar(String(i.value));e.type=this.getQuoteType(),Object.assign(i,e)}}))})),m(i)}}class C{static name="service-container-name-regex";get name(){return C.name}type="error";category="security";severity="critical";static containerNameRegex=/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/;meta={description:`Container names must match the pattern \`${C.containerNameRegex}\` to comply with Docker naming conventions.`,url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-container-name-regex-rule.md"};fixable=!1;getMessage({serviceName:e,containerName:t}){return`Service "${e}" has an invalid container name "${t}". It must match the regex pattern ${C.containerNameRegex}.`}check(e){const i=[],r=u(e.sourceCode),s=r.get("services");return t.isMap(s)?(s.items.forEach((s=>{if(!t.isScalar(s.key))return;const o=String(s.key.value),n=s.value;if(!t.isMap(n)||!n.has("container_name"))return;const a=String(n.get("container_name"));if(!C.containerNameRegex.test(a)){const t=h(r,e.sourceCode,o,"container_name");i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({serviceName:o,containerName:a}),line:t,column:1,meta:this.meta,fixable:this.fixable,data:{serviceName:o,containerName:a}})}})),i):[]}}class ${static name="service-dependencies-alphabetical-order";get name(){return $.name}type="warning";category="style";severity="info";meta={description:"Services in the `depends_on` section should be sorted alphabetically.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-dependencies-alphabetical-order-rule.md"};fixable=!0;getMessage=({serviceName:e})=>`Services in "depends_on" for service "${e}" should be in alphabetical order.`;static extractServiceName(e){return t.isScalar(e)?String(e.value):t.isPair(e)?String(e.key):""}check(e){const i=[],r=u(e.sourceCode),s=r.get("services");return t.isMap(s)?(s.items.forEach((s=>{if(!t.isScalar(s.key))return;const o=String(s.key.value),n=s.value;if(!n||!t.isMap(n))return;const a=n.get("depends_on");if(!t.isSeq(a)&&!t.isMap(a))return;const c=a.items.map((e=>$.extractServiceName(e))),p=[...c].sort(((e,t)=>e.localeCompare(t)));if(JSON.stringify(c)!==JSON.stringify(p)){const t=h(r,e.sourceCode,o,"depends_on");i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({serviceName:o}),line:t,column:1,meta:this.meta,fixable:this.fixable,data:{serviceName:o}})}})),i):[]}fix(e){const i=u(e),r=i.get("services");return t.isMap(r)?(r.items.forEach((e=>{const i=e.value;if(!i||!t.isMap(i))return;const r=i.get("depends_on");(t.isSeq(r)||t.isMap(r))&&r.items.sort(((e,t)=>{const i=$.extractServiceName(e),r=$.extractServiceName(t);return i.localeCompare(r)}))})),m(i)):e}}class M{static name="service-image-require-explicit-tag";get name(){return M.name}type="error";category="security";severity="major";meta={description:'Services must use a specific image tag instead of "latest", "stable" or similar or no tag.',url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-image-require-explicit-tag-rule.md"};fixable=!1;options;constructor(e){this.options={prohibitedTags:["latest","stable","edge","test","nightly","dev","beta","canary","lts"],...e}}getMessage({serviceName:e,image:t}){return`Service "${e}" is using the image "${t}", which does not have a concrete version tag. Specify a concrete version tag.`}isImageTagExplicit(e){const t=e.split("/").pop();if(!t||!t.includes(":"))return!1;const[,i]=t.split(":");return!this.options.prohibitedTags.includes(i)}check(e){const i=[],r=u(e.sourceCode),s=r.get("services");return t.isMap(s)?(s.items.forEach((s=>{if(!t.isScalar(s.key))return;const o=String(s.key.value),n=s.value;if(!t.isMap(n)||!n.has("image"))return;const a=String(n.get("image"));if(!this.isImageTagExplicit(a)){const t=h(r,e.sourceCode,o,"image");i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({serviceName:o,image:a}),line:t,column:1,meta:this.meta,fixable:this.fixable,data:{image:a,serviceName:o}})}})),i):[]}}var N,I;!function(e){e.CoreDefinitions="Core Definitions",e.ServiceDependencies="Service Dependencies",e.DataManagementAndConfiguration="Data Management and Configuration",e.EnvironmentConfiguration="Environment Configuration",e.Networking="Networking",e.RuntimeBehavior="Runtime Behavior",e.OperationalMetadata="Operational Metadata",e.SecurityAndExecutionContext="Security and Execution Context",e.Other="Other"}(N||(N={}));class E{static name="service-keys-order";get name(){return E.name}type="warning";category="style";severity="minor";meta={description:"Keys within each service should follow a specific order.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-keys-order-rule.md"};fixable=!0;getMessage({serviceName:e,key:t,correctOrder:i}){return`Key "${t}" in service "${e}" is out of order. Expected order is: ${i.join(", ")}.`}options;constructor(e){const t={groups:{[N.CoreDefinitions]:["image","build","container_name"],[N.ServiceDependencies]:["depends_on"],[N.DataManagementAndConfiguration]:["volumes","volumes_from","configs","secrets"],[N.EnvironmentConfiguration]:["environment","env_file"],[N.Networking]:["ports","networks","network_mode","extra_hosts"],[N.RuntimeBehavior]:["command","entrypoint","working_dir","restart","healthcheck"],[N.OperationalMetadata]:["logging","labels"],[N.SecurityAndExecutionContext]:["user","isolation"],[N.Other]:[]},groupOrder:[N.CoreDefinitions,N.ServiceDependencies,N.DataManagementAndConfiguration,N.EnvironmentConfiguration,N.Networking,N.RuntimeBehavior,N.OperationalMetadata,N.SecurityAndExecutionContext,N.Other]};this.options={groups:{...t.groups,...e?.groups},groupOrder:e?.groupOrder||t.groupOrder}}getCorrectOrder(e){const t=e.filter((e=>!Object.values(this.options.groups).flat().includes(e))).sort(((e,t)=>e.localeCompare(t)));return[...this.options.groupOrder.flatMap((e=>this.options.groups[e])),...t]}check(e){const i=[],r=u(e.sourceCode),s=r.get("services");return t.isMap(s)?(s.items.forEach((s=>{if(!t.isScalar(s.key))return;const o=String(s.key.value),n=s.value;if(!t.isMap(n))return;const a=n.items.map((e=>String(e.key))),c=this.getCorrectOrder(a);let p=-1;a.forEach((t=>{const s=c.indexOf(t);if(-1!==s){if(s<p){const s=h(r,e.sourceCode,o,t);i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({serviceName:o,key:t,correctOrder:c}),line:s,column:1,meta:this.meta,fixable:this.fixable,data:{serviceName:o,key:t,correctOrder:c}})}p=s}}))})),i):[]}fix(e){const i=u(e),r=i.get("services");return t.isMap(r)?(r.items.forEach((e=>{if(!t.isScalar(e.key))return;const i=String(e.key.value),s=e.value;if(!t.isMap(s))return;const o=s.items.map((e=>t.isScalar(e.key)?String(e.key.value):"")).filter(Boolean),n=this.getCorrectOrder(o),a=new t.YAMLMap;n.forEach((e=>{const i=s.items.find((i=>t.isScalar(i.key)&&i.key.value===e));i&&a.add(i)})),o.forEach((e=>{if(!n.includes(e)){const i=s.items.find((i=>t.isScalar(i.key)&&i.key.value===e));i&&a.add(i)}})),r.set(i,a)})),m(i)):e}}class O{static name="service-ports-alphabetical-order";get name(){return O.name}type="warning";category="style";severity="info";meta={description:"The list of ports in a service should be sorted alphabetically.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-ports-alphabetical-order-rule.md"};fixable=!0;getMessage({serviceName:e}){return`Ports in service "${e}" should be in alphabetical order.`}check(e){const i=[],r=u(e.sourceCode),s=r.get("services");return t.isMap(s)?(s.items.forEach((s=>{if(!t.isScalar(s.key))return;const o=String(s.key.value),n=s.value;if(!t.isMap(n)||!t.isSeq(n.get("ports")))return;const a=n.get("ports");if(!t.isSeq(a))return;const c=a.items.map((e=>b(e).port)),p=[...c].sort(((e,t)=>e.localeCompare(t,void 0,{numeric:!0})));if(JSON.stringify(c)!==JSON.stringify(p)){const t=h(r,e.sourceCode,o,"ports");i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:`Ports in service "${o}" should be in alphabetical order.`,line:t,column:1,meta:this.meta,fixable:this.fixable,data:{serviceName:o}})}})),i):[]}fix(e){const i=u(e),r=i.get("services");return t.isMap(r)?(r.items.forEach((e=>{const i=e.value;if(!t.isMap(i)||!t.isSeq(i.get("ports")))return;const r=i.get("ports");t.isSeq(r)&&r.items.sort(((e,t)=>{const i=b(e).port,r=b(t).port;return i.localeCompare(r,void 0,{numeric:!0})}))})),m(i)):e}}class j{static name="services-alphabetical-order";get name(){return j.name}type="warning";category="style";severity="minor";meta={description:"Services should be sorted alphabetically.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/services-alphabetical-order-rule.md"};fixable=!0;getMessage({serviceName:e,misplacedBefore:t}){return`Service "${e}" should be before "${t}".`}static findMisplacedService(e,t){let i="";return e.forEach((e=>{e.localeCompare(t)>0&&(!i||e.localeCompare(i)<0)&&(i=e)})),i}check(e){const i=[],r=u(e.sourceCode),s=r.get("services");if(!t.isMap(s))return[];const o=[];return s.items.forEach((s=>{if(!t.isScalar(s.key))return;const n=String(s.key.value),a=j.findMisplacedService(o,n);if(a){const t=h(r,e.sourceCode,n);i.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({serviceName:n,misplacedBefore:a}),line:t,column:1,meta:this.meta,fixable:this.fixable,data:{serviceName:n,misplacedBefore:a}})}o.push(n)})),i}fix(e){const i=u(e),r=i.get("services");if(!t.isMap(r))return e;const s=new t.YAMLMap;return r.items.toSorted(((e,i)=>t.isScalar(e.key)&&t.isScalar(i.key)?String(e.key.value).localeCompare(String(i.key.value)):0)).forEach((e=>{s.add(e)})),i.set("services",s),m(i)}}!function(e){e.XProperties="x-properties",e.Version="version",e.Name="name",e.Include="include",e.Services="services",e.Networks="networks",e.Volumes="volumes",e.Secrets="secrets",e.Configs="configs"}(I||(I={}));class A{static name="top-level-properties-order";get name(){return A.name}type="warning";category="style";severity="major";meta={description:"Top-level properties should follow a specific order.",url:"https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/top-level-properties-order-rule.md"};fixable=!0;options;constructor(e){const t={customOrder:[I.XProperties,I.Version,I.Name,I.Include,I.Services,I.Networks,I.Volumes,I.Secrets,I.Configs]};this.options={...t,...e}}getMessage({key:e,correctOrder:t}){return`Property "${e}" is out of order. Expected order is: ${t.join(", ")}.`}check(e){const t=[],i=Object.keys(e.content),r=i.filter((e=>e.startsWith("x-"))).sort(((e,t)=>e.localeCompare(t))),s=this.options.customOrder.flatMap((e=>e===I.XProperties?r:[e]));let o=-1;return i.forEach((i=>{const r=s.indexOf(i);if(-1===r||r<o){const r=f(e.sourceCode,i);t.push({rule:this.name,type:this.type,category:this.category,severity:this.severity,message:this.getMessage({key:i,correctOrder:s}),line:r,column:1,meta:this.meta,fixable:this.fixable,data:{key:i,correctOrder:s}})}else o=r})),t}fix(e){const i=u(e),{contents:r}=i;if(!t.isMap(r))return e;const s=r.items.map((e=>t.isScalar(e.key)?String(e.key.value):"")).filter(Boolean).filter((e=>e.startsWith("x-"))).sort(((e,t)=>e.localeCompare(t))),o=this.options.customOrder.flatMap((e=>e===I.XProperties?s:[e])),n=new t.YAMLMap;return o.forEach((e=>{const i=r.items.find((i=>t.isScalar(i.key)&&String(i.key.value)===e));i&&n.items.push(i)})),i.contents=n,m(i)}}var D={NoBuildAndImageRule:y,NoDuplicateContainerNamesRule:v,NoDuplicateExportedPortsRule:_,NoQuotesInVolumesRule:x,NoUnboundPortInterfacesRule:P,NoVersionFieldRule:S,RequireProjectNameFieldRule:w,RequireQuotesInPortsRule:k,ServiceContainerNameRegexRule:C,ServiceDependenciesAlphabeticalOrderRule:$,ServiceImageRequireExplicitTagRule:M,ServiceKeysOrderRule:E,ServicePortsAlphabeticalOrderRule:O,ServicesAlphabeticalOrderRule:j,TopLevelPropertiesOrderRule:A};class T extends Error{keyword;instancePath;schemaPath;details;constructor(e){super(`Validation error: ${e?.message}`),this.name="ComposeValidationError",this.keyword=e.keyword,this.instancePath=e.instancePath,this.schemaPath=e.schemaPath,this.details=e}toString(){return`ComposeValidationError: instancePath="${this.instancePath}", schemaPath="${this.schemaPath}", message="${this.message}".`}}var L={$schema:"https://json-schema.org/draft-07/schema",$id:"compose_spec.json",type:"object",title:"Compose Specification",description:"The Compose file is a YAML file defining a multi-containers based application.",properties:{version:{type:"string",deprecated:!0,description:"declared for backward compatibility, ignored. Please remove it."},name:{type:"string",description:"define the Compose project name, until user defines one explicitly."},include:{type:"array",items:{$ref:"#/definitions/include"},description:"compose sub-projects to be included."},services:{type:"object",patternProperties:{"^[a-zA-Z0-9._-]+$":{$ref:"#/definitions/service"}},additionalProperties:!1,description:"The services that will be used by your application."},networks:{type:"object",patternProperties:{"^[a-zA-Z0-9._-]+$":{$ref:"#/definitions/network"}},description:"Networks that are shared among multiple services."},volumes:{type:"object",patternProperties:{"^[a-zA-Z0-9._-]+$":{$ref:"#/definitions/volume"}},additionalProperties:!1,description:"Named volumes that are shared among multiple services."},secrets:{type:"object",patternProperties:{"^[a-zA-Z0-9._-]+$":{$ref:"#/definitions/secret"}},additionalProperties:!1,description:"Secrets that are shared among multiple services."},configs:{type:"object",patternProperties:{"^[a-zA-Z0-9._-]+$":{$ref:"#/definitions/config"}},additionalProperties:!1,description:"Configurations that are shared among multiple services."}},patternProperties:{"^x-":{}},additionalProperties:!1,definitions:{service:{type:"object",description:"Configuration for a service.",properties:{develop:{$ref:"#/definitions/development"},deploy:{$ref:"#/definitions/deployment"},annotations:{$ref:"#/definitions/list_or_dict"},attach:{type:["boolean","string"]},build:{description:"Configuration options for building the service's image.",oneOf:[{type:"string",description:"Path to the build context. Can be a relative path or a URL."},{type:"object",properties:{context:{type:"string",description:"Path to the build context. Can be a relative path or a URL."},dockerfile:{type:"string",description:"Name of the Dockerfile to use for building the image."},dockerfile_inline:{type:"string",description:"Inline Dockerfile content to use instead of a Dockerfile from the build context."},entitlements:{type:"array",items:{type:"string"},description:"List of extra privileged entitlements to grant to the build process."},args:{$ref:"#/definitions/list_or_dict",description:"Build-time variables, specified as a map or a list of KEY=VAL pairs."},ssh:{$ref:"#/definitions/list_or_dict",description:"SSH agent socket or keys to expose to the build. Format is either a string or a list of 'default|<id>[=<socket>|<key>[,<key>]]'."},labels:{$ref:"#/definitions/list_or_dict",description:"Labels to apply to the built image."},cache_from:{type:"array",items:{type:"string"},description:"List of sources the image builder should use for cache resolution"},cache_to:{type:"array",items:{type:"string"},description:"Cache destinations for the build cache."},no_cache:{type:["boolean","string"],description:"Do not use cache when building the image."},additional_contexts:{$ref:"#/definitions/list_or_dict",description:"Additional build contexts to use, specified as a map of name to context path or URL."},network:{type:"string",description:"Network mode to use for the build. Options include 'default', 'none', 'host', or a network name."},pull:{type:["boolean","string"],description:"Always attempt to pull a newer version of the image."},target:{type:"string",description:"Build stage to target in a multi-stage Dockerfile."},shm_size:{type:["integer","string"],description:"Size of /dev/shm for the build container. A string value can use suffix like '2g' for 2 gigabytes."},extra_hosts:{$ref:"#/definitions/extra_hosts",description:"Add hostname mappings for the build container."},isolation:{type:"string",description:"Container isolation technology to use for the build process."},privileged:{type:["boolean","string"],description:"Give extended privileges to the build container."},secrets:{$ref:"#/definitions/service_config_or_secret",description:"Secrets to expose to the build. These are accessible at build-time."},tags:{type:"array",items:{type:"string"},description:"Additional tags to apply to the built image."},ulimits:{$ref:"#/definitions/ulimits",description:"Override the default ulimits for the build container."},platforms:{type:"array",items:{type:"string"},description:"Platforms to build for, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'."}},additionalProperties:!1,patternProperties:{"^x-":{}}}]},blkio_config:{type:"object",description:"Block IO configuration for the service.",properties:{device_read_bps:{type:"array",description:"Limit read rate (bytes per second) from a device.",items:{$ref:"#/definitions/blkio_limit"}},device_read_iops:{type:"array",description:"Limit read rate (IO per second) from a device.",items:{$ref:"#/definitions/blkio_limit"}},device_write_bps:{type:"array",description:"Limit write rate (bytes per second) to a device.",items:{$ref:"#/definitions/blkio_limit"}},device_write_iops:{type:"array",description:"Limit write rate (IO per second) to a device.",items:{$ref:"#/definitions/blkio_limit"}},weight:{type:["integer","string"],description:"Block IO weight (relative weight) for the service, between 10 and 1000."},weight_device:{type:"array",description:"Block IO weight (relative weight) for specific devices.",items:{$ref:"#/definitions/blkio_weight"}}},additionalProperties:!1},cap_add:{type:"array",items:{type:"string"},uniqueItems:!0,description:"Add Linux capabilities. For example, 'CAP_SYS_ADMIN', 'SYS_ADMIN', or 'NET_ADMIN'."},cap_drop:{type:"array",items:{type:"string"},uniqueItems:!0,description:"Drop Linux capabilities. For example, 'CAP_SYS_ADMIN', 'SYS_ADMIN', or 'NET_ADMIN'."},cgroup:{type:"string",enum:["host","private"],description:"Specify the cgroup namespace to join. Use 'host' to use the host's cgroup namespace, or 'private' to use a private cgroup namespace."},cgroup_parent:{type:"string",description:"Specify an optional parent cgroup for the container."},command:{$ref:"#/definitions/command",description:"Override the default command declared by the container image, for example 'CMD' in Dockerfile."},configs:{$ref:"#/definitions/service_config_or_secret",description:"Grant access to Configs on a per-service basis."},container_name:{type:"string",description:"Specify a custom container name, rather than a generated default name."},cpu_count:{oneOf:[{type:"string"},{type:"integer",minimum:0}],description:"Number of usable CPUs."},cpu_percent:{oneOf:[{type:"string"},{type:"integer",minimum:0,maximum:100}],description:"Percentage of CPU resources to use."},cpu_shares:{type:["number","string"],description:"CPU shares (relative weight) for the container."},cpu_quota:{type:["number","string"],description:"Limit the CPU CFS (Completely Fair Scheduler) quota."},cpu_period:{type:["number","string"],description:"Limit the CPU CFS (Completely Fair Scheduler) period."},cpu_rt_period:{type:["number","string"],description:"Limit the CPU real-time period in microseconds or a duration."},cpu_rt_runtime:{type:["number","string"],description:"Limit the CPU real-time runtime in microseconds or a duration."},cpus:{type:["number","string"],description:"Number of CPUs to use. A floating-point value is supported to request partial CPUs."},cpuset:{type:"string",description:"CPUs in which to allow execution (0-3, 0,1)."},credential_spec:{type:"object",description:"Configure the credential spec for managed service account.",properties:{config:{type:"string",description:"The name of the credential spec Config to use."},file:{type:"string",description:"Path to a credential spec file."},registry:{type:"string",description:"Path to a credential spec in the Windows registry."}},additionalProperties:!1,patternProperties:{"^x-":{}}},depends_on:{oneOf:[{$ref:"#/definitions/list_of_strings"},{type:"object",additionalProperties:!1,patternProperties:{"^[a-zA-Z0-9._-]+$":{type:"object",additionalProperties:!1,patternProperties:{"^x-":{}},properties:{restart:{type:["boolean","string"],description:"Whether to restart dependent services when this service is restarted."},required:{type:"boolean",default:!0,description:"Whether the dependency is required for the dependent service to start."},condition:{type:"string",enum:["service_started","service_healthy","service_completed_successfully"],description:"Condition to wait for. 'service_started' waits until the service has started, 'service_healthy' waits until the service is healthy (as defined by its healthcheck), 'service_completed_successfully' waits until the service has completed successfully."}},required:["condition"]}}}],description:"Express dependency between services. Service dependencies cause services to be started in dependency order. The dependent service will wait for the dependency to be ready before starting."},device_cgroup_rules:{$ref:"#/definitions/list_of_strings",description:"Add rules to the cgroup allowed devices list."},devices:{type:"array",description:"List of device mappings for the container.",items:{oneOf:[{type:"string"},{type:"object",required:["source"],properties:{source:{type:"string",description:"Path on the host to the device."},target:{type:"string",description:"Path in the container where the device will be mapped."},permissions:{type:"string",description:"Cgroup permissions for the device (rwm)."}},additionalProperties:!1,patternProperties:{"^x-":{}}}]}},dns:{$ref:"#/definitions/string_or_list",description:"Custom DNS servers to set for the service container."},dns_opt:{type:"array",items:{type:"string"},uniqueItems:!0,description:"Custom DNS options to be passed to the container's DNS resolver."},dns_search:{$ref:"#/definitions/string_or_list",description:"Custom DNS search domains to set on the service container."},domainname:{type:"string",description:"Custom domain name to use for the service container."},entrypoint:{$ref:"#/definitions/command",description:"Override the default entrypoint declared by the container image, for example 'ENTRYPOINT' in Dockerfile."},env_file:{$ref:"#/definitions/env_file",description:"Add environment variables from a file or multiple files. Can be a single file path or a list of file paths."},label_file:{$ref:"#/definitions/label_file",description:"Add metadata to containers using files containing Docker labels."},environment:{$ref:"#/definitions/list_or_dict",description:"Add environment variables. You can use either an array or a list of KEY=VAL pairs."},expose:{type:"array",items:{type:["string","number"]},uniqueItems:!0,description:"Expose ports without publishing them to the host machine - they'll only be accessible to linked services."},extends:{oneOf:[{type:"string"},{type:"object",properties:{service:{type:"string",description:"The name of the service to extend."},file:{type:"string",description:"The file path where the service to extend is defined."}},required:["service"],additionalProperties:!1}],description:"Extend another service, in the current file or another file."},provider:{type:"object",description:"Specify a service which will not be manage by Compose directly, and delegate its management to an external provider.",properties:{type:{type:"string",description:"External component used by Compose to manage setup and teardown lifecycle of the service."},options:{type:"object",description:"Provider-specific options.",patternProperties:{"^.+$":{type:["string","number","null"]}}}},additionalProperties:!1,patternProperties:{"^x-":{}}},external_links:{type:"array",items:{type:"string"},uniqueItems:!0,description:"Link to services started outside this Compose application. Specify services as <service_name>:<alias>."},extra_hosts:{$ref:"#/definitions/extra_hosts",description:"Add hostname mappings to the container network interface configuration."},gpus:{$ref:"#/definitions/gpus",description:"Define GPU devices to use. Can be set to 'all' to use all GPUs, or a list of specific GPU devices."},group_add:{type:"array",items:{type:["string","number"]},uniqueItems:!0,description:"Add additional groups which user inside the container should be member of."},healthcheck:{$ref:"#/definitions/healthcheck",description:"Configure a health check for the container to monitor its health status."},hostname:{type:"string",description:"Define a custom hostname for the service container."},image:{type:"string",description:"Specify the image to start the container from. Can be a repository/tag, a digest, or a local image ID."},init:{type:["boolean","string"],description:"Run as an init process inside the container that forwards signals and reaps processes."},ipc:{type:"string",description:"IPC sharing mode for the service container. Use 'host' to share the host's IPC namespace, 'service:[service_name]' to share with another service, or 'shareable' to allow other services to share this service's IPC namespace."},isolation:{type:"string",description:"Container isolation technology to use. Supported values are platform-specific."},labels:{$ref:"#/definitions/list_or_dict",description:"Add metadata to containers using Docker labels. You can use either an array or a list."},links:{type:"array",items:{type:"string"},uniqueItems:!0,description:"Link to containers in another service. Either specify both the service name and a link alias (SERVICE:ALIAS), or just the service name."},logging:{type:"object",description:"Logging configuration for the service.",properties:{driver:{type:"string",description:"Logging driver to use, such as 'json-file', 'syslog', 'journald', etc."},options:{type:"object",description:"Options for the logging driver.",patternProperties:{"^.+$":{type:["string","number","null"]}}}},additionalProperties:!1,patternProperties:{"^x-":{}}},mac_address:{type:"string",description:"Container MAC address to set."},mem_limit:{type:["number","string"],description:"Memory limit for the container. A string value can use suffix like '2g' for 2 gigabytes."},mem_reservation:{type:["string","integer"],description:"Memory reservation for the container."},mem_swappiness:{type:["integer","string"],description:"Container memory swappiness as percentage (0 to 100)."},memswap_limit:{type:["number","string"],description:"Amount of memory the container is allowed to swap to disk. Set to -1 to enable unlimited swap."},network_mode:{type:"string",description:"Network mode. Values can be 'bridge', 'host', 'none', 'service:[service name]', or 'container:[container name]'."},networks:{oneOf:[{$ref:"#/definitions/list_of_strings"},{type:"object",patternProperties:{"^[a-zA-Z0-9._-]+$":{oneOf:[{type:"object",properties:{aliases:{$ref:"#/definitions/list_of_strings",description:"Alternative hostnames for this service on the network."},interface_name:{type:"string",description:"Interface network name used to connect to network"},ipv4_address:{type:"string",description:"Specify a static IPv4 address for this service on this network."},ipv6_address:{type:"string",description:"Specify a static IPv6 address for this service on this network."},link_local_ips:{$ref:"#/definitions/list_of_strings",description:"List of link-local IPs."},mac_address:{type:"string",description:"Specify a MAC address for this service on this network."},driver_opts:{type:"object",description:"Driver options for this network.",patternProperties:{"^.+$":{type:["string","number"]}}},priority:{type:"number",description:"Specify the priority for the network connection."},gw_priority:{type:"number",description:"Specify the gateway priority for the network connection."}},additionalProperties:!1,patternProperties:{"^x-":{}}},{type:"null"}]}},additionalProperties:!1}],description:"Networks to join, referencing entries under the top-level networks key. Can be a list of network names or a mapping of network name to network configuration."},oom_kill_disable:{type:["boolean","string"],description:"Disable OOM Killer for the container."},oom_score_adj:{oneOf:[{type:"string"},{type:"integer",minimum:-1e3,maximum:1e3}],description:"Tune host's OOM preferences for the container (accepts -1000 to 1000)."},pid:{type:["string","null"],description:"PID mode for container."},pids_limit:{type:["number","string"],description:"Tune a container's PIDs limit. Set to -1 for unlimited PIDs."},platform:{type:"string",description:"Target platform to run on, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'."},ports:{type:"array",description:"Expose container ports. Short format ([HOST:]CONTAINER[/PROTOCOL]).",items:{oneOf:[{type:"number"},{type:"string"},{type:"object",properties:{name:{type:"string",description:"A human-readable name for this port mapping."},mode:{type:"string",description:"The port binding mode, either 'host' for publishing a host port or 'ingress' for load balancing."},host_ip:{type:"string",description:"The host IP to bind to."},target:{type:["integer","string"],description:"The port inside the container."},published:{type:["string","integer"],description:"The publicly exposed port."},protocol:{type:"string",description:"The port protocol (tcp or udp)."},app_protocol:{type:"string",description:"Application protocol to use with the port (e.g., http, https, mysql)."}},additionalProperties:!1,patternProperties:{"^x-":{}}}]},uniqueItems:!0},post_start:{type:"array",items:{$ref:"#/definitions/service_hook"},description:"Commands to run after the container starts. If any command fails, the container stops."},pre_stop:{type:"array",items:{$ref:"#/definitions/service_hook"},description:"Commands to run before the container stops. If any command fails, the container stop is aborted."},privileged:{type:["boolean","string"],description:"Give extended privileges to the service container."},profiles:{$ref:"#/definitions/list_of_strings",description:"List of profiles for this service. When profiles are specified, services are only started when the profile is activated."},pull_policy:{type:"string",pattern:"always|never|build|if_not_present|missing|refresh|daily|weekly|every_([0-9]+[wdhms])+",description:"Policy for pulling images. Options include: 'always', 'never', 'if_not_present', 'missing', 'build', or time-based refresh policies."},pull_refresh_after:{type:"string",description:"Time after which to refresh the image. Used with pull_policy=refresh."},read_only:{type:["boolean","string"],description:"Mount the container's filesystem as read only."},restart:{type:"string",description:"Restart policy for the service container. Options include: 'no', 'always', 'on-failure', and 'unless-stopped'."},runtime:{type:"string",description:"Runtime to use for this container, e.g., 'runc'."},scale:{type:["integer","string"],description:"Number of containers to deploy for this service."},security_opt:{type:"array",items:{type:"string"},uniqueItems:!0,description:"Override the default labeling scheme for each container."},shm_size:{type:["number","string"],description:"Size of /dev/shm. A string value can use suffix like '2g' for 2 gigabytes."},secrets:{$ref:"#/definitions/service_config_or_secret",description:"Grant access to Secrets on a per-service basis."},sysctls:{$ref:"#/definitions/list_or_dict",description:"Kernel parameters to set in the container. You can use either an array or a list."},stdin_open:{type:["boolean","string"],description:"Keep STDIN open even if not attached."},stop_grace_period:{type:"string",description:"Time to wait for the container to stop gracefully before sending SIGKILL (e.g., '1s', '1m30s')."},stop_signal:{type:"string",description:"Signal to stop the container (e.g., 'SIGTERM', 'SIGINT')."},storage_opt:{type:"object",description:"Storage driver options for the container."},tmpfs:{$ref:"#/definitions/string_or_list",description:"Mount a temporary filesystem (tmpfs) into the container. Can be a single value or a list."},tty:{type:["boolean","string"],description:"Allocate a pseudo-TTY to service container."},ulimits:{$ref:"#/definitions/ulimits",description:"Override the default ulimits for a container."},user:{type:"string",description:"Username or UID to run the container process as."},uts:{type:"string",description:"UTS namespace to use. 'host' shares the host's UTS namespace."},userns_mode:{type:"string",description:"User namespace to use. 'host' shares the host's user namespace."},volumes:{type:"array",description:"Mount host paths or named volumes accessible to the container. Short syntax (VOLUME:CONTAINER_PATH[:MODE])",items:{oneOf:[{type:"string"},{type:"object",required:["type"],properties:{type:{type:"string",enum:["bind","volume","tmpfs","cluster","npipe","image"],description:"The mount type: bind for mounting host directories, volume for named volumes, tmpfs for temporary filesystems, cluster for cluster volumes, npipe for named pipes, or image for mounting from an image."},source:{type:"string",description:"The source of the mount, a path on the host for a bind mount, a docker image reference for an image mount, or the name of a volume defined in the top-level volumes key. Not applicable for a tmpfs mount."},target:{type:"string",description:"The path in the container where the volume is mounted."},read_only:{type:["boolean","string"],description:"Flag to set the volume as read-only."},consistency:{type:"string",description:"The consistency requirements for the mount. Available values are platform specific."},bind:{type:"object",description:"Configuration specific to bind mounts.",properties:{propagation:{type:"string",description:"The propagation mode for the bind mount: 'shared', 'slave', 'private', 'rshared', 'rslave', or 'rprivate'."},create_host_path:{type:["boolean","string"],description:"Create the host path if it doesn't exist."},recursive:{type:"string",enum:["enabled","disabled","writable","readonly"],description:"Recursively mount the source directory."},selinux:{type:"string",enum:["z","Z"],description:"SELinux relabeling options: 'z' for shared content, 'Z' for private unshared content."}},additionalProperties:!1,patternProperties:{"^x-":{}}},volume:{type:"object",description:"Configuration specific to volume mounts.",properties:{labels:{$ref:"#/definitions/list_or_dict",description:"Labels to apply to the volume."},nocopy:{type:["boolean","string"],description:"Flag to disable copying of data from a container when a volume is created."},subpath:{type:"string",description:"Path within the volume to mount instead of the volume root."}},additionalProperties:!1,patternProperties:{"^x-":{}}},tmpfs:{type:"object",description:"Configuration specific to tmpfs mounts.",properties:{size:{oneOf:[{type:"integer",minimum:0},{type:"string"}],description:"Size of the tmpfs mount in bytes."},mode:{type:["number","string"],description:"File mode of the tmpfs in octal."}},additionalProperties:!1,patternProperties:{"^x-":{}}},image:{type:"object",description:"Configuration specific to image mounts.",properties:{subpath:{type:"string",description:"Path within the image to mount instead of the image root."}},additionalProperties:!1,patternProperties:{"^x-":{}}}},additionalProperties:!1,patternProperties:{"^x-":{}}}]},uniqueItems:!0},volumes_from:{type:"array",items:{type:"string"},uniqueItems:!0,description:"Mount volumes from another service or container. Optionally specify read-only access (ro) or read-write (rw)."},working_dir:{type:"string",description:"The working directory in which the entrypoint or command will be run"}},patternProperties:{"^x-":{}},additionalProperties:!1},healthcheck:{type:"object",description:"Configuration options to determine whether the container is healthy.",properties:{disable:{type:["boolean","string"],description:"Disable any container-specified healthcheck. Set to true to disable."},interval:{type:"string",description:"Time between running the check (e.g., '1s', '1m30s'). Default: 30s."},retries:{type:["number","string"],description:"Number of consecutive failures needed to consider the container as unhealthy. Default: 3."},test:{oneOf:[{type:"string"},{type:"array",items:{type:"string"}}],description:"The test to perform to check container health. Can be a string or a list. The first item is either NONE, CMD, or CMD-SHELL. If it's CMD, the rest of the command is exec'd. If it's CMD-SHELL, the rest is run in the shell."},timeout:{type:"string",description:"Maximum time to allow one check to run (e.g., '1s', '1m30s'). Default: 30s."},start_period:{type:"string",description:"Start period for the container to initialize before starting health-retries countdown (e.g., '1s', '1m30s'). Default: 0s."},start_interval:{type:"string",description:"Time between running the check during the start period (e.g., '1s', '1m30s'). Default: interval value."}},additionalProperties:!1,patternProper