Experimental slim runtime artifacts and Docker images for Supabase services.
This repo asks a simple question, on three axes:
How small can each Supabase service be — in image size, memory, and CPU — if we package only the runtime files it actually needs and ship it with a low-footprint runtime profile?
For the latest published Linux ARM64 release set (10 services), upstream images total 2042.0 MiB compressed; the slim set totals 562.1 MiB (72.5% smaller — exact numbers below). Every published service also ships measured steady-state RSS and idle-CPU numbers, and a minimal core stack (postgres + auth + postgrest) idles at roughly 65 MiB of RSS per stack with near-zero idle CPU.
This project has three long-term goals:
- Figure out how to package each Supabase service as a self-contained archive or executable for macOS and Linux, as small as we can make it while still being reliable and smoke-testable.
- Use those artifacts to produce the smallest practical Docker images, then upstream the maintainable build and packaging improvements back to each service repository.
- Minimize each service's runtime footprint — steady-state memory and idle CPU — so many local stacks can run in parallel on one developer machine (the working target: ~25 stacks on a 32 GB laptop).
The Docker images are the first delivery target because they immediately help local development and CI. The deeper goal is portable, minimal service runtime artifacts that can be reused both inside and outside containers.
Supabase local development and CI pull a lot of service images. Large images cost time, bandwidth, cache space, and iteration speed. The goal here is to produce smaller local/CI-oriented service images while keeping upstream service source trees read-only and preserving a clear validation path.
Disk is only half the story: image layers are stored once and shared by every container, but RSS and CPU multiply per running stack. That is why each service now also carries a runtime profile and measured runtime numbers — see Runtime Footprint below.
The approach is intentionally service-by-service:
- Build or extract a minimal runtime artifact.
- Remove sourcemaps, debug files, caches, docs, tests, and other non-runtime debris.
- Copy only required binaries, release files, assets, and runtime libraries.
- Use the smallest viable final base:
scratchfirst, then Distroless, then Alpine only when it is clearly the right fit. - Smoke-test the artifact-backed image and the final slim image.
Image sizes are gzip-compressed; Idle RSS and Idle CPU are steady-state values
sampled by each service's smoke test (docker stats). The slim image and
runtime values below come from the linux-arm64 manifest attached to each
service's latest release in this repository. Upstream ARM64 sizes are measured
from the matching upstream image tag; Pooler retains its documented comparison
override because its exact release tag is unavailable on Docker Hub.
| Service | Version | Upstream ARM64 | Published slim | Reduction | Idle RSS | Idle CPU | Sources |
|---|---|---|---|---|---|---|---|
| Postgres | 17.6.1.160 (all PG17 extensions, minimal preload) |
349.8 MiB |
132.7 MiB |
62.1% |
47.6 MiB |
0.01% |
release · report |
| PostgREST | v16.1 |
6.1 MiB |
5.9 MiB |
3.8% |
8.3 MiB |
0.07% |
release · report |
| Auth | v2.195.0 |
26.0 MiB |
11.5 MiB |
55.7% |
9.0 MiB |
0.01% |
release · report |
| Realtime | v2.125.0 |
116.6 MiB |
27.0 MiB |
76.8% |
176.0 MiB |
0.22% |
release · report |
| Storage | v1.69.0 |
223.4 MiB |
51.0 MiB |
77.2% |
218.4 MiB |
0.03% |
release · report |
| Edge Runtime | v1.74.3 (no-AI) |
360.6 MiB |
52.7 MiB |
85.4% |
18.1 MiB |
0.01% |
release · report |
| Studio | 2026.08.10-sha-5b68af1 |
306.5 MiB |
127.6 MiB |
58.4% |
195.6 MiB |
0.00% |
release · report |
| Analytics | v1.50.2 |
261.1 MiB |
58.5 MiB |
77.6% |
504.2 MiB |
0.36% |
release · report |
| PgMeta | v0.96.9 |
102.5 MiB |
55.9 MiB |
45.5% |
105.2 MiB |
0.23% |
release · report |
| Pooler | v2.9.10 |
289.4 MiB* |
39.3 MiB |
86.4%* |
163.5 MiB |
0.15% |
release · report |
* Upstream comparison uses UPSTREAM_COMPARE_IMAGE from the recipe (the exact tag is not published on Docker Hub), so the percentage is directional.
Studio is now in the native automatic release pipeline. It remains omitted from this release-backed snapshot until the first native Studio release publishes its measured manifests.
Postgres is native-first like everything else: the image is derived from the
portable artifact, which ships every extension the upstream PG17 image
supports (timescaledb/plv8 are PG17-incompatible upstream). Extensions are
installed but not enabled — only the minimal shared_preload_libraries set
is on by default, so the footprint numbers are unaffected; the few
preload-gated extensions (pgaudit, pg_stat_monitor, pg_tle) take a config
opt-in.
Every service in the release workflow ships a self-contained, relocatable
tar.zst archive per target (HOST_NATIVE_PLAN.md) that
the CLI can download to ~/.supabase/bin/<service>/<version>/ and run without
Docker — on macOS and on Linux (only the glibc family is assumed from a
Linux host; each Node service bundles its upstream-selected runtime inside the archive — the
wrapper prefers node/bin/node, no external runtime, runtime_requires is
null). The Linux Docker images are derived from these same artifacts. The
table below shows the darwin-arm64 values from the manifest attached to the
same published release used above. Idle RSS and Idle CPU are sampled from the
artifact running as a real host process with runtime.env applied (ps-based,
recorded in the manifest). Local rebuilds can preview table changes with
scripts/update-results-tables.sh --host-native-only (darwin) or --merge;
published release manifests remain the source of truth for this snapshot.
Linux archives are glibc artifacts with a measured, CI-gated host floor
(glibc >= 2.39: Ubuntu 24.04+, Debian 13+, Fedora 40+ — see CI_MATRIX.md);
macOS archives require macOS 14+. The manifest records the exact floor as
os_floor.
| Service | Version | Archive | rootfs | Idle RSS | Idle CPU | Portable | Sources |
|---|---|---|---|---|---|---|---|
| Postgres | 17.6.1.160 |
103.0 MiB |
662.6 MiB |
71.1 MiB |
0.00% |
yes | release · report |
| PostgREST | v16.1 |
12.5 MiB |
78.0 MiB |
55.9 MiB |
0.03% |
yes | release · report |
| Auth | v2.195.0 |
9.5 MiB |
33.8 MiB |
29.8 MiB |
0.00% |
yes | release · report |
| Realtime | v2.125.0 |
11.7 MiB |
47.8 MiB |
205.6 MiB |
0.50% |
yes | release · report |
| Storage | v1.69.0 |
36.1 MiB |
143.6 MiB |
288.4 MiB |
0.00% |
yes | release · report |
| Edge Runtime | v1.74.3 |
39.9 MiB |
161.4 MiB |
56.8 MiB |
0.00% |
yes | release · report |
| Studio | 2026.08.10-sha-5b68af1 |
75.3 MiB |
460.4 MiB |
303.2 MiB |
0.50% |
yes | release · report |
| Analytics | v1.50.2 |
33.4 MiB |
140.5 MiB |
520.5 MiB |
0.33% |
yes | release · report |
| PgMeta | v0.96.9 |
36.4 MiB |
169.3 MiB |
143.3 MiB |
0.30% |
yes | release · report |
| Pooler | v2.9.10 |
23.5 MiB |
52.4 MiB |
207.9 MiB |
0.10% |
yes | release · report |
See SLIM_IMAGES_REPORT.md for the global summary. Each service report is self-contained for distribution to the owning team. For Nix-backed native services, see NIX_PORTABLE_ARTIFACT_PLAYBOOK.md for the reusable artifact-to-image pattern learned from Edge Runtime. For CI target naming and commands, see CI_MATRIX.md.
Memory and CPU are first-class optimization targets, not just disk:
- Runtime profiles — each service has a
services/<service>/runtime.envwith low-footprint local-dev defaults, baked into the image as ENV and overridable atdocker run -e. The same KEY=VALUE files are applied as process environment for host-native (no-Docker) runs — the host-process smokes do exactly that, mirroring the CLI. Highlights:- BEAM services (realtime, analytics, pooler): one scheduler and no
scheduler busy-waiting (
+S 1:1 +sbwt none ...) — idle CPU drops from several percent to ≤0.5%. - Node services (storage, studio, pgmeta): V8 heap caps
(
--max-old-space-size). - Go services (auth):
GOMEMLIMIT,GOGC,GOMAXPROCS. - All DB clients: shrunk connection pools — every pooled connection holds a server-side postgres backend, so this also cuts postgres memory.
- Postgres: a conf overlay (
shared_buffers=32MB,jit=off, slowed idle ticks) via the stockinclude_dir;wal_level=logicaluntouched.
- BEAM services (realtime, analytics, pooler): one scheduler and no
scheduler busy-waiting (
- Measurement — every smoke samples steady-state RSS and idle CPU
(
record_runtime_metricsviadocker statsfor containers,record_host_runtime_metricsviapsover the process tree for host processes — both inscripts/smoke-lib.sh) and records them underruntimein the artifactmanifest.json, so regressions on these axes are visible per build, exactly like size. - The parallel-stacks view — image layers are shared; RSS multiplies per stack. A minimal core stack (postgres + auth + postgrest) idles at roughly 145 MiB, so 25 parallel stacks cost ~3.5 GiB. Analytics (~500 MiB) and Studio (~200 MiB) dominate when run per-stack and are disabled by default in the minimal stack.
.
├── scripts/ Shared artifact, image, measure, and smoke helpers
├── services/<service>/ Per-service recipes, Dockerfiles, smoke tests, reports
├── sources/<service>/ Upstream source repositories as pinned submodules
├── artifacts/ Generated rootfs outputs and optional archives, gitignored
└── SLIM_IMAGES_REPORT.md Global summary and cross-service lessons
The sources/ directory is treated as read-only. Any local build changes,
runtime wrappers, shims, or packaging helpers belong under services/<service>/
or shared scripts in this repo.
Every backend writes the same layout:
artifacts/<service>/<version>/<platform>-<arch>/
├── rootfs/
├── <service>.tar.zst Optional distribution archive
└── manifest.json
The expanded rootfs/ is the canonical artifact for local smoke tests,
inspection, and Docker image assembly. Compressed archives are derived
distribution products and may be generated separately from an existing rootfs.
The manifest records source ref (or pinned image digest), selected base image,
entrypoint, smoke command, artifact size, image size, and — after an image
smoke — steady-state runtime metrics (runtime.runtime_rss_mib,
runtime.idle_cpu_pct).
Native-first (HOST_NATIVE_PLAN.md): for every
Supabase-owned service the portable, relocatable artifact is the single
source of truth on every target, and the Docker image is derived from that
same rootfs. Each service has a services/<service>/recipe.env file; the
dispatcher reads ARTIFACT_BACKEND and chooses one of:
nix: build the portable rootfs from the repo-owned Nix package inservices/<service>/nix/(applied over the read-only submodule viaNIX_PACKAGE_OVERLAY). Used by the BEAM services (realtime, analytics, pooler) and edge-runtime. On Linux this runs local Nix when the host matches, or the service'sDockerfile.artifactnixos/nix builder otherwise (e.g. building Linux artifacts from macOS).docker-sourcewithARTIFACT_SOURCE_BUILD="host": build withservices/<service>/build-host.shon the host toolchain — Go cross-compiles (auth) and Node bundles (storage, pgmeta, Studio; these must run on a host matching the target because package managers resolve platform packages).docker-image: runDockerfile.artifactrooted at a published upstream image (FROM $SOURCE_IMAGE, pinned bySOURCE_IMAGE_DIGEST) — used when pruning the published image is the practical path (postgres).image: extract selected paths from a published image (postgrest — the extraction bundles the full ELF closure, so the result is still portable).
The final Dockerfile.slim files derive the image from the artifact: they
copy the prepared rootfs/ into the smallest proven runtime base and add
only entry wiring (busybox/tini/CA-bundle stages where a shell entrypoint is
needed). Images are always assembled through scripts/render-dockerfile.sh,
which appends the runtime.env profile as ENV — never build
Dockerfile.slim directly or the runtime profile is silently skipped.
Portable archive builds share two hardening steps. For Nix-backed portable artifacts, these checks should run inside the Nix package when practical; the scripts remain available as shared helpers and external verification.
scripts/portable-darwin-fixup.shcompletes macOS dylib closures, rewrites Nix store install names, removes Nix store rpaths, strips local Mach-O symbols, and ad-hoc signs the result.scripts/audit-portable-artifact.shfails artifacts that still have unresolved runtime dependencies or absolute Nix store references.
Archives prefer zstd -19 and are produced by scripts/archive-artifact.sh
when a distributable bundle is needed. The script uses Nix's zstd package
automatically when zstd is not on PATH.
Clone with submodules:
git clone --recursive git@github.com:supabase/slim-services.git
cd slim-servicesOr, after a normal clone:
git submodule update --init --recursiveBuild one service artifact (host-native for your machine):
TARGET_OS=darwin ARCH=arm64 scripts/build-artifact.sh auth v2.194.0
TARGET_OS=linux ARCH=arm64 scripts/build-artifact.sh realtime v2.121.2Build the derived slim image from a Linux artifact:
scripts/build-image-from-artifact.sh \
realtime \
artifacts/realtime/v2.121.2/linux-arm64/rootfs \
local/realtime:slim-v2.121.2-arm64Run the service smoke test against the image:
scripts/smoke.sh realtime --image local/realtime:slim-v2.121.2-arm64Or smoke an artifact rootfs. On a matching darwin host this runs the service
as a real host process (no Docker for the service); Linux artifacts smoke
through a temporary image by default, or as a host process on a matching
Linux host with SLIM_DIRECT_LINUX_ARTIFACT_SMOKE=1:
scripts/smoke.sh auth --artifact artifacts/auth/v2.194.0/darwin-arm64/rootfs
SLIM_DIRECT_LINUX_ARTIFACT_SMOKE=1 \
scripts/smoke.sh realtime --artifact artifacts/realtime/v2.121.2/linux-arm64/rootfsHosts without Docker (e.g. macOS CI runners) can run the harness postgres as
a host process too: SLIM_SMOKE_HOST_POSTGRES=1.
Run the full CI-style build for one service and matrix cell (build, portable audit, smoke, archive + SHA256SUMS; Linux additionally derives and smokes the Docker image):
TARGET_OS=darwin ARCH=arm64 scripts/ci-build-service.sh edge-runtime v1.74.2
TARGET_OS=linux ARCH=arm64 scripts/ci-build-service.sh realtime v2.121.2.github/workflows/service-release.yml publishes one upstream service release
at a time. It verifies that the requested version is an exact, stable,
published release tag in the configured upstream repository, checks out only
that tag (never a branch such as main), and builds the complete
linux-amd64, linux-arm64, and darwin-arm64 artifact matrix. Publication
only starts after every artifact and Docker smoke passes:
- Portable archives, platform manifests, and a combined
SHA256SUMSare attached to the GitHub release<service>-<version>. - The exact smoked Linux images are published as a multi-platform image at
ghcr.io/supabase/cli/<service>:<version>.
Run a release manually with:
gh workflow run service-release.yml \
-f service=auth \
-f version=v2.194.0 \
-f force=false.github/workflows/poll-service-releases.yml polls stable upstream GitHub
releases hourly and dispatches independent service-release runs for
the newest version tag matching each service policy that does not yet have a
corresponding GitHub release here. All configured services are enabled. The
PostgreSQL policy accepts only plain 17.x.x.x releases; PostgreSQL 15,
OrioleDB, architecture-specific, and other suffixed release tags are ignored.
After a successful release run, .github/workflows/release-results.yml
downloads the newest published manifest set for every service, regenerates the
two README tables, and merges the result through a short-lived docs pull
request. The shared concurrency group coalesces simultaneous service releases,
and the hourly reconciliation schedule repairs any missed or failed refresh.
It uses the repository's GitHub App credentials because the organization does
not allow the built-in Actions token to create pull requests.
The release workflow rechecks the upstream release policy independently, so a
manual dispatch cannot publish main, another branch, a draft/prerelease, or
an unsupported tag. A newly triggered build can still fail safely when a
service's version-specific dependency hashes need to be refreshed; no release
or image is published unless every build and smoke test passes.
Build the selected backend for a service:
scripts/build-artifact.sh <service> [version]Build a final slim image:
scripts/build-image-from-artifact.sh <service> <artifact-rootfs> [image-tag]Run smoke tests:
scripts/smoke.sh <service> --image <image-tag>
scripts/smoke.sh <service> --artifact <artifact-rootfs>Measure rootfs/archive/image sizes:
scripts/measure-artifact.sh <artifact-rootfs> [archive] [image-tag]Create a distribution archive from an existing rootfs:
scripts/archive-artifact.sh <artifact-rootfs> [archive-prefix]- Postgres: Nix reference-graph prune of the published image; every supported extension kept (31 smoke-verified, including PostGIS/pgroonga/wrappers), low-memory conf overlay.
- PostgREST: stable ARM64 dynamic bundle in
scratch; static upstream artifact path validated for a future stable release. - Studio: target-native Next/TanStack-aware build, bundled upstream-selected Node runtime, artifact-derived slim image, and Docker-tag release polling with provenance verification.
- Edge Runtime: adopted Nix/native artifact
pruning; local-dev default excludes ONNX/OpenBLAS (
withAi = false), the AI profile stays available upstream or viawithAi = true. - Analytics: adopted native stripping, sourcemap-gzip pruning, curl removal, and base-library dedupe.
- Realtime: adopted production-ready launcher aligned with upstream PR #1837 and base-library dedupe; Alpine experiment rejected.
- Pooler: adopted POSIX launcher, native stripping, and base-library dedupe.
- PgMeta: upstream TypeScript runtime with a bundled upstream-selected Node runtime; obsolete experimental variants are removed.
- Storage: adopted Rolldown emitted-JS bundle with minification and no dependency shims.
- Auth: static Go executable runs from
scratch; phase 2 repeats phase 1 because further binary-level experiments are not worth carrying.
- Upstream submodules stay read-only.
- Service-specific Nix changes live in this repo, not in
sources/. - Prefer
scratchwhen the artifact proves it can run there. - Prefer Distroless Debian 13 for glibc services.
- Avoid Alpine unless musl is validated and wins.
- Keep optimizations maintainable; do not carry a phase 2 variant for a tiny compressed gain.
- Record rejected experiments. Knowing what is not worth doing is part of the asset.
Four passes are complete: base-image/artifact slimming (pass 1),
service-specific pruning (pass 2), the runtime-footprint pass (pass 3 —
latest versions, postgres onboarding, runtime.env profiles, RSS/CPU
measurement in every smoke), and the host-native pass (pass 4 —
HOST_NATIVE_PLAN.md). The release workflow has completed
the full darwin-arm64, linux-arm64, and linux-amd64 build-and-smoke matrix
for all nine published services, attaching the portable archives to GitHub
Releases and pushing the exact tested Linux images to GHCR. The legacy
.github/workflows/service-artifacts.yml remains for experiments and services
outside the release set. CLI-side integration (download/verify and
process-compose wiring) is the next step, tracked in
SLIM_IMAGES_REPORT.md § Remaining Work and the plan's
Phase 5 notes.
The packaging code in this repository is licensed under the MIT License.
Built artifacts contain independently licensed upstream
software. See THIRD_PARTY_NOTICES.md; each archive
also carries the license material collected from its dependency closure under
share/licenses/ and is published with an SPDX SBOM.