Build, deploy and release

A product reaches production through three commands that do three different jobs. product:build compiles the product into deployable artifacts, product:deploy runs those artifacts on the machine you are on, and product:release publishes a version of the product to the outside world. Two of them form a chain. The third stands on its own.

Table of contents

Three verbs

Build compiles, deploy runs, release publishes product:build Compile Merges env and Compose files, builds the production image. product:deploy Run Starts the containers from what build produced. product:release Publish Bumps the version, tags it, pushes to the forge, PyPI and Docker Hub. Nothing leaves this machine The only one that publishes
Build and deploy stay on the machine that runs them. Release is the one that sends things out.

Build reads the product and every feature it declares, merges their production environment and Compose definitions into two files under docker/, and builds the production image on the local Docker daemon. Deploy takes those two files, fills in the secrets, and starts the production containers from that image, on the same machine. Release bumps the version, tags the commit, and pushes the product to the forge, to PyPI and to Docker Hub. It does not start anything. Build and then deploy is how a product goes live on a host. Release is how a version of the product gets a name that other machines and other people can refer to.


Side by side

  product:build product:deploy product:release
In one word Compile Run Publish
Reads The product’s pyproject.toml, its docker/ directory and the docker/ directory of every declared feature docker/.env.deploy.example and docker/docker-compose.deploy.yml, both written by build The product’s git repository and pyproject.toml
Writes docker/.env.deploy.example, docker/docker-compose.deploy.yml, docker/features/, and the image <product>:<version> on the local daemon docker/.env.deploy, then the containers of the <product>_deploy Compose project A version bump committed in pyproject.toml, and a git tag
Sends outside the machine Nothing Nothing The commit, the tag and a release page to the forge, the package to PyPI, the image to Docker Hub
Needs Every production feature pinned to a version that is on PyPI or tagged on the forge A previous build, and the <SET> values the first time Every feature pinned, lint and tests passing, credentials for the three channels
Undo Run it again product:deploy --down None. A PyPI version is permanent
Typical place The production host, or your workspace to check that the image builds The production host Your workspace

The chain, build then deploy

What build reads and writes, and what deploy runs BUILD READS <product>/pyproject.toml which features, at which versions <product>/docker/ env, Compose and the prod Dockerfile <feature>/docker/ env and Compose defaults, per feature product:build BUILD WRITES docker/.env.deploy.example production env, secrets left as <SET> docker-compose.deploy.yml product and feature services, one stack image <product>:latest on this machine's Docker daemon product:deploy DEPLOY RUNS <product>_deploy Compose project, env from docker/.env.deploy, your secrets kept across deploys web, from image <product>:latest db feature services
Deploy consumes what build produced and nothing else. The web container runs the image build tagged latest.

product:build, compile

product:build turns the product and its features into something Docker can run without the workspace. It runs the pre-flight checks first (the feature selection is satisfiable, the contracts do not collide, every production feature is pinned and reachable) and then does three things.

  1. Merges the production environment. Each feature’s .env.prod.example and declared configuration, then the product’s choices for those features, then the product’s own .env.prod.example, into docker/.env.deploy.example. Secrets stay as <SET> placeholders.
  2. Merges the Compose definitions. The product’s docker-compose.prod.yml and every feature’s, with the feature Dockerfiles and assets copied into docker/features/, into docker/docker-compose.deploy.yml.
  3. Builds the production image from docker/Dockerfile.<product>.prod, with the workspace as build context, and tags it <product>:<version> and <product>:latest. The version is the one in pyproject.toml. The image installs every feature from PyPI at its pinned version, or from its git tag when PyPI does not serve it, and never from your working copies. --no-image skips this step.

The two files are deployment artifacts and are versioned with the product, so a clean clone carries them. The image lives on the Docker daemon of the machine that ran the build. See product:build for the merge rules and Env files for where each variable comes from.

product:deploy, run

product:deploy starts the product in production mode on the machine where you run it. It refuses to start without the two files from build. It creates docker/.env.deploy from the template the first time, asks for every <SET> value with a sensible suggestion, generates SECRET_KEY, and on later deploys syncs the file with the template while keeping what you filled in. It checks that no other container holds the ports, marks the product as deployed with SPLENT_ENV=prod, and runs

docker compose -p <product>_deploy -f docker-compose.deploy.yml --env-file .env.deploy up -d

The web service in that Compose file names image: <product>:latest, so deploy runs exactly the image that build made and never rebuilds it on its own. When the app answers, deploy prints its URL. product:deploy --down stops the same Compose project with the same env file. See product:deploy for the sync rules and for what the CLI does while production is deployed.

Putting a change into production

To put a change into production, a new pinned feature version, a new environment variable, a change in the product itself, run build again and then deploy again. Build rewrites the template and the image. Deploy syncs .env.deploy without losing your secrets and restarts the containers on the new image. product:derive --prod runs the two in one command.


Release stands apart

Release publishes a version to three channels, in order YOUR WORKSPACE <product>/ git repository every feature pinned, lint and tests green pyproject.toml version 1.2.2 becomes 1.2.3 product:release 1 PyPI <product> 1.2.3, permanent 2 The forge commit, tag v1.2.3, release page 3 Docker Hub <user>/<product>:1.2.3 and :latest WHAT RELEASE DOES NOT DO It does not read what build wrote, and it does not start a container. The version it bumps is the one the next build, on any host, tags its image with.
PyPI goes first because a version there can never be replaced. Docker Hub goes last because it is the channel most able to diverge.

product:release gives a version of the product a name and publishes it. It refuses to run while any feature is editable, because a release must be reproducible from its pinned versions alone. Then it verifies the three channels, runs lint and the product’s tests, bumps the version in pyproject.toml, commits, uploads the package to PyPI, pushes the commit and the tag v<version> to the forge and creates the release page there, and finally builds the production image and pushes it to Docker Hub as <user>/<product>:<version> and <user>/<product>:latest. That image is the same build as product:build makes, the production Dockerfile with the workspace as context, tagged for Docker Hub instead of for the local daemon.

If a later step fails, release:resume finishes the channels that are missing without bumping the version again. See product:release for the gates and the credentials.

Release does not need a build to have happened, and build does not need a release. What ties them is the version in pyproject.toml. Release is what moves it, and build reads it to tag the local image, so a host that builds after a release gets an image tagged with the released version. Release also runs from your workspace, where the git repository and the credentials are, while build and deploy run wherever the product is served.

Nothing in this chain is automatic. A release does not deploy, and a deploy does not release. A host that should run a released version clones the product repository at its tag, builds and deploys.


Which one do I run

You want to Run
Put the product, or a change to it, into production on this machine product:build, then product:deploy
The same, in one command product:derive --prod
Check that the production image builds, without deploying product:build
Regenerate the deploy files without the slow image build product:build --no-image
Stop production product:deploy --down
Name a version of the product and publish it to the forge, PyPI and Docker Hub product:release
Finish a release that stopped halfway release:resume product
Release a feature, not the product feature:release, see Editable and pinned features

See also


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es