From ed590bc82d57e110be00333df1854ad1ed061b25 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Wed, 14 Oct 2026 13:18:18 +0200 Subject: [PATCH] fix(electric): mirror the pinned image instead of building a context alchemy hashes a Dockerfile build context together with bun.lock, so any dependency change retagged the Electric image and restarted the singleton task. On 2026-10-14 two back-to-back PRD deploys (one landing-only) each caused a ~2 min sync outage this way. The Dockerfile was a single pinned FROM, so pass that ref as `image`: alchemy mirrors it to ECR under a tag derived from the ref and platform only, and Electric restarts only when the pin changes. The deploy that ships this restarts it once more, since the tag scheme changes. --- .github/workflows/publish-electric-image.yml | 6 +++--- apps/electric/Dockerfile | 15 --------------- apps/electric/alchemy.run.ts | 15 ++++++++++----- docs/electric-sync.md | 7 +++++-- 4 files changed, 18 insertions(+), 25 deletions(-) delete mode 100644 apps/electric/Dockerfile diff --git a/.github/workflows/publish-electric-image.yml b/.github/workflows/publish-electric-image.yml index 6eb57f20c3..34a4a36e73 100644 --- a/.github/workflows/publish-electric-image.yml +++ b/.github/workflows/publish-electric-image.yml @@ -2,7 +2,7 @@ name: Publish Electric image # Upstream's `electricsql/electric` Docker Hub repo was deleted (electric-sql/electric#4822), # which broke every PRD deploy at the Electric image build. This builds the same release -# from upstream source, once, into our own registry; `apps/electric/Dockerfile` pins the +# from upstream source, once, into our own registry; `ELECTRIC_IMAGE` in `apps/electric/alchemy.run.ts` pins the # resulting digest. Run it again only to adopt a new Electric version. on: @@ -91,7 +91,7 @@ jobs: { echo "Pushed \`${NAME}:${VERSION}@${DIGEST}\`" echo - echo "Built from electric-sql/electric@${UPSTREAM_SHA}. Pin it in apps/electric/Dockerfile:" + echo "Built from electric-sql/electric@${UPSTREAM_SHA}. Pin it as ELECTRIC_IMAGE in apps/electric/alchemy.run.ts:" echo - echo " FROM ${NAME}:${VERSION}@${DIGEST}" + echo " ${NAME}:${VERSION}@${DIGEST}" } >> "$GITHUB_STEP_SUMMARY" diff --git a/apps/electric/Dockerfile b/apps/electric/Dockerfile deleted file mode 100644 index bb8638b2f8..0000000000 --- a/apps/electric/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# The upstream ElectricSQL sync service, unmodified. -# -# A Dockerfile rather than an image reference because alchemy's ECS service -# builds a context and pushes to ECR — so the task pulls from there over the -# VPC's S3 gateway endpoint, not from Docker Hub on every placement. -# -# PINNED, never `:latest`. `docker-compose.yml` tracks `:latest` locally on -# purpose, but here an upgrade is a deliberate read of the release notes: the -# storage format has changed across minors, and rolling back after the new -# version has rewritten the shape cache re-snapshots every connected client. -# -# Built from upstream source into our own registry by publish-electric-image.yml: -# upstream's Docker Hub repo was deleted (electric-sql/electric#4822). To upgrade, -# run that workflow with the new version and pin the digest from its summary. -FROM ghcr.io/mapletechlabs/maple/electric:1.7.12@sha256:d8a377eee0edeac0d4dbbd77c22b92c1ef601184722ea2b7da5b3381e3bb8e2c diff --git a/apps/electric/alchemy.run.ts b/apps/electric/alchemy.run.ts index 78042f67a2..c036f47823 100644 --- a/apps/electric/alchemy.run.ts +++ b/apps/electric/alchemy.run.ts @@ -1,4 +1,3 @@ -import { resolve } from "node:path" import * as AWS from "alchemy/AWS" import type * as Output from "alchemy/Output" import * as Effect from "effect/Effect" @@ -10,8 +9,15 @@ import { requiredPlain } from "@maple/infra/env" /** Port Electric's HTTP API binds (`ELECTRIC_PORT`, whose own default is 3000). */ const ELECTRIC_PORT = 3000 -/** Absolute: alchemy has changed how a relative `dockerfile` resolves between releases. */ -const DOCKERFILE = resolve("apps/electric/Dockerfile") +/** + * Upstream Electric, built into our registry by publish-electric-image.yml (upstream's Docker + * Hub repo was deleted, electric-sql/electric#4822). PINNED by digest: the shape cache format + * changes across minors, so an upgrade is a deliberate read of the release notes. Mirrored as an + * image ref, never built from a context: alchemy hashes a context together with `bun.lock`, so + * any unrelated dependency change would retag the image and restart this singleton. + */ +const ELECTRIC_IMAGE = + "ghcr.io/mapletechlabs/maple/electric:1.7.12@sha256:d8a377eee0edeac0d4dbbd77c22b92c1ef601184722ea2b7da5b3381e3bb8e2c" export interface CreateMapleElectricOptions extends Pick< MapleStackContext, @@ -114,8 +120,7 @@ export const createMapleElectric = ({ cluster, serviceName: name("electric"), - context: "apps/electric", - dockerfile: DOCKERFILE, + image: ELECTRIC_IMAGE, // Must match the image arch: a mismatch dies at start with `exec format error`. runtimePlatform: { cpuArchitecture: "ARM64", operatingSystemFamily: "LINUX" }, cpu: taskSize.cpu, diff --git a/docs/electric-sync.md b/docs/electric-sync.md index 3bc6f6347f..2aa4ba0da6 100644 --- a/docs/electric-sync.md +++ b/docs/electric-sync.md @@ -199,8 +199,11 @@ service runs `desiredCount: 1` with `minimumHealthyPercent: 0`: stop the old task, then start the new one. Every deploy therefore has a ~60s window with no sync. `/dashboards` degrades to its HTTP snapshot (`SyncDegradedBanner`), while the alerts lists have no fallback and sit in their retry state. This is why the -image tag is pinned in `apps/electric/Dockerfile` rather than tracking `:latest` -as local docker does. +image is pinned by digest (`ELECTRIC_IMAGE` in `apps/electric/alchemy.run.ts`) +rather than tracking `:latest` as local docker does, and why it is mirrored as an +image ref instead of built from a Dockerfile: alchemy hashes a build context +together with `bun.lock`, so any dependency bump retagged the image and restarted +Electric (two ~2 min gaps on 2026-10-14). ### Standing it up