Push, build, cut over, live

Deploys

A deploy takes your source (or a pinned image) and updates one Kubernetes Deployment with a rolling update. Kubernetes keeps ready old replicas serving while the new revision becomes ready. Every deploy is recorded as an immutable history entry you can inspect and roll back to.

The deploy lifecycle

A deploy moves through a fixed set of states:

State What's happening
Building The image is being built (from a Dockerfile or source) or pulled. With deploy intents, this state appears from the first second of CI.
Release Your release_command runs once, before cutover — migrations, asset prep. If it fails, the deploy stops here and the old version keeps serving.
Running The Deployment's current generation completed its rolling update. For an enrolled app, every desired replica passed its configured TCP or HTTP readiness probe and the requested image is running.
Succeeded The cutover completed and the new version is live.
Failed A build, release, or rollout failed. Ready old replicas can continue serving while Kubernetes refuses to complete a broken new revision.

Readiness and observed health

New apps default to TCP readiness on their primary container port — including the platform's default port when you name none — so a pod only receives traffic once something is listening. Pass readiness_probe_mode at create to choose disabled or http instead. Existing apps stay unenrolled until you choose disabled, tcp, or http; HTTP mode requires an absolute app-specific path such as /health. A change takes effect on the next deploy.

Flicker still adds no automatic liveness probe, so a mistuned health path cannot restart-loop your container.

You can opt one in with liveness_probe_path in your flicker.toml [app] table. Point it at a dependency-free endpoint — not the one readiness uses. The two probes do different jobs: readiness decides whether a pod should receive traffic, so it is right for it to check your database; liveness kills the container, and restarting cannot repair a database. A liveness path that touches your database will restart-loop your app on the first blip. Liveness is checked every 10s and must fail 6 times in a row (after a 30s grace period) before Kubernetes restarts the container, so it fires on a genuinely wedged process — not on a slow one.

The app's top-level status remains lifecycle state for compatibility. Existing API responses also include nested observed health with healthy, unhealthy, or unknown, its check time, and staleness. CLI lists label these separately as HEALTH, CHECKED, and LIFECYCLE. A stale or never-run check never appears healthy. Dedicated authenticated endpoints — GET /api/v1/projects/:id/health, GET /api/v1/apps/:id/health, GET /api/v1/databases/:id/health — and flicker health <project|app|database> <ref> return the same stored results as a rollup or a single resource. They require an API key, are scoped to the caller's organization, and never probe live.

Building from the first second of CI

Normally a deploy only becomes visible when CI finally calls Flicker — minutes into the pipeline. Deploy intents fix that. CI opens an intent at workflow start, so the deploy shows as Building from the very first second, carrying the commit, the actor, and the CI run URL. When the build finishes and the real deploy lands, the worker attaches to that same intent — giving you one continuous record from git push to rollout instead of a row that appears only at the end.

✦
An abandoned intent (a CI run that died before deploying) stays in history as Building rather than being silently adopted by an unrelated later deploy — so the timeline always tells the truth.

Two deploy modes

One command, two ways to point it at your code:

$ flicker deploy                 # manifest mode — pinned image or [build] repo (CI)
$ flicker deploy --from-local    # tar the working dir, build on host (dev)

Manifest mode deploys exactly what's pinned in flicker.toml — a registry image, or a git repo + ref the host clones and builds. Reproducible and reviewable in a PR — the right primary for CI. Local mode tars your working directory (including uncommitted changes), ships it over, and builds it on the host — the "deploy what I have right now" path for development. Both converge on the same cutover.

Building from the project's repo. When a project has a GitHub repository attached, a new app in that project builds from it by default: the app links to the repository, and each build clones the repository's current URL. The link only accepts a repository attached to the app's own project. Detaching the repository, or moving the app to another project, removes the link and leaves the last URL in place. An app given a plain Git URL, from the API or the CLI, builds from that URL. If both are given, the attached repository wins. A private repository builds too: each build of a linked app clones with a GitHub App installation token minted for that one build, restricted to that one repository, read-only, and valid for about an hour. It reaches the build as a Kubernetes Secret the build Job mounts, never as an argument, an environment value, or a layer in the image, and it is deleted when the build ends. A plain Git URL gets no token, so a private repository given that way still cannot be cloned.

Build secrets. A value your build needs but your running app must not carry — a private package token, a license key — is a build secret. Set it with flicker deploy --build-secret KEY=VALUE (repeatable); it is stored encrypted on the app and reused by every later build. A key must look like an environment variable name ([A-Za-z_][A-Za-z0-9_]*). At build time, on both builders, every build secret travels the same way as the clone token above: one Kubernetes Secret written for that build alone, mounted read-only as files, and handed to BuildKit as --secret id=KEY,src=<file>. No value is in the build Job's definition, its arguments or its environment, and none reaches an image layer. Your Dockerfile reads it only where it mounts it:

Dockerfile
RUN --mount=type=secret,id=FLICKER_SUGGESTIONS_TOKEN \
    GITHUB_TOKEN=$(cat /run/secrets/FLICKER_SUGGESTIONS_TOKEN) mix deps.get

Anything a RUN step prints is in the build log, so a step that echoes a secret would still persist it there; build secret values are redacted from the log before it is stored, but do not rely on that.

Deploy history

Every deploy writes an immutable history entry with its state transitions, the image it ran, and a link to its build and deploy logs. The current status is a cheap denormalized cache; the event log is the source of truth, and historical rows are never mutated. The app page surfaces this timeline so you can see what shipped, when, and from which commit.

From the CLI, tail a running deploy's container logs:

$ flicker app logs web

Rollback via the Registry

Because every deploy stamps the exact image ref it ran, rolling back is just deploying a previous image. The app's Registry lists the images prior deploys used; pick the last good one and redeploy it:

$ flicker app deploy web --image myorg/web:v2
# re-run the same rolling cutover with the previous image

The rollback runs through the identical rolling cutover as a forward deploy — if the old image doesn't come up, the current version keeps serving. Rollback is a deploy, so it's recorded in history like any other.