This is an experiment that uses docker-compose
within a container to self-deploy
a stack of containers, along with watchtower
to auto-update that stack.
The central conceit is that by using watchtower
we can run the container built from this repo once on every host and
it, along with service containers, will be automatically updated. When this container is updated it will re-run
docker-compose up
, which will start new services and remove old services as needed.
To use this thing, create a new GitHub repo and add a Dockerfile that looks like this:
FROM ghcr.io/peterkeen/docker-compose-stack:main
COPY . /app
Now create a structure that looks like this:
hosts.yml
stacks/
configs/
scripts/
The idea of stacks is that we can use one repo to deploy to multiple hosts.
First we create a docker-compose file within stacks/
for every different stack of containers we want to deploy. For example, here's stacks/echo.yml
:
services:
http-echo:
image: hashicorp/http-echo
ports:
- "5678:5678"
command: "-text=foobarbaz"
Then we set a list of stacks
in hosts.yml
for every host like this:
hosts:
martin:
stacks: ["echo"]
Every stack listed for a host is added as a -f
argument to the docker-compose
invocation, which merges every file together using its ordinary merge logic.
Stacks are similar in concept to Compose's built-in profiles
concept with the important difference that if you remove a stack from a host those containers will be stopped and destroyed on next run.
$ mkdir /var/lib/docker/stack_configs
$ docker run --init --restart=unless-stopped -d -v /var/lib/docker/stack_configs:/configs -v /var/run/docker.sock:/var/run/docker.sock -v /etc/hostname:/app/hostname:ro -e CONFIGS_DIR=/var/lib/docker/stack_configs --name dockerstack-root -l dockerstack-root ghcr.io/your-name/your-repo:main
Configs live in configs/
in named directories. Hosts can define what configs they want, and then those directories are copied to the host and made available for stacks to bind mount. The bind mounting does not happen automatically, you still have to specify the bind mounts you want to happen.
Each host can optionally define a set of environment variables in the environment
key as an array of strings. The contents of this array is written to a .env
file before invoking docker-compose
.
You can optionally create a file on the host at /var/lib/docker/stack_configs/secrets
. This file is sourced in start.sh
so can contain any valid ash
statements. Secrets defined here are available for services, pre-start scripts, and crons. Generally you'd put a bunch of export
s in, like this:
export SOME_SECRET=this-is-secret
export SOME_OTHER_SECRET=this-is-also-secret
A host can optionally define a pre-start
key consisting of an array of script names to run. Each script must be executable and live in scripts/
. Example:
in scripts/testing-prestart.sh
:
#!/bin/sh
echo "this is a prestart script"
in hosts.yml
:
hosts:
somehost:
pre-start: ["testing-prestart.sh"]
After executing docker-compose up
start.sh
will then exec itself to crond
. Cron jobs are services defined in a stack with the cron
profile:
in stacks/test.yml
services:
testcron:
image: alpine
profiles:
- cron
Setting a profile means that this service will not automatically be started by docker-compose
but it is available to be run. To define the cron schedule, set a host-level crons
key like this:
hosts:
somehost:
stacks: ["test"]
crons:
- schedule: "* * * * *"
service: "testcron"