@@ -235,7 +235,7 @@ func (d *Discovery) loadMetadata(path string) (*models.ServiceMetadata, error) {
235235 return & metadata , nil
236236}
237237
238- func (d * Discovery ) CreateDeployment (name string , composeContent string ) error {
238+ func (d * Discovery ) CreateDeployment (name string , composeContent string , fileMounts [] string ) error {
239239 dirPath := filepath .Join (d .basePath , name )
240240
241241 if err := os .MkdirAll (dirPath , 0755 ); err != nil {
@@ -246,7 +246,7 @@ func (d *Discovery) CreateDeployment(name string, composeContent string) error {
246246 composeContent = d .ensureComposeName (name , composeContent )
247247
248248 // Pre-create bind mount directories with permissive access for non-root containers
249- if err := d .createBindMountDirs (dirPath , composeContent ); err != nil {
249+ if err := d .createBindMountDirs (dirPath , composeContent , fileMounts ); err != nil {
250250 return fmt .Errorf ("failed to create mount directories: %w" , err )
251251 }
252252
@@ -255,8 +255,11 @@ func (d *Discovery) CreateDeployment(name string, composeContent string) error {
255255}
256256
257257// createBindMountDirs parses compose content and creates bind mount directories
258- // with world-writable permissions to support non-root containers (e.g., Bitnami)
259- func (d * Discovery ) createBindMountDirs (deploymentPath , composeContent string ) error {
258+ // with world-writable permissions to support non-root containers (e.g., Bitnami).
259+ // fileMounts lists relative paths (e.g., "./nginx.conf") that are file mounts
260+ // from template metadata. For paths not in fileMounts, a basename-contains-dot
261+ // heuristic is used as a fallback to avoid creating files as directories.
262+ func (d * Discovery ) createBindMountDirs (deploymentPath , composeContent string , fileMounts []string ) error {
260263 var compose struct {
261264 Services map [string ]struct {
262265 Volumes []string `yaml:"volumes"`
@@ -267,19 +270,41 @@ func (d *Discovery) createBindMountDirs(deploymentPath, composeContent string) e
267270 return nil // Skip if parse fails, not critical
268271 }
269272
273+ fileMountSet := make (map [string ]bool , len (fileMounts ))
274+ for _ , fm := range fileMounts {
275+ cleanPath := filepath .Clean (fm )
276+ fileMountSet [cleanPath ] = true
277+ fileMountSet ["./" + cleanPath ] = true
278+ }
279+
270280 for _ , service := range compose .Services {
271281 for _ , volume := range service .Volumes {
272282 hostPath := extractBindMountPath (volume )
273283 if hostPath == "" {
274284 continue
275285 }
276286
277- // Only handle relative paths (bind mounts)
278287 if ! strings .HasPrefix (hostPath , "./" ) && ! strings .HasPrefix (hostPath , "../" ) {
279288 continue
280289 }
281290
282291 fullPath := filepath .Join (deploymentPath , hostPath )
292+
293+ if _ , err := os .Stat (fullPath ); err == nil {
294+ continue
295+ }
296+
297+ if isFileMount (hostPath , fileMountSet ) {
298+ parentDir := filepath .Dir (fullPath )
299+ if err := os .MkdirAll (parentDir , 0777 ); err != nil {
300+ return err
301+ }
302+ if err := os .Chmod (parentDir , 0777 ); err != nil {
303+ return err
304+ }
305+ continue
306+ }
307+
283308 if err := os .MkdirAll (fullPath , 0777 ); err != nil {
284309 return err
285310 }
@@ -292,6 +317,28 @@ func (d *Discovery) createBindMountDirs(deploymentPath, composeContent string) e
292317 return nil
293318}
294319
320+ // isFileMount checks whether a bind mount host path refers to a file.
321+ // It first checks the metadata-provided fileMounts set, then falls back
322+ // to a heuristic: if the basename contains a dot and is not a known
323+ // directory pattern (e.g., conf.d), it's treated as a file.
324+ func isFileMount (hostPath string , fileMountSet map [string ]bool ) bool {
325+ if fileMountSet [hostPath ] {
326+ return true
327+ }
328+
329+ base := filepath .Base (hostPath )
330+ if ! strings .Contains (base , "." ) {
331+ return false
332+ }
333+
334+ // Known directory suffixes like .d (conf.d, certs.d, etc.)
335+ if strings .HasSuffix (base , ".d" ) {
336+ return false
337+ }
338+
339+ return true
340+ }
341+
295342// extractBindMountPath extracts the host path from a volume mount string
296343// Handles formats: "./path:/container/path" or "./path:/container/path:ro"
297344func extractBindMountPath (volume string ) string {
0 commit comments