Skip to content

Container releases

The Backend containers workflow builds images from one source commit, checks each image before publishing it to GitHub Container Registry (GHCR), assembles a manifest of immutable digests, and then publishes one multi-architecture index per self-hosted repository together with an immutable build tag and guarded channel tags such as dev and latest. Follow a channel tag for development hosts; pin a digest or build tag for production and retain it with the deployment configuration.

Repository names follow ghcr.io/<owner>/flow-like-<target>-<workload>; the owner is the lowercased GitHub repository owner. Forks publish to their own namespace. The checked-in matrix contains 58 platform builds for 42 image repositories:

TargetWorkloadsPlatform
AWSAPI, compiler, file-tracker, media-transformer, event-bridge, maintenance, signaling, migrationARM64
AWSexecutor, executor-asyncAMD64
AWSapi-ecsAMD64 and ARM64
GCPAPI, queue-worker, executor, signaling, migration, scheduler, maintenanceAMD64
AzureAPI, queue-worker, executor, maintenance, scheduler, migration, signaling, otel-collectorAMD64
Docker ComposeAPI, runtime, compiler, execution-manager, sink-services, signaling, db-init, object-store-init, webAMD64 and ARM64
KubernetesAPI, executor, execution-manager, sink-trigger, migration, webAMD64 and ARM64

Workload names in image repositories are lowercase. Kubernetes also uses the Compose runtime, compiler, signaling and object-store initializer images. The workflow leaves PostgreSQL, Redis, RustFS and monitoring tools as upstream images. See the target matrix for exact Dockerfile paths and platforms.

Every build record identifies a single-platform image. AMD64 and ARM64 are separate records, such as docker-compose-runtime-arm64. After the complete set is published, the workflow combines both records of each self-hosted repository into one multi-architecture image index, so a Compose host or a mixed-architecture Kubernetes cluster pulls the index by tag or digest and receives the matching platform. Cloud repositories stay single-image manifests because Lambda rejects an index. The AWS ECS API never runs on Lambda, so its two records become an index as well. See Tags and visibility.

The fifteen self-hosted packages are public: anyone can pull them without a registry login. Cloud packages (AWS, GCP, Azure) stay private to the owner organization. Nothing in the repository changes visibility; it is a package setting in the GitHub UI, and forks publish private packages by default.

Run Docker builds from the repository root, with Docker and Buildx available. Use the target’s native architecture when compiling Rust. For the AWS API:

Terminal window
docker buildx build \
--platform linux/arm64 \
--provenance=false \
--load \
--tag flow-like-aws-api:local \
--file apps/backend/aws/api/Dockerfile \
.

The final . supplies the root workspace, lockfile and packages. --load imports this single-platform result into the local Docker image store. Choose another recipe and platform from the matrix when building another image. CARGO_BUILD_JOBS can limit Rust compilation to fit builder memory.

The AWS API and file tracker use an empty CRDB_CA_URL for DSQL. For an Aurora database in eu-west-1, add:

Terminal window
--build-arg CRDB_CA_URL=https://truststore.pki.rds.amazonaws.com/eu-west-1/eu-west-1-bundle.pem

An external database may require its own CA bundle. Runtime hub configuration does not replace certificates baked into an image. Database passwords, signing keys and deployment storage credentials belong in runtime configuration and are not required to compile the standard image matrix.

GCP and Azure API recipes also accept an optional legacy custom compiled default through the flow_like_config BuildKit secret and its FLOW_LIKE_CONFIG_SHA256 build argument. The resulting executable contains that document. A secret mount avoids a loose copy in a build layer; it does not hide the embedded content from someone who can pull the image. Prefer runtime config for installation-specific settings.

The dedicated audit worker recipes take the same build input as their API: FLOW_LIKE_CONFIG for flow-like-audit-worker (default: the Compose example, whose audit section matches the Kubernetes example) and the flow_like_config secret for the Azure and GCP workers. The worker compiles that document’s audit section as its only policy and accepts no runtime config; see Audit policy.

Set one nonempty source on the API container:

VariableValue
FLOW_LIKE_CONFIG_JSONA complete JSON configuration, including a deployment-secret injection
FLOW_LIKE_CONFIG_FILEA readable mounted JSON file
FLOW_LIKE_CONFIG_SECRET_REFA key or secret://... reference resolved by the API’s configured secret store

An override replaces the entire document. It is never merged with the compiled default. With no override, the default is used. Empty environment values count as unset; whitespace-only values, conflicting sources, unreadable files, unresolved secrets and invalid documents stop startup without selecting a fallback. Input must be UTF-8 and no larger than 4 MiB. Deployment platforms and secret providers may impose smaller limits; mounted files avoid environment size limits.

The API reads the document once. Restart or roll out API processes after changing it. For example, supply the normal API environment and a read-only config mount:

Terminal window
docker run --env-file ./api.env \
--mount type=bind,src=/absolute/path/flow-like.config.json,dst=/run/secrets/flow-like.config.json,readonly \
--env FLOW_LIKE_CONFIG_FILE=/run/secrets/flow-like.config.json \
ghcr.io/rheosoph/flow-like-docker-compose-api@sha256:<selected-platform-digest>

The selected document configures Hub metadata, OpenID issuer/audience/client validation and third-party OAuth endpoints. Keep OAuth client secrets in the secret store, referenced by client_secret_env; literal oauth_providers.*.client_secret values are rejected. Public Hub fields remain visible to clients even when the document comes from a secret. Startup errors do not include the document contents or secret reference.

Secret references use the API’s existing provider configuration: AWS Parameter Store, GCP Secret Manager or Azure Key Vault where configured. File mounts and secret-injected environment values work for self-hosting. This selection grants no new cloud permissions and does not change compiled provider features.

Compose and Kubernetes API releases include the corresponding self-hosting example with placeholder OIDC settings. Supply the installation’s complete document at startup. Their release records use runtime-config-self-hosted-default, hash that fallback, and identify the full-document-v1 runtime contract. Follow the Compose configuration or Helm configuration instructions to supply it.

Both self-hosted web images accept these public settings at container startup:

VariableMeaning
FLOW_LIKE_WEB_API_URLRequired browser-reachable HTTP(S) API base URL
FLOW_LIKE_WEB_REDIRECT_URLOptional login callback; defaults to the browser origin plus /callback
FLOW_LIKE_WEB_LOGOUT_URLOptional logout destination; defaults to the browser origin plus /

Compose maps its NEXT_PUBLIC_* deployment values into these variables. Helm uses web.runtimeConfig.apiUrl, falling back to api.publicUrl, plus optional redirectUrl and logoutUrl. Recreate containers or roll out Pods after changes; the same image can serve another installation without rebuilding.

The entrypoint validates URLs and writes only these allowlisted public values to /tmp/flow-like-web/runtime-config.js. Nginx serves it with no-store and the app loads it before hydration. A missing API URL, URL credentials, query strings or fragments stop startup. The image runs without root and supports a read-only root filesystem with writable /tmp.

Non-container web and desktop builds retain their build-time behavior. Static SEO/social metadata also keeps its build defaults. Third-party integration OAuth still uses the hosted flow-like.com/thirdparty/callback relay and may select the stored profile’s Hub. These runtime variables do not configure that separate relay or its provider registrations.

The existing apps/web application includes the hosted App route /a/<app-id>/<route>, which serves the chat, form or custom page published on that route. Deploy the normal Compose or Kubernetes web image, or publish the normal web static export. The browser resolves routes through the API at runtime, so publishing an Event does not require another web build.

Container images use the runtime web settings above. For a static host, build the web application from a checkout with the workspace dependencies installed:

Terminal window
NEXT_PUBLIC_API_URL=https://api.example.com \
NEXT_PUBLIC_REDIRECT_URL=https://app.example.com/callback \
NEXT_PUBLIC_REDIRECT_LOGOUT_URL=https://app.example.com/ \
bun --cwd apps/web run build

Upload apps/web/out to the web host. These static-build settings use the same API and login callback as the rest of the web application. NEXT_PUBLIC_API_URL is the browser-reachable API origin, without /api/v1. Changing these build settings requires another export; container runtime settings do not.

Set FRONTEND_BASE_URL on the API to the existing web origin, for example https://app.example.com. Compose setup derives it from --web-origin; Kubernetes setup derives api.frontendBaseUrl from PUBLIC_WEB_URL. Both local defaults use http://localhost:3001. Update this value when moving the web application to another domain and restart the API. If FRONTEND_BASE_URL is unset or blank, the API can reuse a configured FRONTEND_URL.

Keep the web origin in the API’s CORS_ALLOWED_ORIGINS, preserving desktop and other application origins. Helm uses api.corsAllowedOrigins. Hosted frontends use the web application’s existing OpenID client and /callback route. Keep the deployed callback registered with that identity provider. No separate OpenID application or callback is needed for /a.

Hosting starts disabled. Enabling it requires sign-in by default: the web application uses its normal login and returns to the original App route. Allow anonymous access is a separate opt-in with a confirmation warning that anyone with the link can execute workflows and the App owner pays for usage. Disabling hosting clears that choice in the editor. Save the Event to apply hosting and access changes. It must also use Remote execution and Public exposure, and be active. See Events.

The API permits anonymous use only when the Event’s decoded configuration explicitly sets frontend_hosting.allow_anonymous to true. A legacy auth_proxy: false or a missing allow_anonymous value never enables anonymous access.

Micro-widgets follow the registry’s existing access rules. Anonymous pages can load public widget packages when the registry allows anonymous reads. Private packages still require package access. Custom Page actions use the published page contract; hosted chat does not execute raw board widget actions.

The web export includes _redirects rules for hosts that support that format. Both container web images include equivalent Nginx routing. On other static hosts, rewrite these request paths to the corresponding export file while preserving the requested browser URL and query string:

Web host requestExport file
/a/<app-id>/<route>/a.html
/use/<app-route>?id=<app-id>/use.html
/callback/callback.html

The query form, such as /a?app=<app-id>&route=/orders, also works when the host resolves /a to /a.html. Configure clean URLs for the exported pages. An index.html fallback alone does not load the requested interface entry point. On Cloudflare Pages, rewrite /a/* to /a, never to /a.html: Pages answers a .html target with a redirect to the clean path, which drops the route.

App routes also need the /use/* rewrite for direct links such as /use/orders/123?id=my-app. Serve existing exported files first, including /use/__next.*.txt route payloads used when entering the App. Preserve id and other query parameters when serving /use.html. /use/?id=my-app selects the App’s root route; event-only links remain /use?id=my-app&eventId=event. Legacy /use?id=my-app&route=/orders/123 links are converted to the path form by the web application through browser history replacement. Desktop uses the same path format and resolves deep links through its bundled assets.

With separate API and web origins, the API’s /a/* shortlinks redirect to the same path on the web origin. Forward those API shortlinks to the API along with /frontend/*, /api/v1/*, /r/* and /m/*. The bundled Compose API proxy and default Helm API ingress already forward every API path.

With one shared origin, route /a and /use, including their subpaths, directly to the web service. Route /frontend/*, /api/v1/*, /r/* and /m/* to the API service. The Helm values include this path-based example. Sending the shared origin’s /a/* paths to the API would redirect them back to themselves.

For API requests, preserve Authorization, X-Flow-Like-Session, query strings and request bodies, allow CORS preflight requests, disable response caching, and stream execution responses without proxy buffering. An edge that replaces Authorization with its origin signature must preserve the viewer token using the existing X-Flow-Like-Authorization contract.

After deployment, open a published route’s link in a fresh browser session and submit a form or send a chat message. Test a sign-in-protected route and confirm the normal callback returns to it. A 503 mentioning FRONTEND_BASE_URL means the API shortlink destination is missing or invalid. A route with no Event, or whose Event is not published, inactive, Local or Internal, returns 404 from the hosted API; the web host may still serve the static entry page that displays that error.

Each repository receives three kinds of references. Every tag of one release resolves to the same digest, so a tag is only a name for a digest.

ReferenceExampleMoves?
Digestghcr.io/rheosoph/flow-like-docker-compose-api@sha256:…Never; pin this in production
Build tagsha-<40-char commit>-run-<run id>-<attempt>Never; one per workflow attempt
Channel tagdev, main, alpha, beta, latest, 1, 1.2, 1.2.3, 1.2.3-betaForward only, see the guard below

The build job also pushes a per-architecture tag, sha-<commit>-<amd64|arm64>-run-<run id>-<attempt>, for each scanned single-platform image. Those remain available for a platform-specific pull, but the references above are the ones to consume.

For the fifteen self-hosted repositories the digest and every tag name a multi-architecture index containing the AMD64 and ARM64 images built from that commit. The workflow requests the index annotations org.opencontainers.image.revision, org.opencontainers.image.source and org.opencontainers.image.version; an OCI index keeps them, while a Docker manifest list (produced when the pushed images use Docker media types) cannot carry annotations. Apart from the ECS API, the twenty-six cloud repositories are single-platform: the tags point at the image manifest itself, never at an index, because Lambda requires a single-image manifest. The workflow asserts this after every copy. The image config labels carry the revision and source for both kinds and are the fallback the guard reads.

Which channel tags a run applies depends on the Git ref that triggered it:

RefChannel tagsversion annotation
dev, main, alpha branchdev, main or alphabranch name
v1.2.31.2.3, 1.2, 1, latest1.2.3
v0.4.00.4.0, 0.4, latest (no 0 major tag)0.4.0
v1.2.3-rc.11.2.3-rc.1 only1.2.3-rc.1
beta-v0.4.0 (or any <channel>-v…)0.4.0-beta, beta0.4.0
any other refnone; the build tag onlynone

The desktop release workflow creates beta-v* tags, so each desktop beta also publishes backend images under <version>-beta and beta. Semantic versions follow MAJOR.MINOR.PATCH with an optional -prerelease part; build metadata is not a valid Docker tag and is not accepted. python3 .github/scripts/container_images.py tags --ref refs/tags/v1.2.3 prints the tags for a ref.

Moving channel tags are guarded so a re-run of an old ref, or a release branch that lost commits, never makes a package point backwards:

  • dev, main, alpha, beta and other channel names move only when the commit recorded on the existing tag is the same as, or an ancestor of, the commit being published (git merge-base --is-ancestor).
  • latest, <major> and <major>.<minor> move only when the new semantic version is greater than or equal to the version recorded on the existing tag. When an index carries no version annotation (a Docker manifest list drops annotations; single-image cloud manifests never have one), the guard reads the v* tag on the recorded commit from Git history instead.
  • Exact version tags such as 1.2.3 and 1.2.3-beta are not guarded.
  • Missing tags, unreadable metadata or a recorded commit that no longer exists in the repository let the tag move; the job logs the reason.

A skipped tag never fails the job. Each skip is logged as a workflow warning and recorded in container-indexes.json under skipped_tags with its reason. The build tag and the digest are always published, so a release whose channel tag was kept is still consumable by its immutable references.

Self-hosted packages are public and need no docker login. Cloud packages are private; pulling them, or the packages of a private fork, needs a login with a token that has read:packages. The workflow never changes visibility.

The workflow needs contents: read, packages: write and permission to create organization packages. It authenticates with GITHUB_TOKEN after image checks pass. Existing GHCR packages must also grant this source repository Actions write access. It does not need deployment-cloud credentials or a custom registry token. Protect dev, main, alpha, v* and beta-v* because pushes to them publish images and move channel tags.

  1. Push the reviewed source commit or tag, or select Actions > Backend containers > Run workflow when a manual release is needed. A manual run from dev, main or alpha moves that branch’s channel tag.
  2. Select aws, gcp, azure, docker-compose, kubernetes, self-hosted or all. kubernetes includes its shared Compose helpers; self-hosted selects all fifteen self-hosted repositories on both architectures.
  3. After the complete set succeeds, the manifest job uploads container-images-<commit>-<run>-<attempt>. Its container-images.json maps workloads and platforms to single-platform digest references.
  4. The index job then creates the per-repository index or manifest copy, applies the build tag and the guarded channel tags, and uploads container-indexes-<commit>-<run>-<attempt>. Its container-indexes.json lists, per repository, the kind (index or manifest), the digest to pin, the platforms, the applied tags and any skipped_tags.
  5. Set package visibility and consumer read access explicitly.

Records bind the Dockerfile and relevant config/CA identity. The manifest job rejects missing, duplicate, wrong-platform and mixed-build records, and the index job re-validates the manifest before touching the registry. A failed matrix can leave individually checked images in GHCR without a complete manifest and without channel tags. Use Re-run all jobs so every record comes from one attempt. Pin digests for deployments; save both manifests before their 90-day artifact retention expires.

For production, pin the index digest from container-indexes.json or the build tag; both are immutable. Use a channel tag only where following a channel is the intent, such as a development host on dev, and keep pullPolicy: Always or an explicit re-pull step so that host receives the moved tag.

Compose reads FLOW_LIKE_IMAGE_TAG (default dev) and the per-workload API_IMAGE, RUNTIME_IMAGE, EXECUTION_MANAGER_IMAGE, COMPILER_IMAGE, SINK_SERVICES_IMAGE, SIGNALING_IMAGE, DB_INIT_IMAGE, OBJECT_STORE_INIT_IMAGE and WEB_IMAGE overrides. scripts/pull-images.py pulls one tag and rewrites the overrides, plus the sandbox pins, to repository@sha256:… digests; scripts/prepare-images.py builds the runtime and execution manager locally instead. See Compose installation.

Helm defaults every first-party image to its published repository at tag dev and accepts a digest per image. scripts/resolve-images.py resolves one tag to digests for every image, including the execution manager and sandbox, and can add pull secrets and an architecture node selector. See Kubernetes installation. Database migration images require a reviewed migration operation after publication. In particular, the Kubernetes migration command uses --accept-data-loss; review schema changes and recovery before deploying it. See Database.

For cloud deployment, copy the selected image into the installation’s ECR, Artifact Registry or ACR, verify the destination digest and pass that reference to deployment tooling. Keep one image digest across environments and version each environment’s API configuration separately. Verify the deployment tooling accepts prebuilt images before removing any local build steps.

For Lambda, the destination ECR repository must be in the function’s region. Keep the deployable image single-architecture and retain --provenance=false when building it so an attestation image index does not replace the image manifest.

Each job builds once and loads the result locally. The filesystem helper checks platform, the included root license and common credential paths without starting the container. Pinned Trivy v0.69.3 scans image files, configuration and history. A second scan checks printable text extracted from every ELF binary because this scanner version skips ELF files. The job then pushes that same checked image and verifies that the registry manifest identifies its config.

These are recognized-secret-pattern gates. Compressed, encoded, encrypted and unrecognized credentials can evade them. They do not establish a CVE policy, complete dependency-license compliance, signing or cloud promotion. Images include /usr/share/licenses/flow-like/LICENSE; its full Business Source License parameters govern use.

Image consumers can read executable text and layers. API binaries contain their default config, including public client IDs, domains and secret-reference names. Signaling images contain selected TypeScript sources; migration images contain schemas and migration code. Review those contents for the intended audience. Keep deployment credentials out of build contexts and runtime configuration outside image builds.

The supplemental binary scan has two narrow treatments for reviewed public fixtures:

  • Complete GnuTLS self-test PEM blocks are replaced in scanner input only when their exact SHA-256 and byte length match the reviewed fingerprints in the fixture catalog. Unknown, modified, truncated or differently encoded keys remain in the input.
  • A complete reviewed sequence of adjacent public Copilot redaction literals receives a line break at its source boundary. This prevents the Slack prefix and adjacent field names from forming a false token. All original bytes remain; partial or changed sequences are not normalized.

Neither treatment excludes an image path, library or detection rule, and neither changes the image. Trivy reports and extracted strings can contain matched secrets, so they stay in temporary runner storage and are not uploaded or printed. Reproduce failures in a trusted environment and handle raw reports as sensitive.

The workflow uses native runners, at most sixteen parallel jobs, and two Cargo compilation jobs per runner. ARM64 runner access is required. Measure peak memory, disk and build time before raising those limits.

Each image has a separate GitHub Actions layer-cache scope. Builder layers copy the source checkout, so do not export them to a public registry build-cache package. Actions caches are also available to eligible PR workflows; credentials and private dependency source do not belong there.

Rust recipes use RUN --mount=type=cache for crates and build targets. Ordinary layer-cache export does not persist these mounts. Repository variable CONTAINER_CARGO_CACHE=true enables the optional mount restore/extraction steps; it is off by default. Budget storage before enabling it. A source change may otherwise trigger a cold compile even when unchanged layers are cached. The optional Cargo steps do not export the web builder’s .next mounts.

Several AWS recipes use sharing=locked on a Cargo target cache and can serialize on one BuildKit worker. Separate concurrent workers or cache namespaces when needed. Retained native BuildKit workers can preserve large Cargo mounts for frequent releases, but the current workflow uses ephemeral GitHub-hosted builders. Keep trusted release caches separate from untrusted builds.