Skip to main content

From docker compose

Available

Deployed and supported. The fields and commands described here are stable within version v1alpha1.

Automatic conversion of a compose file is not yet offered on the command line: this guide gives the mapping to apply by hand. It follows exactly the rules of the platform's converter.

An App manifest reads like a docker-compose.yml: named services, an image, ports, variables, volumes, dependencies. This guide gives you the field-by-field mapping to go from one to the other, and lists what does not translate.

Prerequisites​

  • An open session and a chosen tenant: sng sso login, sng tenant use.
  • Your images built and pushed to a registry reachable from the platform (build: is not translated).
  • For a private image: a docker-registry Secret on your tenant (Variables and secrets).

Steps​

1. Start from the template​

sng template gen App shop
créé /home/vous/shop.yml (kind=App)

The file contains a commented-out web service; replace it with your services.

2. Translate service by service​

Take this docker-compose.yml:

docker-compose.yml
name: shop
services:
api:
image: registry.gitlab.com/stackngo/shop-api:1.4.2
command: shop serve --port 8080
ports: ["8080"]
environment:
LOG_LEVEL: info
WORKERS: 4
env_file: [.env]
depends_on:
migrate: { condition: service_completed_successfully }
db: { condition: service_healthy }
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 15s
retries: 5
deploy:
replicas: 2
resources:
reservations: { cpus: "0.25", memory: 256M }
update_config: { order: start-first }
migrate:
image: registry.gitlab.com/stackngo/shop-api:1.4.2
command: ["shop", "migrate"]
depends_on: [db]
db:
image: postgres:17
user: "999"
expose: ["5432"]
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
volumes:
pgdata:

Its translation:

shop.yml
apiVersion: deploy.stackngo.io/v1alpha1
kind: App
metadata:
name: shop # name of the compose project
spec:
imagePullSecrets: [gitlab-registry] # private registry: Secret to create on the tenant
services:
api:
image: registry.gitlab.com/stackngo/shop-api:1.4.2
command: ["shop", "serve", "--port", "8080"]
ports: ["8080"]
env:
LOG_LEVEL: info
WORKERS: 4
envFrom: [api-env] # env_file → Secret api-env, to create
dependsOn: [db, migrate] # migrate awaited as "completed" → migrate becomes a job
healthcheck:
cmd: ["wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 15s
retries: 5
replicas: 2
size: xs # 0.25 CPU / 256M → smallest size that covers it
strategy: { type: rolling, maxSurge: 1, maxUnavailable: 0 } # order: start-first
ingress: { port: 8080 } # compose has no equivalent: to add
migrate:
kind: job # awaited as service_completed_successfully
image: registry.gitlab.com/stackngo/shop-api:1.4.2
command: ["shop", "migrate"]
dependsOn: [db]
db:
image: postgres:17
user: "999"
ports: ["5432"] # expose → ports (reachable at db:5432 by the other services)
env:
POSTGRES_PASSWORD: { secret: db, key: password } # never the value in clear text
healthcheck: { cmd: ["sh", "-c", "pg_isready -U postgres"] }
volumes:
- { path: /var/lib/postgresql/data, size: 10Gi } # named volume → persistent volume, size to choose

3. Create the expected Secrets​

The manifest references gitlab-registry, api-env and db. Create them before or right after the apply: see Injection into an App. As long as an environment Secret is missing, the service stays in Progressing with the reason SecretNotFound; a missing registry Secret blocks the service in Waiting (ImagePullSecretNotFound).

4. Apply and follow​

sng apply shop.yml
sng app describe shop
App : shop
Statut : en cours
Message : api : attend : migrate
SERVICE KIND IMAGE PHASE URL DETAIL
api deployment registry.gitlab.com/stackngo/shop-api:1.4.2 Waiting - WaitingForDependency attend : migrate
db deployment postgres:17 Ready - Reconciled 1/1 prêts
migrate job registry.gitlab.com/stackngo/shop-api:1.4.2 Progressing - JobRunning job en cours

Verification​

sng app describe shop displays Statut : prête and, for api, the URL https://api-shop.<tenant>.dev.stackngo.app/. curl -I on this URL responds HTTP/2 200.

Field-by-field mapping​

composeAppNote
name (project)metadata.namelowercase, _ and . become -, 24 characters at most
service namekey of spec.servicessame normalization; cache_store → cache-store
imageimagerequired
build—ignored: build and push the image, then set image
commandcommandlist form; a compose string is split as the shell would
entrypointentrypoint
user: "999", "1000:1000"useronly a numeric non-root uid; user: redis is ignored
environment: K: venv: K: vvalue copied; ${VAR} is not interpolated (replace the value)
environment: K: (no value)—ignored: the value came from your machine
env_file: [.env]envFrom: [<service>-env]the file is not copied: create the Secret <service>-env with its content
value that looks like a secret (PASSWORD, TOKEN, KEY…)env: K: { secret: s, key: k }put it in a Secret
ports: "8080", "80:8080", "53:53/udp"portspublished:target; host_ip ignored; a range (8000-8010) is rejected
expose: ["5432"]ports: ["5432"]same thing: a declared port is reachable by the other services of the App
volumes: name:/pathvolumes: [{ path: /path, size: 1Gi }]persistent volume; size to adjust (1Gi by default); forces a single replica
volumes: ./file:/path (bind), tmpfs—ignored: only named volumes become volumes
depends_on: [db]dependsOn: [db]the service_healthy condition is implicit (the service must be ready)
depends_on: x: { condition: service_completed_successfully }dependsOn: [x] and x becomes kind: job
healthcheck.test: ["CMD", …]healthcheck.cmd: […]
healthcheck.test: ["CMD-SHELL", "…"]healthcheck.cmd: ["sh", "-c", "…"]
interval, timeout, start_period, retriesinterval, timeout, startPeriod, retriesretries bounded to 1–30
healthcheck: disable: true or test: ["NONE"]—no probe
pull_policy: always / missing, if_not_present / neverpullPolicy: always / missing / never
deploy.replicas: nreplicas: n0–100; reduced to 1 if the service has a volume
deploy.resources.reservations (or limits otherwise)sizesmallest size whose reservations cover CPU and memory; beyond xxxl, xxxl
deploy.update_config: order: start-firststrategy: { type: rolling, maxSurge: <parallelism or 1>, maxUnavailable: 0 }no strategy if the service has a volume (it is in recreate)
deploy.update_config: order: stop-first (or absent)strategy: { type: rolling, maxSurge: 0, maxUnavailable: <parallelism or 1> }
profiles—choose the services to keep
—ingressno compose equivalent: to add on the web service
—egress: [internet]compose allows outbound Internet; here it is closed by default
—sizedefault xxs if you specify nothing

What is not supported​

These compose keys have no equivalent and are discarded, most often because they contradict security by default (unprivileged container, closed network):

  • privileged, cap_add, network_mode, pid, devices, tmpfs;
  • secrets mounted as files and the secrets: section: use env: { secret, key } or envFrom;
  • configs and the configs: section;
  • build, bind mounts, port ranges, host_ip;
  • restart, container_name, networks, labels, logging, extra_hosts: not carried over (restart is always automatic, the service name serves as the DNS name, the network is the App's).
Two behavior changes to anticipate

Non-root. Your containers run without privileges. An image that starts as root (nginx, postgres without user) is rejected: use a non-root variant (nginxinc/nginx-unprivileged) or set user: "<uid>".

Closed network. No outbound Internet without egress: [internet] on the service concerned. Between services of the same App, only the declared ports are reachable.

Going further​

  • A compose service named web in the App shop is reachable by the other services at web:<port>, as in compose: what counts is the service name, not the App name.
  • Sizes and how they are computed from deploy.resources: Sizes and resources.
  • The rules that can reject the manifest: App page of the manifest reference.

See also​