Compose
One app is one Compose file. The console lists that app once. On the cluster, each Compose service becomes its own pod and its own service. The file you use locally can stay as it is. The file you deploy is a separate production file.
What runs
Section titled “What runs”The platform reads services and ignores the rest of the file. A top-level name: is not read. It does not choose the namespace and it does not isolate one app from another. Every service in the file is deployed. Profiles are not read, so a service you only want sometimes still starts.
Each service needs an image. A build with no image is rejected. If a service has both, the image is what runs and the build is ignored. This platform does not build from the Compose file. Build the image in your repository and push it, then put that image name in the file.
A service name must be a lowercase DNS label of at most 40 characters: it starts with a letter, and the rest is letters, digits, or hyphens. site-help is valid. Site is not.
What the file may set
Section titled “What the file may set”| Field | What happens |
|---|---|
image | Required. This is the container that runs. |
environment | A map or a list of KEY=value. Values are copied literally. ${VAR} is not expanded. env_file is not read. |
command | A string, or a list joined into one string. It runs as sh -c, which replaces the image entrypoint. The image must contain a shell. Leave it empty to keep the image entrypoint. Do not set it on an official database image. |
ports | One number is the container port, for example "5432". That port is reachable by other services in this app and by a domain. "8080:8080" also publishes that port on the cluster node. Do not do that. Apps share the node, and a host port can be taken only once. |
volumes | name:/path becomes a 1Gi volume for that service. The same volume name on two services does not share data. /host:/path and ./path:/path become a directory on the cluster node, which does not have the files from your laptop. |
deploy.resources | limits and reservations for memory and cpus are applied. |
Anything else in the file is ignored, including depends_on, healthcheck, restart, networks, labels, and Traefik labels. Services start together. If one service has to wait for another, do that in the image entrypoint.
There is no Traefik service in the production file, and the file must not mount the Docker socket. The cluster already has an ingress. It routes the domains you set on the app. Pods of the same app reach each other by the Compose service name: postgres is the database of this app, server is this app’s server.
Images
Section titled “Images”A private image on registry.m12p.net needs the registry login on the app. The cluster pulls :latest again whenever a pod is created.
A public image must be written so the first part of the name contains a dot or a colon, or is localhost. postgres:16-alpine is left as written, because of the colon. minio/minio does not contain either, so it would be pulled from registry.m12p.net and fail. Write docker.io/... or another registry host.
Pushing a large layer through the public registry name can be rejected before it reaches the registry. A build that publishes an image should resolve registry.m12p.net to this host before docker login.
A deploy restarts every service whose image is :latest or has no tag, one after another, so each new pod pulls the image again. An image with any other tag is pulled only when the tag changes. Push a new tag for each release, or use :latest.
Removing a service from the file does not delete the workload that was already applied. Delete that leftover yourself if you still see it.
Domains
Section titled “Domains”Domains are not read from the Compose file. Set them on the app. Each domain names a hostname, a path, a port, and the service that receives the traffic.
https on that domain means the cluster ingress serves HTTPS. Leave it off. The cluster then serves plain HTTP, and this host terminates TLS for the public name. The address you open in a browser is https:// that hostname.
A wildcard covers one label. *.m12p.com and its certificate cover app.m12p.com. They do not cover site.app.m12p.com. A browser that opens the longer name fails the TLS handshake, often with ERR_QUIC_PROTOCOL_ERROR, even though the pods are running. Use one label: app.m12p.com, app-help.m12p.com.
A name under m12p.com with one label needs no extra DNS record. Each one belongs to a single app, and the names the platform uses itself are reserved. A name under the older m12p.net redirects to the same label under m12p.com. A name on another domain has to point at this host. A traefik.me name points at the cluster and is not served from this host. See Tenants, apps, and services.
The public ingress does not speak gRPC. Publish an HTTP port.
Isolation
Section titled “Isolation”Every Compose app has its own namespace. That namespace is not shared and cannot be turned off. The service name in your file is a DNS name inside that namespace only.
Two apps can both have a service named postgres. In the first app, postgres is that app’s database. In the second app, postgres is the second database. Both can run on the same cluster, and each applies its schema to its own database only.
The short name cannot select another app. The cluster network is k3s with its default flannel network, which does not enforce a network policy. A pod that is given the full DNS name of another app’s service can still open a connection. That name includes the other app’s namespace and ends in .svc.cluster.local. Your Compose file never creates it. Do not put another app’s namespace into your configuration.
Deploy again
Section titled “Deploy again”Build the images, push them to the registry, then deploy with the production Compose file from the repository:
platform service deploy <service id> --compose docker-compose.paas.yml --wait--compose replaces the file saved on the app, then deploys it. Without it, the deploy uses the saved file. The deploy runs in the background, and --wait follows it and fails when it fails. From GitHub Actions, sign in as a service principal without a secret. A CI system that cannot sign in can send the file to the deploy hook instead.
If a deploy fails, its summary starts with why. See Troubleshooting.
services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: platform POSTGRES_USER: platform POSTGRES_PASSWORD: platform ports: - "5432" volumes: - pgdata:/var/lib/postgresql/data server: image: registry.m12p.net/example-server:latest environment: DATABASE_URL: postgres://platform:platform@postgres:5432/platform?sslmode=disable ports: - "8080"
volumes: pgdata: