Compose Panel docs

ADR 0024 โ€” Controlled stack creation

Status: Accepted

Decision

Allow the authenticated administrator to create one named directory under the configured stacks root, containing raw compose.yaml and an optional raw .env. This is a narrow exception to the file-generation non-goal: no templates, catalogue, upload extraction, arbitrary path, or automatic Docker deployment. GET/POST /new-stack are HTML-first; POST requires Origin, CSRF and authorization. Drafts and errors stay authenticated, escaped and no-store.

Reserve the final directory with exclusive mkdir, mode 0700, under the shared stack lock. Validate private temporary files in that exact directory using the provider, explicit project-directory, Compose and interpolation files. Resolve the effective project name without forcing the folder name; refuse unavailable or colliding Docker discovery, and recheck it before commit. Up retains its own ownership preflight. File creation does not reserve a Docker project for future deployment or serialize with external CLI writers.

On Linux the new directory, Compose file and .env inherit the uid/gid of the stacks root, including when the panel runs as root. Keep directory mode 0700 and file mode 0600: .env stays private to that host administrator. A refused chown fails creation and triggers cleanup rather than leaving an inaccessible root-owned stack. The root's owner/group must identify the intended host admin; these modes do not grant access to other users or groups. Existing files are never chowned by creation.

Write a private .env before validation when supplied so relative env_file: .env resolves in the same context as deployment. Otherwise pass no --env-file, exactly like later operations without .env (resolveProjectContext): Compose then applies its default sources, including an inherited COMPOSE_ENV_FILES (ADR 0008). A substitute empty file would validate and collision-check a different project identity than the one used after publication. A .env appearing in the reserved directory when none was supplied is refused before publication, since Compose would load it by default. Publish compose.yaml last with an exclusive hard link to its completed draft (0600). This is atomic publication of the discoverable stack, not an atomic directory rename: the reserved empty directory exists during validation, but discovery sees no stack until Compose is published. An existing directory, file or symlink is never replaced, even if empty. Preserve submitted text byte-for-byte; no YAML parser or new dependency. Hard links are required for exclusive publication. A filesystem or permission policy that refuses them produces an explicit error and cleanup; there is no overwrite-capable rename fallback. Docker Desktop shares and some FUSE/NFS mounts may need a different stacks storage location.

Validation errors share the editor's diagnostic formatter: .env secrets are redacted before limiting lines/characters; timed-out or truncated output is never displayed. If cleanup also fails, the safe original application error is kept in the partial-outcome message. Server logs carry only the stack name and a fixed phase, never raw provider errors or diagnostics.

On Linux pin the root and new directory, anchor writes and cleanup through their open descriptors, and recheck path identity and draft hashes after awaited validation. Cleanup unlinks only files created by this request with matching inode identities and removes the directory only if still owned and empty. Never recursively delete a submitted path. A failed cleanup is reported as incomplete, with manual inspection needed, not as a successful rollback. These checks assume trusted privileged local writers; they do not provide a filesystem transaction against external CLI processes.

Creation joins the save drain and blocks self-update. Authorization and shutdown are rechecked before publishing. A crash can leave a reserved directory or partially published pair; no restart recovery or replay is promised. Remove or complete the files manually after inspection. Existing stacks stay untouched.

Verification

Tests cover auth/CSRF/Origin, invalid names, bounded bodies, duplicates, concurrency, unsafe paths, validation failures, unavailable/colliding projects, raw text and permissions, rollback, directory substitution, revoked sessions and shutdown. Linux tests use isolated filesystem/process fixtures; the opt-in real Compose config test uses no daemon mutations. Manual check: create a stack with a relative env_file referring to its supplied .env, verify raw files and no containers, then deploy separately with Up. Try invalid Compose and an existing name; verify the draft remains and the existing stack is intact. When testing a root panel, give the disposable stacks root to a non-root uid/gid, create a stack and confirm that owner can read it and run compose config without sudo. Test malformed YAML with a dummy .env secret echoed in the diagnostic: the syntax location stays visible and the secret is redacted.

Limitations

Creation checks deployed Docker projects only. Two undeployed directories can declare the same effective name:; their future Up still rechecks Docker project ownership and reserves the project during the action. Creation does not inspect every existing directory's config or reserve names for future deployment.