Skip to content
NebulaCtrldocs

Compose mapping

How each Docker Compose file, service and key becomes a NebulaCtrl service, database or variable, what blocks an import, and the limits of one import.

The Compose import reads a repository's Compose files as text and proposes NebulaCtrl services, databases, variables and files. This page lists what each part of a Compose file becomes. To run an import, follow Import from Docker Compose.

Files

The import looks in the repository root and in each folder directly below it, or in the build context you set and its folders. A file two folders down is not found.

RuleValue
File namescompose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml, and variants such as compose.dev.yaml or docker-compose.prod.yml
Folders searchedFolders that start with ., node_modules and vendor are skipped, then the first 50 subfolders in alphabetical order are searched. If the repository cannot be listed, only the four standard names in the root are tried, with a note
Files listed20. Files in the root come first, then each subfolder alphabetically
Default selectionThe first standard-named file in the searched folder, which is the repository root or the build context you set, in the order compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml. Without one, the first file listed

Each file is read on its own and never merged with another, the way docker compose -f a.yaml -f b.yaml would merge them. A .env, env_file or build context in deploy/compose.yaml resolves against deploy/. Every service of every selected file becomes its own service. Two files that both declare web give web and web-2 when the first web is ticked. The name of a ticked service stays unique across the files and against the project.

The override file of a selected file, <name>.override.yaml or <name>.override.yml such as compose.override.yaml beside compose.yaml, is noted and not applied, because it usually holds development settings. Select it as a file of its own, or merge what you need into the base file.

The files are never run. include, and an extends that names another file, are refused because they would read outside the file you picked. An extends within the same file is read. A file that cannot be loaded has the status error, and an import that selects it fails until you untick it. That covers a file with include or a cross-file extends, invalid YAML, and a file over 256 KiB. A .env file and each env_file are read from the same repository at the same commit, at most 64 KiB each, and at most 16 distinct env_file paths per Compose file.

A service name becomes a slug: lowercase, runs of other characters replaced by -, at most 30 characters, and a leading digit prefixed with app-. A name that is already taken, and inputs, get a -2, -3 suffix.

Services

build

Becomes a service built from this repository, on the branch you picked, with the compose build context, Dockerfile and target (the Dockerfile stage).

Build arguments are not imported; a note tells you to put them in the [build.args] table of a nebula.toml beside the Dockerfile. A build that uses an inline Dockerfile, a remote or out-of-repository context, an absolute Dockerfile path, a Dockerfile outside the build context, or a target that does not match ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ blocks the service. additional_contexts, ssh and secrets under build produce a warning that the build may fail.

image

Becomes an image service. The reference is normalized to an explicit registry and tag, such as docker.io/library/nginx:1.27. An image without a tag produces a note that asks you to pin a version. A service with neither image nor build, or with an image that is not a valid reference, has nothing to run and blocks. An image beside a build is noted and not used.

Database images

Becomes a database service, where NebulaCtrl runs the engine, when the image is an exact Docker Hub match from this list. Images from other registries never match, and no match is by substring, so postgrest/postgrest and pgadmin stay images.

ImageResult
postgres, bitnami/postgresqlPostgreSQL
mysql, mysql/mysql-server, bitnami/mysqlMySQL
mongo, mongodb/mongodb-community-server, bitnami/mongodbMongoDB
valkey/valkey, bitnami/valkeyValkey
redis, bitnami/redisValkey, with a warning
postgis/postgis, pgvector/pgvector, ankane/pgvector, timescale/timescaledb, timescale/timescaledb-ha, supabase/postgresPostgreSQL offered, but the image is kept by default, because a database service has none of the extensions
mariadb, linuxserver/mariadb, bitnami/mariadbMySQL offered, but the image is kept by default
redis/redis-stack, redis/redis-stack-serverValkey offered, but the image is kept by default

bitnamilegacy and bitnamisecure match like bitnami. A Redis tag of 8 or higher is kept as an image. You can switch a kept image to a database service, or a database service back to the image, in the review.

A database service runs the tag's major version when NebulaCtrl runs it, and otherwise the nearest newer major. An image with no tag, or a tag with no readable version such as latest, gets the newest. A tag newer than every major NebulaCtrl runs keeps the image. NebulaCtrl runs PostgreSQL 18 and 16, Valkey 9, MySQL 8 and MongoDB 7. A Redis image can also map to the Valkey 7 template, which still runs the Redis 7 image. Tags such as 16, 16.4, 16-alpine, v7, pg16 and latest-pg16 are read. A Redis image produces the warning that Valkey is compatible with Redis 7.2 and earlier. MariaDB becomes the newest MySQL whatever its tag.

A database service generates its own credentials. The container's password, data volume, command, entrypoint, settings and /docker-entrypoint-initdb.d scripts are dropped, each with a note. Data in a local Compose volume is not migrated: a database service starts empty. A database that is kept as an image runs as user 999 when the service sets no usable user, unless it is a Bitnami, extension or Redis Stack image, or its tag contains alpine.

ports and expose

Becomes one process. Published TCP ports are collected first, then expose ports. The first one collected is the process's port, and a note names the others.

The process is an http process named web when that first port is published on a non-loopback address and is not a port of a well-known non-HTTP protocol (21, 22, 25, 53, 110, 143, 389, 445, 465, 514, 587, 636, 993, 995, 1433, 1521, 1883, 2181, 3306, 3389, 4222, 5432, 5672, 5900, 6379, 6432, 8883, 9042, 9092, 11211, 27017). Otherwise it is a worker named worker, which keeps the port when there is one. A published port range contributes each of its ports, and an expose range contributes only its first port. A non-TCP port is noted and not reachable in the cluster.

command and entrypoint

Becomes the process's arguments and command. Compose entrypoint becomes the process's command, and Compose command becomes its arguments.

user

Becomes the process's user ID when numeric. uid:gid keeps the uid, and root becomes 0. A user name or a group-only value is noted and sets nothing.

deploy, scale, cpus, mem_limit, mem_reservation

Becomes the process's replicas, CPU and memory. Replicas come from scale, else deploy.replicas. CPU and memory limits come from deploy.resources.limits, else cpus and mem_limit. Requests come from deploy.resources.reservations, else mem_reservation, and a request is capped at its limit. A service scaled to zero is proposed but not ticked.

volumes

Becomes a NebulaCtrl volume of 5Gi for each named or anonymous volume, at most 5 per process. An external volume, or one with a driver or driver options, becomes a new empty volume, with a warning. Data in a Compose volume is not migrated. A named volume that several services share needs a decision. A relative bind mount, tmpfs and an absolute host path outside the system paths are imported without effect, with a note.

environment and env_file

Becomes the service's variables, at most 100 per service. A value that contains ${{ is not imported. A variable name that does not match ^[A-Z_][A-Z0-9_]*$ needs a decision. How a value is classified is in Values.

secrets

Becomes a sealed variable and a read-only file. The variable is named after the secret in upper snake case: characters other than A-Z0-9_ become _, a leading digit gets a _ prefix, and a clash gets _2. The file is at the secret's absolute target, or at /run/secrets/<target or name>. The file holds the variable's value, is mounted in every container of the release including the release command, and belongs to the service's files setting, which you can edit later under Release commands and files.

A *_FILE variable keeps its path value and is never secret. A secret that several services use is one variable: the first included service holds it, and every other service reads ${{ SERVICE.KEY }}.

A service blocks when its secret files would sit in a place NebulaCtrl cannot put a file: under /proc, /sys or /dev, directly in /, /etc, /usr, /bin, /sbin, /lib, /var or /tmp, or inside a volume. A service has at most 20 secret files. A cluster whose agent cannot mount files refuses the first deploy and says to update the agent.

depends_on

Becomes the dependent's Deploy after list for every condition except service_completed_successfully. A service_started, service_healthy or short-form entry all mean the dependent deploys after that service is healthy. An entry resolves to the imported service of that name, or to the database service that replaces a Compose database. An entry that names a service left out of the import is dropped with a note. A managed database gets no list, and a cycle is refused. The first deploy starts databases first and then the services along their dependencies. See Deploy order.

service_completed_successfully is not an ordering; see the next entry.

One-shot services

Becomes the release command of the service that waits for it, when the one-shot is not imported as a service of its own.

A service is a one-shot when it has no ports and either its restart is no or on-failure, or another service depends on it with condition: service_completed_successfully. It is folded into every included, non-database service that waits for it with service_completed_successfully, when the one-shot itself is not ticked, and when it runs the same image as the dependent: the same build context, Dockerfile and target, or the same image reference. Its command becomes the release command, and its variables and secrets are merged into the dependent's. It runs before each release of each such service, so a command that runs more than once must be safe. A one-shot that has ports stays a service, and a one-shot that is a database image is not folded.

A one-shot that is not folded is proposed but not ticked. A decision blocks the import when folding would lose something: see Decisions.

profiles

A service behind a Compose profile is proposed and not ticked.

Decisions

A decision is something only you can settle. The import is refused until each decision on a ticked service is accepted. The review shows each decision on its service. Over the API, the ids go in choices.services.<key>.accept. A release- id contains the one-shot's name in the Compose file, and it is accepted on the service that waits for the one-shot.

IdRaised whenAccepting means
variable-namesNames do not match ^[A-Z_][A-Z0-9_]*$, such as CLAMD_CONF_StreamMaxLengthThe names are left out. Set them another way first, for example a config file in the image
volume:<name>Several ticked services mount one named volumeAccept it on one service, and only one. The others get none
release-image:<one-shot>The one-shot runs another imageThe service is imported without that release command
release-command:<one-shot>The one-shot has no command, or one over 8 KiBGive a release command in choices.services.<key>.releaseCommand to settle it. Accepting without one imports the service with no release command
release-other:<one-shot>The one-shot cannot be importedThe service is imported without waiting for it
release-other:<one-shot>:entrypointThe one-shot sets an entrypoint that differs from the service'sThe command is kept as the release command and runs after the service's own entrypoint
release-env:<one-shot>The one-shot sets a variable to another value than the dependentThe dependent keeps its own value
release-files:<one-shot>The one-shot's secret files clash with the dependent'sThe service is imported without the one-shot's files
release-more:<one-shot>A second one-shot waits for the same serviceThe second one is not folded in

An image service whose image starts as root, or runs as a named user that Kubernetes cannot verify as non-root, needs a user decision instead. It is not in decisions: the proposal reports it as userDecision, either root or named, and the import is refused until it is answered. Give a user ID in choices.services.<key>.runAsUser, or 0 to allow root. An image that starts as root under an init system (init, tini, dumb-init, s6-svscan, s6-overlay-suexec) is reported with imageInit: true, and the review offers only root for it, because forcing a user ID breaks its setup. A user: in the Compose file already answers it: a numeric ID or root is applied, and any other name is reported in a note and not applied. NebulaCtrl reads at most 20 images, and an image it cannot read is checked again when the import is staged.

What cannot run

A service that uses one of these is blocked. It is listed with the reason and cannot be ticked, because NebulaCtrl drops every capability and runs pods on the cluster network.

  • privileged, cap_add, devices, device_cgroup_rules, gpus, use_api_socket, provider.
  • network_mode of host, service: or container:, and pid, ipc, uts, userns_mode or cgroup of host, service: or container:.
  • A security_opt that contains unconfined or disable.
  • A host path under a system path (/, /proc, /sys, /dev, /etc, /boot, /root, /run, /var/run, /var/lib/docker, /var/lib/kubelet, /var/lib/containerd, /var/log, /usr, /lib, /lib64, /bin, /sbin), which includes the Docker socket.
  • A relative bind mount outside the repository.
  • A ${NAME} without a value in image, the context, dockerfile or target of build, ports, expose, volumes, tmpfs, command, entrypoint, user, container_name, hostname, env_file, deploy, scale, profiles, restart or working_dir.

Imported without effect

These are imported without their effect, with a note that says what to do instead.

Compose keyNote
healthcheckNebulaCtrl uses its own health checks
networksEvery service of an environment can reach each other
configsNot mounted; move the content into variables
tmpfsThe path is on the container's disk and is cleared on restart
traefik.* and caddy* labelsIgnored; add a domain in NebulaCtrl
Relative bind mountsFiles from the repository are not mounted; use a volume for data and an image or build for code
Absolute host paths outside the system pathsDropped; use a volume
working_dir, sysctls, ulimits, volumes_fromNot applied

A restart policy produces no note, apart from one-shot handling.

Values

Every variable of every service is in one group, which the review shows and the API reports as origin.

GrouporiginMeaning
You need to provideempty with needsInput, or userA secret, or a value the Compose file marks required with ${NAME:?message}, that nothing supplied. The import does not stage until you type a value or accept it as empty with I'll set it later. A value you typed has origin: user and stays in this group. The comment above the variable in its env_file, the :? message or the secret's name is the hint
Generated for yougeneratedA secret the app owns and the file gives no real value for: none, empty or a placeholder. It becomes ${{ secret(32) }}, drawn when the variable is written and sealed. Turn Generate a random value off to move a sealed secret to You need to provide. Another generated name stays empty under From your compose file
From your compose filecomposeA literal value the file gives. It is sealed when its name suggests a secret. A secret that is committed in the file is already in the repository's history, and the review says to rotate it
Linkedreference or databaseA database URL, credential or host rewritten to a reference, shown as KEY → service

A variable with no value, written without = or as an unset ${NAME}, that is neither secret nor required has origin: empty and needsInput: false. The review lists it under From your compose file, and the import does not wait for it. A variable written as an empty string keeps origin: compose.

What is generated. A name is generated only when the app can own the value: a whole word SECRET, SECRETS, PASSWORD, PASSWORDS, PASSWD, PASSPHRASE, PASS, PWD or SALT, or KEY beside a master, encryption, signing, session, cookie, JWT, app, auth, nonce or HMAC word. A name that someone else issues is never generated: client secrets, OIDC and OAuth, webhook secrets, API keys, tokens, SMTP, LDAP, Stripe, S3, AWS and access keys. A Compose secrets: entry is generated only when its name is clearly the app's own: a master, encryption, signing, session, cookie, JWT or HMAC word beside key or secret, secret beside key, a salt, or the password of a database the import creates. A ${NAME:?message} is never generated. A variable that needs input is generated only when you ask for it, because a generated value would be one that an identity provider, a bank or a mail server has never heard of.

A placeholder counts as no value, whatever its case: changeme, change-me, change_me, replace-me, replace_me, replaceme, todo, tbd, ..., …, a value in angle brackets, a value that starts with your-, your_ or your , and three or more x with only -, _, . or spaces between them.

A name is sealed as a secret when it contains SECRET, PASSWORD, PASSWD, PASSPHRASE, PASS, TOKEN, KEY, DSN or CREDENTIAL, or the plural of one, as a whole word between underscores. A name ending in _FILE is not sealed, and a URL value with a password in it is. A reference is secret only when the key it names is: POSTGRES_DB of a database service is not.

Rewrites

A rewritten value replaces the Compose value and shows in the review next to the value it replaced.

  • A database URL whose host is an alias of a managed database service, with the scheme of that engine, becomes the database's connection reference: ${{ db.DATABASE_URL }} for PostgreSQL, ${{ db.REDIS_URL }} for Valkey, ${{ db.MYSQL_URL }} for MySQL and ${{ db.MONGODB_URL }} for MongoDB. A URL with a query or a fragment, or a Valkey database index other than 0, keeps its shape, with the user, password, host, port and database name replaced by references.
  • A service name used as an address, such as after ://, @, host=, server= or addr=, before :port, or at the start of a value without spaces, or after a comma in one, whose key has the word HOST, HOSTS, HOSTNAME, ADDR, ADDRESS, URL, URI, ENDPOINT, SERVER, DSN, BROKER or CONNECTION, becomes ${{ host(NAME) }}. NebulaCtrl matches the service name, container_name, hostname and network aliases. A name that points at a service you did not import is left alone, with a note. A name that two services share is left alone.
  • A credential that equals one a database container declared becomes the matching variable of the database service, when the variable's name says which credential it is (user, password or database), or when both read the same ${NAME}.
  • A secret file that a replaced database container read, such as the file behind POSTGRES_PASSWORD_FILE, becomes a reference to the database's password key in every service that mounts it. Its user secret and database name secret likewise become references to the user and database keys.
  • A shared ${NAME} that has no value and that NebulaCtrl would generate becomes one variable on the first service that uses it, and a reference to it from the others. A ${NAME} that has a value is copied to each service.

Interpolation

A ${NAME} that shapes the file itself, such as an image tag or a port, must be known before the import. The proposal lists each one and asks for a value. The value comes from, in this order: what you enter, the .env beside the file, the file's own default. A ${NAME} in a variable's value that has none follows the rules in Values.

Not ticked by default

A service is proposed and not ticked when it has a Compose profiles entry, when it is a one-shot, when it is scaled to zero replicas, or when ticking it would pass the limit of 20 services in the import. A blocked service is never ticked.

Limits

LimitValue
Services in one import20, counted across all selected files
Compose files listed20
Subfolders searched50 immediate subfolders of the searched folder
Compose file size256 KiB
Services in one Compose file200
Published and exposed ports in one file256 after ranges expand
Variables per service100
Secret files per service20
Volumes per process5, of 5Gi each
Release command8 KiB

Ticking more than 20 services is refused with N services are ticked, but one import holds at most 20; untick M. A service name is at most 30 characters.

Choices over the API

The preview and the import take the same choices object. Services are keyed <file>#<composeName>, such as compose.yaml#web.

ChoiceMeaning
choices.filesPaths of the files to import. Left out selects the default, and an empty list selects nothing
choices.valuesValues for the files' ${NAME} interpolations, by name, for every selected file
choices.generatePer ${NAME}, false leaves an offered value empty
choices.acceptEmpty<serviceKey>#KEY entries that stay empty
choices.services.<key>.includeTick or untick the service
choices.services.<key>.nameThe service's name
choices.services.<key>.databasemanaged or image, for a database image
choices.services.<key>.runAsUserThe user ID of an image service, or 0 to allow root
choices.services.<key>.valuesValues for the service's variables that need input. Sealed on import, never echoed or logged
choices.services.<key>.generatePer variable, false leaves an offered variable empty and true generates a secret that needs input
choices.services.<key>.acceptDecision ids accepted
choices.services.<key>.releaseCommandThe release command, which settles a release-command decision

On this page