The stack.yml file defines the structure and configuration of a deployment stack. It specifies the containers, pods,
and any pre/post-start commands required for the deployment.
How these files relate to each other — which one is authoritative for what, and how stack validate
checks that they agree — is described in stack-integrity.md.
# The name of the stack.
name: gitea
# A brief description of the stack (optional).
description: "Gitea SCM and Actions"
# A list of containers to be used in the stack.
containers:
# The name of the container in the form `<organization>/<name>`. The pod's composefile.yml will need to use the
# same name for the image with the tag `stack`. For example: `image: bozemanpass/act-runner:stack`
- name: bozemanpass/act-runner
# An (optional) reference to the container's repository. The format is: [hostname/]organization/repo[@tag_or_branch]
# The hostname is optional. When omitted, github.com is assumed. The tag is also optional. If omitted,
# the main repo branch is used. If `ref` is omitted entirely, the current repo is assumed.
ref: bozemanpass/gitea-containers
# The relative path in the repo to the container build info. This directory must contain one (or more) of:
# - container.yml descriptor file (more info below)
# - build.sh build script
# The result of execution should be a local image tagged `<name>:stack`. The exact tag is available
# in the script build environment under ${STACK_DEFAULT_CONTAINER_IMAGE_TAG}.
# - Dockerfile
# The container will be built using the Dockerfile in this directory similar to:
# docker build -t ${STACK_DEFAULT_CONTAINER_IMAGE_TAG} .
# This directory is also what gets built, unless `content-root` says otherwise.
path: ./act-runner
- name: bozemanpass/gitea
ref: bozemanpass/gitea-containers
path: ./gitea
# An (optional) container wrapper scheme (see docs/wrappers.md). When specified, the container image is built by
# wrapping the contents of the repo at `ref` using the named wrapper, e.g. `static-content` serves the repo
# contents with nginx, `nextjs` builds and serves a Next.js webapp, and `node-service` builds and runs a
# long-running node service. The repo does not need to contain any container build files. Available
# wrappers can be listed with `stack webapp wrappers`.
- name: bozemanpass/my-static-site
ref: myorg/my-static-site
wrapper: static-content
# An (optional) reference to the wrapper's repository, same format as `ref`. When omitted,
# the wrapper is resolved from already-fetched repos (or the default wrapper repos).
wrapper-ref: bozemanpass/stack-wrapper-static-content@main
# An (optional) subdirectory of the source repo holding what is actually built: the content
# wrapped by `wrapper`, or the build context of the default build. Not wrapper-specific.
# When omitted it defaults to `path`. See "path vs content-root" below.
content-root: site
# Pods are groups of containers that are deployed together. Each pod corresponds to one composefile.yml.
pods:
# The name of the pod.
- name: gitea
# The relative path in this repo to the directory containing the pod composefile.yml and other files.
path: ./gitea
# An (optional) command to run just _before_ the pod starts. The command is executed on the host, and the location
# is relative to the `path` specified above. The deployment directory will be set in the environment under
# ${STACK_DEPLOYMENT_DIR}, allowing a script to execute commands _inside_ the service containers with:
# stack manage --dir ${STACK_DEPLOYMENT_DIR} exec <service> <command>
pre_start_command: "run-this-first.sh"
# Similar to pre_start_command, but executed _after_ the pod starts.
post_start_command: "initialize-gitea.sh"
- name: act-runner
path: ./act-runner
pre_start_command: "pre_start.sh"
post_start_command: "post_start.sh"
# (Optional) environment variables that are secret: their values are injected at deploy
# time -- generated, or pulled from a reference recorded in the spec -- and never appear
# in the stack files or the deployment artifacts. See secrets.md.
secrets:
# Delivered to every container of the deployment; generated by default.
GITEA_ADMIN_PASSWORD:
# `external: true` means a generated value would be useless (its counterpart lives
# outside the deployment), so a reference must be supplied at init time.
SMTP_PASSWORD:
external: trueA container entry answers two questions that are easy to confuse:
- Where is the recipe? —
path, the directory holding the container build info (container.yml,build.sh, or aDockerfile). A pod'spathmeans the same kind of thing: where its composefile lives. - What gets built? — the source repo, narrowed by
content-rootto a subdirectory of it. This is the docker build context: the content a wrapper wraps, or what the default build builds.
path |
content-root |
|
|---|---|---|
| Answers | where the recipe lives | what gets built |
| Relative to | the repo at the entry's ref |
the source repo (example 4 -- not always the same repo) |
| Names | container.yml / build.sh / Dockerfile |
the docker build context |
| When omitted | the repo root | path, except in example 4 |
| In the build | ${STACK_BUILD_DIR} |
${STACK_CONTENT_ROOT_DIR} |
They coincide for a repository that carries a Dockerfile beside the code it builds, which is
why path alone was enough for a long time. They come apart whenever the recipe lives
somewhere other than the source. The four shapes:
The common case: build info sits in the same directory as what it builds.
gitea-containers/ <- ref
└── act-runner/ <- path, and the build context
├── build.sh
└── Dockerfile
containers:
- name: bozemanpass/act-runner
ref: bozemanpass/gitea-containers
path: ./act-runnercontent-root is not needed: it defaults to path, so ./act-runner is what gets built — the
build context for the Dockerfile, and ${STACK_CONTENT_ROOT_DIR} for the build.sh.
The repo contains no build files at all — the recipe comes from the wrapper repo (see
wrappers.md), so there is nothing for path to point at.
my-static-site/ <- ref, and the build context
├── index.html
└── css/
containers:
- name: bozemanpass/my-static-site
ref: myorg/my-static-site
wrapper: static-contentThe same, but the repo holds more than the site: the content is one directory within it.
my-static-site/ <- ref
├── README.md
├── stack-files/ <- the stack.yml itself lives here
└── site/ <- content-root, and the build context
├── index.html
└── pages/
containers:
- name: bozemanpass/my-static-site
ref: myorg/my-static-site
wrapper: static-content
content-root: siteOnly ./site is sent to the container build, so README.md and stack-files/ are not baked
into the image.
A container.yml whose ref names a different repo builds source the entry's own repo doesn't
contain. Here the two fields refer to two different repositories:
gitea-containers/ <- the entry's ref
└── act-runner/ <- path (recipe only)
├── container.yml <- ref: gitea.com/gitea/act_runner
└── build.sh
act_runner/ <- the source repo, named by container.yml
└── src/ <- content-root, and the build context
# stack.yml
containers:
- name: bozemanpass/act-runner
ref: bozemanpass/gitea-containers
path: ./act-runner# gitea-containers/act-runner/container.yml
container:
name: bozemanpass/act-runner
ref: gitea.com/gitea/act_runner
content-root: srcThis is the one case where content-root does not default to path: ./act-runner
describes the layout of gitea-containers and says nothing about the layout of act_runner,
so leaving content-root out builds the source repo's root. Set it explicitly to build from a
subdirectory of the source.
Note:
content-rootmay be given in either file. Instack.ymlit applies to the entry; incontainer.ymlit travels with the repo that declares it, and takes precedence.
A build.sh is free to build whatever it likes, so content-root does not constrain it — it is
handed over instead, along with the other locations resolved for the container:
| Variable | Directory |
|---|---|
STACK_BUILD_DIR |
the recipe: where build.sh was found (the default build's context) |
STACK_CONTENT_ROOT_DIR |
the content root: the source repo narrowed by content-root |
STACK_REPO_SOURCE_DIR |
the source repo, unnarrowed |
STACK_REPO_CONTAINER_DIR |
the repo holding the container build info |
STACK_REPO_STACK_DIR |
the repo holding the stack.yml |
In example 1 STACK_BUILD_DIR and STACK_CONTENT_ROOT_DIR are the same directory. In example
4 every one of them is different.
The container.yml file defines the build configuration for individual containers within a stack. It specifies the container's name,
the repository reference, and the build script or command to be used for building the container image.
The build script path is relative to the container.yml file, not the target repo. In practice, this allows for build
scripts to be located in a separate repo from the container's source code, which is very useful for building customized
container images from repositories that are not under your control.
The following example from the bozemanpass/gitea-containers repo builds the bozemanpass/act-runner container directly from
the gitea.com/gitea/act_runner repo, for example.
container:
# The name of the container in the form `<organization>/<name>`. The pod's composefile.yml will need to use the
# same name for the image with the tag `stack`. For example: `image: bozemanpass/act-runner:stack`
name: bozemanpass/act-runner
# An optional reference to the container's repository. The format is: [hostname/]organization/repo[@tag_or_branch]
# The hostname is optional. When omitted, github.com is assumed. The tag is also optional. If omitted,
# the main repo branch is used. If `ref` is omitted entirely, the current repo is assumed.
ref: gitea.com/gitea/act_runner
# Optional path to the container build script or command. This path is relative to the `container.yml` file.
# If no build script is provided, the default build command will be used.
build: ./build.sh
# An optional subdirectory of the repo at `ref` to build, rather than the whole repo.
content-root: srcA repository can also declare that it is built by wrapping, rather than being named as a wrapper
in every stack that uses it (see wrappers.md). A container.yml at the root of
a static site whose pages live in ./site:
container:
name: bozemanpass/my-static-site
wrapper: static-content
# Optional: pin the wrapper repo, same format as `ref`.
wrapper-ref: bozemanpass/stack-wrapper-static-content@main
# Serve ./site, not the whole repo.
content-root: siteA stack then needs only the name and the repo:
containers:
- name: bozemanpass/my-static-site
ref: myorg/my-static-siteA container image is identified by the commit hash of its recipe repo — the repository hosting the container's
build declaration: the repo carrying its container.yml when there is one, otherwise the repo carrying the
stack.yml that declares it. When the recipe and the source it builds are the same repository (the common,
colocated case) that is simply the source repo's hash. When they are different repositories, lock files committed
in the recipe repo pin the other build inputs — the source ("payload") repo and the wrapper — so that a recipe
commit fully determines image content. An image built from unpinned or deviating inputs, or from a recipe checkout
with uncommitted changes (including a not-yet-committed lock file), gets a synthetic stackdev- tag instead, which
is never published or matched remotely: committing the lock file is what stabilizes the image identity.
Lock files exist to make builds reproducible. To move a pin to a newer version, delete the lock file (or the relevant entry) and rebuild to regenerate it.
When a stack is loaded from a filesystem path (rather than located by name under the repo base directory),
containers whose source is the stack's own repository build directly from that checkout — the repo is not
cloned again, and the image identity and the built content always come from the same tree. This is what a
CI job wants: the pipeline's checkout is the build source, with no second clone (which for a private
repository would need its own credentials). It also means local modifications in a developer checkout are
what gets built — with tracked changes producing a stackdev- tagged image, since the checkout no longer
matches any commit. Repositories referenced by ref that are not the stack's own are always cloned
beneath the repo base directory, at their pinned versions.
The container.lock file lives beside a container.yml whose ref names a different source repository, and pins
that source: hash records the source repo commit (and wrapper records the wrapper repo, when one is used). If
not already present, the file is automatically generated when the container is built, and when the source repository
is cloned by stack the pinned commit is checked out. Commit it to make the build repeatable.
Note: Even when
container.lockis present, any local code changes will be included when building the container, since the hash is used only when the repository is cloned or pulled.
The stack.lock file is generated next to stack.yml when a container declared there is built from inputs the
stack's repo does not itself contain: a source repository other than the stack's own, and/or a wrapper (see
wrappers.md). It has a containers section pinning each such container's source repo commit,
and a wrappers section pinning each wrapper repo commit (which also identifies the exact prebuilt base image to
pull). Commit it to make the stack's builds repeatable and its image tags stable.
Note:
stack.locksupersedes the earlierwrapper.lock; an existingwrapper.lockis still read (as thewrapperssection) when nostack.lockis present, and can be deleted once astack.lockhas been generated.
The stack.lock also carries an images section pinning each external image the stack's pod files name
(anything not tagged :stack, e.g. postgres:14) to its manifest digest. stack prepare records the pins and
stack deploy applies them to the deployment's copy of the pod files; an image annotated # @stack unpinned on
its image: line is left floating. See stack-integrity.md.
The composefile.yml file defines the structure and configuration of a pod. It specifies the containers, volumes,
environment variables, and any other settings required for the pod's deployment.
It is compatible in syntax with docker-compose.yml.
services:
runner:
image: bozemanpass/act-runner:stack
restart: always
privileged: true
environment:
- CONFIG_FILE=/config/act-runner-config.yml
- GITEA_INSTANCE_URL=http://gitea:3000
volumes:
- act-runner-data:/data
- act-runner-config:/config:ro
ports:
- 8088
volumes:
act-runner-data:
act-runner-config:A service can pick up the same variable from more than one place. Later sources in this list win:
- the deployment's
config.env(values supplied at deployment time) - each file listed in the service's
env_file:, in the order listed - the service's inline
environment:block
This is the order Docker Compose uses, and it is applied identically for Kubernetes deployments, so a pod file
hands its containers the same values whichever target it is deployed to. A pod file that wants a deployment-time
value to reach the container can forward it explicitly, e.g. environment: {SOME_VAR: "${SOME_VAR}"}.
In the sequence form, a bare - SOME_VAR does the same thing: it names a variable to take from the sources
listed above it, and passes nothing at all when none of them set it.
The consequence worth knowing is that an inline default beats a value the deployer supplied with --config: the
composefile wins, and the deployment is wrong rather than failed. So stack deploy reports it, naming the service
and the key and both values, whenever a key in the deployment's config.env is shadowed by an inline literal that
differs from it. A forwarding entry (above) is the fix and is not reported, nor is an inline literal that agrees
with the config value, nor a sequence entry with no value of its own (- SOME_VAR).
Every service in a deployment is reachable from every other service by its service name as it appears in the
composefile.yml, regardless of which pod it belongs to. In the example above, the runner service reaches the
gitea service (located in another pod in the same deployment) at the hostname gitea.
This works the same way whether the deployment target is Docker Compose or Kubernetes: on Kubernetes each deployment gets its own namespace, so the unqualified service name resolves within it.
Declaring ports: is not what makes a service addressable. A service with no ports: at all -- a database whose
port nothing outside the deployment should reach, say -- still answers to its own name on both targets; on Kubernetes
it gets a headless Service (no cluster IP, no ports) whose name resolves to the pod. So there is no need to publish
a port merely to obtain a hostname, which on the Compose target would also publish it on the host under
--map-ports-to-host.