Deploying from CI
Deploy from GitHub Actions, GitLab CI, Jenkins, Buildkite or Bitbucket Pipelines, with the environment and version taken from the Git tag and secrets from CI variables.
The same dockflow binary runs on your laptop and on a CI runner. A typical setup builds the images on every branch and deploys when you push a tag.
In CI, Dockflow recognises the provider and reads the tag, branch and commit of the run. It takes its secrets from CI variables.
# .github/workflows/deploy.yml (the deploy job)
- run: curl -fsSL https://raw.githubusercontent.com/Shawiizz/dockflow/main/install.sh | sh -s -- --version v1.0.0-alpha.6
- run: dockflow deploy --wait-lock 10m
env:
DOCKFLOW_SECRETS: ${{ toJSON(secrets) }}Pushing the tag 1.2.0 deploys version 1.2.0 to production. Pushing 1.2.0-staging deploys version 1.2.0 to staging. Complete workflows for each provider are at the end of this page.
What Dockflow reads from the CI provider
Dockflow checks the environment variables of the run in this order, and uses the first provider that matches:
| Provider | Detected by | Tag | Branch | Commit | Actor |
|---|---|---|---|---|---|
| GitHub Actions | GITHUB_ACTIONS=true | GITHUB_REF_NAME when GITHUB_REF_TYPE is tag | GITHUB_HEAD_REF, else GITHUB_REF_NAME | GITHUB_SHA | GITHUB_ACTOR |
| GitLab CI | GITLAB_CI=true | CI_COMMIT_TAG | CI_COMMIT_BRANCH | CI_COMMIT_SHA | GITLAB_USER_LOGIN |
| Buildkite | BUILDKITE=true | BUILDKITE_TAG | BUILDKITE_BRANCH, unless it equals the tag | BUILDKITE_COMMIT | BUILDKITE_BUILD_CREATOR, else BUILDKITE_BUILD_AUTHOR |
| Bitbucket Pipelines | BITBUCKET_BUILD_NUMBER is set | BITBUCKET_TAG | BITBUCKET_BRANCH | BITBUCKET_COMMIT | BITBUCKET_STEP_TRIGGERER_UUID |
| Jenkins | JENKINS_URL is set | TAG_NAME | BRANCH_NAME, else GIT_BRANCH | GIT_COMMIT | BUILD_USER_ID |
| Any other | CI is set | – | – | – | – |
What each value is used for:
- The tag selects the environment and gives the release its version label. See below.
- The branch and the commit fill
DOCKFLOW_GIT_BRANCHandDOCKFLOW_GIT_SHAwhen the checkout cannot tell (CI checkouts are often detached or shallow). Whengitanswers, its values win. Without a tag, the version label is the short commit. - The actor and the provider are recorded as the release's author, which
dockflow releasesshows. They also name the holder of the environment's lock while the run lasts. On Jenkins,BUILD_USER_IDcomes from the Build User Vars plugin; without it, the author is the runner's system user.
Without a terminal, Dockflow never prompts, so deploy applies its plan without --yes.
Tags select the environment and the version
In a run started by a tag, dockflow deploy without an environment splits the tag at its last hyphen:
| Tag | Version label | Environment |
|---|---|---|
1.2.0 | 1.2.0 | The environment whose tag suffix is empty: production by default |
v1.2.0 | v1.2.0 | The same |
1.2.0-staging | 1.2.0 | The environment whose tag suffix is staging |
1.2.0-rc | 1.2.0 | The environment whose tag suffix is rc |
A suffix that names no environment is an error (exit code 2): Dockflow never guesses.
The default suffix is empty for production, and the environment's own name for every other environment. Set ci.tag_suffix to change it:
version: 1
app: shop
environments:
production: {} # tags 1.2.0
staging: {} # tags 1.2.0-staging
preview:
ci:
tag_suffix: rc # tags 1.2.0-rcA pre-release tag with a hyphen, such as 1.2.0-rc.1, is read the same way: its suffix is rc.1. When your tags don't follow this pattern, name the environment on the command line.
An environment named on the command line always wins over the tag. dockflow deploy staging in a tag build deploys to staging, and the version label still comes from the tag. --version <label> replaces the version label.
Secrets as CI variables
Every secret of an environment is a CI variable prefixed with the environment's name in upper case. For production:
| CI variable | What it holds |
|---|---|
PRODUCTION_CONNECTION | The connection string of the server: its address, its host key and the deploy key. dockflow server init <admin>@<host> prints it when it prepares the server; dockflow server connection <user@host[:port]> -k <key> prints one for an existing user and key |
PRODUCTION_DB_PASSWORD | The value of ${DB_PASSWORD} in your Compose files and dockflow.yml, for production |
Give the connection string as this variable, not as --connection "$PRODUCTION_CONNECTION": an argument is visible in the process list to every user of the runner, which matters on shared or self-hosted runners. When a step must pass it explicitly, use --connection @<file> or --connection - with the string on standard input.
Among the process variables, Dockflow reads only those that start with the environment's prefix. A CI variable named DB_PASSWORD alone is ignored, so a variable meant for staging never reaches production by accident.
You can also pass a set of secrets as one JSON object in DOCKFLOW_SECRETS. On GitHub Actions, this gives Dockflow every secret of the repository:
env:
DOCKFLOW_SECRETS: ${{ toJSON(secrets) }}GitHub may hold the first run of a workflow that hands every secret to a job this way, until someone approves it on the run's page in the Actions tab. To avoid that, name each secret instead: PRODUCTION_DB_PASSWORD: ${{ secrets.PRODUCTION_DB_PASSWORD }}.
Inside DOCKFLOW_SECRETS, the key X is looked up as PRODUCTION_X first, then as X. An unprefixed key there is a value shared by every environment.
Process variables and DOCKFLOW_SECRETS come before .env.dockflow, the file you keep out of Git for local deploys. Secret values are masked as *** in everything Dockflow prints or stores: events, build and hook output, errors. See Secrets for the rest.
dockflow build and dockflow deploy interpolate the whole Compose file. A build job therefore needs every variable the Compose files use, as the deploy job does. On GitHub, secrets are not given to workflows started from forks.
Overlapping runs: --wait-lock
In CI, two runs often overlap, such as a push to main and a tag pushed right after. Let the second one wait:
dockflow deploy --wait-lock 10mEach environment has one lock. Without --wait-lock, a run that starts while another holds the lock fails at once with exit code 5. With it, the run waits up to that long for the other one to finish, and emits a lock.waiting event that names the holder.
A lock taken by hand with dockflow lock acquire (a maintenance window) is never waited for. The deploy fails with exit code 5 unless you pass --force.
Building on every branch
dockflow build builds the images of the services with build:, without deploying. It needs no server: it builds for the runner's platform (--platform changes it) and checks that every image builds.
dockflow build productionIn CI, dockflow build takes the environment from the tag, as deploy does. On a branch, name the environment when dockflow.yml has several. --only web,worker builds some services only. See Building images for every option.
A runner that builds needs Docker with buildx (or Podman, with build.engine: podman). A runner that only deploys services with public images needs no Docker at all: Dockflow reaches the server's Docker over SSH.
Build in one job, deploy in another
With --push, dockflow build connects to the server and sends the images where a deploy would, built for the server's platform. A later job deploys them with --skip-build, without building again. Both jobs must check out the same commit: it gives the version label, and so the image tag.
dockflow build production --push # job 1
dockflow deploy production --skip-build # job 2, same commitTo have a person review the plan before it is applied, save it in one job and apply it in another, after an approval step:
dockflow plan production --out plan.json # job 1: keep plan.json as an artifact
dockflow deploy --plan plan.json # job 2, once approvedThe second job builds nothing and applies exactly the reviewed plan. If anything changed in between, on the server or in the project, it fails with plan.stale (exit code 6) and changes nothing.
Complete workflows
Each workflow builds on branches and deploys on tags, with the CLI pinned to a version. The GitHub and GitLab ones also back up on a schedule, as dockflow init --ci writes them.
Replace production with your environment. Add the secrets your Compose files use (PRODUCTION_DB_PASSWORD here) as CI variables.
Add PRODUCTION_CONNECTION and your other secrets under Settings → Secrets and variables → Actions.
name: Deploy
on:
push:
branches: ["**"]
tags: ["*"]
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
jobs:
build:
if: github.event_name == 'push' && github.ref_type == 'branch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dockflow
run: curl -fsSL https://raw.githubusercontent.com/Shawiizz/dockflow/main/install.sh | sh -s -- --version v1.0.0-alpha.6
- name: Build
run: dockflow build production
env:
DOCKFLOW_SECRETS: ${{ toJSON(secrets) }}
deploy:
if: github.event_name == 'push' && github.ref_type == 'tag'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dockflow
run: curl -fsSL https://raw.githubusercontent.com/Shawiizz/dockflow/main/install.sh | sh -s -- --version v1.0.0-alpha.6
- name: Deploy
run: dockflow deploy --wait-lock 10m
env:
DOCKFLOW_SECRETS: ${{ toJSON(secrets) }}
# the services that declare x-dockflow.backup, then the server's state
backup:
if: github.event_name != 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dockflow
run: curl -fsSL https://raw.githubusercontent.com/Shawiizz/dockflow/main/install.sh | sh -s -- --version v1.0.0-alpha.6
- name: Back up
run: dockflow backup run production --wait-lock 10m && dockflow backup run production --server
env:
DOCKFLOW_SECRETS: ${{ toJSON(secrets) }}schedule runs the backup job every night at 03:17 UTC, and workflow_dispatch lets you run it from the Actions tab.
The installer adds its directory to GITHUB_PATH when it is not already on the PATH, so later steps find dockflow.
The GitHub Action
On GitHub, the Shawiizz/dockflow action installs the CLI and runs it:
- On a pull request, it posts the plan of the change as a comment, so a reviewer sees what merging would change on the server. The comment is updated on each push.
- Elsewhere, it deploys.
name: Deploy
on:
pull_request:
push:
tags: ["*"]
permissions:
contents: read
pull-requests: write # the plan's comment
jobs:
dockflow:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Shawiizz/[email protected]
with:
env: production # the environment a pull request's plan is for
command: ${{ github.event_name == 'push' && 'deploy --wait-lock 10m' || '' }}
env:
DOCKFLOW_SECRETS: ${{ toJSON(secrets) }}The comment looks like this:
Dockflow plan: shop to production
Version
4f2c1a9, servermain.
Change Target Strategy Why ~ service.update webstart-first image changed upload.apply /etc/nginx/sites-enabled/shop.confchanged
| Input | Default | Description |
|---|---|---|
command | plan on a pull request, deploy otherwise | The command and its arguments, as deploy staging or backup run. |
env | none | The environment a pull request's plan is for. Needed when dockflow.yml has several. |
comment | true | On a pull request, post the plan as a comment. It is also written to the job's summary. |
build | false | On a pull request, build the images, so the plan says exactly which services change. Off, the pull request's code is not built, and built services show as changed. |
version | the action's ref when it is a version, else the newest release | The Dockflow version to install; none installs nothing and uses the dockflow on the PATH. |
working-directory | . | The project's directory. |
github-token | github.token | The token that posts the comment. It needs pull-requests: write. |
The action installs cosign first, then the CLI with install.sh --require-signature: the binary is checked against the release's SHA256SUMS, and SHA256SUMS against its Sigstore signature from the repository's release workflow. If either does not verify, the step fails and nothing runs. With version: none, it installs neither.
The plan needs the connection secret to read the server's state. GitHub gives no secrets to pull requests from forks: their plan fails, and the comment says so. A failed plan fails the job.
The plan gives pull requests the production connection
The connection secret is the deploy user's key: it can deploy, not only read. A pull request's run from a branch of the repository gets it, and that branch can change the workflow. With the plan on pull requests, anyone who can push a branch can reach production.
Turn it on only where everyone who pushes branches may deploy anyway, as in a team that reviews changes to its Compose files and dockflow.yml. Elsewhere, leave pull_request out of the workflow's triggers, or keep the secrets in a GitHub environment whose required reviewers approve each run.
The action computes the plan again at deploy time. After a merge, the commit (and maybe the server) changed since the review, so the action deploys the merged code, not the reviewed plan. To apply exactly a reviewed plan, use dockflow plan --out and dockflow deploy --plan, as in Build in one job, deploy in another.
Deploying from a branch
Tags are not required. To deploy every push to main to staging, name the environment:
dockflow deploy staging --wait-lock 10mThe version label is then the short commit, such as 3f9c2ab.
Exit codes
Every failure has its own exit code, so a CI step can tell a refused configuration (3) from a held lock (5), a failed build (7) or a deploy that was rolled back (8). See Exit codes.
Configuration
How a Dockflow project is configured, with dockflow.yml, Compose files, environments, variables, secrets and the two interpolation syntaxes.
Building images
How Dockflow builds the services that have a Compose build section, sends their images to the server or to a registry, and pins them by digest.