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.
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:
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.