English · 中文
This guide brings up the mega2 (trunk / storage-only) evaluation stack locally with the repository-root compose file for your platform — macos-orbstack-mega2-compose.yml on macOS with OrbStack, linux-mega2-compose.yml on Linux Docker — and runs the first closed loop: HTTP clone → push to main → read back over the API. The stack pulls the official release image from Docker Hub (genedna/mega2:latest) — no source build, no bootstrap, no token. Product rules: monorepo.md.
- macOS: OrbStack (the macOS compose file relies on its
*.orb.localDNS). Linux: Docker Engine; the Linux compose file uses host networking for mega2 instead. Either way: the Compose plugin (v2),git,curl; run everything from the repository root. - The first
uppulls thegenedna/mega2:latest, PostgreSQL, Redis, RustFS, and RustFS CLI images; duration depends on your network.
# Pick the compose file for your platform:
COMPOSE=macos-orbstack-mega2-compose.yml # macOS with OrbStack
COMPOSE=linux-mega2-compose.yml # Linux Docker
docker compose -f $COMPOSE up -d --waitNo initialization command is needed after up: rustfs-init creates the mega2 bucket automatically, and mega2 initializes the empty Monorepo (main and the top-level directories) during service startup. The readiness probe is /api/openapi.json.
This is a local-only, anonymous setup: the stack runs with
push_auth=none, so clone / fetch / push need no credentials, and port 9000 is bound to127.0.0.1for that reason. Do not rebind it to0.0.0.0or expose it through a reverse proxy; shared or internet-facing deployments must use token authentication instead — seedeployment.mdanddeploy-trunk.md.
In storage-only mode you clone a subpath (do not root-clone /, see deploy-trunk.md §9):
git clone http://127.0.0.1:9000/project
cd projectmain is the only public branch of the monorepo. With the anonymous setup the push needs no credentials:
echo "# hello mega2" > hello.md
git add hello.md && git commit -m "add hello.md"
git push origin mainYou can only push to main (pushes of other branches are rejected); Git-client tag pushes are rejected too — create, list, and delete tags via the HTTP API or libra mega2 browser (monorepo.md).
# Read back the file you just pushed; this should print the contents of hello.md
curl -fsS "http://127.0.0.1:9000/api/v1/blob?path=/project/hello.md"Browse the full HTTP surface (Git smart HTTP, LFS, product writes, tags, optional OCI /v2) in Swagger UI: http://127.0.0.1:9000/swagger-ui (OpenAPI JSON: /api/openapi.json). For interactive terminal browsing use Libra's libra mega2 browser; mega2 itself serves no Web UI.
You don't need to create directories in advance in the monorepo. Just build a multi-level directory inside your /project clone and push — both rust-lang/ and rust-lang/crate/ are written by that push:
git clone http://127.0.0.1:9000/project
cd project
mkdir -p rust-lang/crate
echo "# crate" > rust-lang/crate/README.md
git add . && git commit -m "add rust-lang/crate"
git push origin mainAfter the push, the nested path is a subpath you can clone / push independently, and you can read it back over the API:
# Read back the file under the nested path
curl -fsS "http://127.0.0.1:9000/api/v1/blob?path=/project/rust-lang/crate/README.md"
# Clone the subpath (never root-clone /)
git clone http://127.0.0.1:9000/project/rust-lang/crateTo migrate an existing repository (keeping its branches and tags), use an ImportRepo under /third-party: repos there follow ordinary Git semantics — multi-branch and Git-client tags are both allowed (monorepo.md). If the target path does not exist yet, it is created automatically by the push; no prior setup is needed:
cd /path/to/your-repo
git remote add mega2 http://127.0.0.1:9000/third-party/your-repo
git push mega2 --all # push all branches
git push mega2 --tags # push all tagsThen clone / fetch as usual:
git clone http://127.0.0.1:9000/third-party/your-repoDo not confuse the two path semantics:
/third-party/**(ImportRepo): multi-branch and client tags allowed — suited for hosting third-party dependency sources and migrating existing repositories.- Everywhere else (Monorepo):
mainis the only public branch and Git-client tags are forbidden. An existing repo's multi-branch history cannot be pushed straight into a monorepo subpath; the rules are inmonorepo.md.
A full mirror of an existing GitHub repo — every branch and tag — takes one push. Using https://github.com/brewfs/brewfs as the example:
git clone --mirror https://github.com/brewfs/brewfs.git
cd brewfs.git
git lfs fetch --all origin # best effort; one historic object is 404 on GitHub
git config lfs.allowincompletepush true # push the LFS objects that are still available
git remote add mega2 http://127.0.0.1:9000/third-party/brewfs
git push --mirror mega2Two things worth knowing:
--mirrorpushes every ref (all branches + all tags). Thegit push mega2 --allin the previous case only pushes local branches, which in a normal clone is usually justmain.- brewfs tracks large test fixtures with Git LFS. One object referenced by old history no longer exists on GitHub, so
git lfs fetch --allprints a 404 error — expected;lfs.allowincompletepush truelets the push proceed without it.
Verify the round trip — annotated tags and LFS content both survive:
git clone http://127.0.0.1:9000/third-party/brewfs /tmp/verify-brewfs
cd /tmp/verify-brewfs
git for-each-ref refs/tags # v0.0.1 / v0.1.1 / v0.1.2
git cat-file -t v0.1.2^{tag} # => tag (annotated tag object intact)
ls -la tests/scripts/xfstests-prebuilt/xfstests-prebuilt.tar.gz
# ~8 MB gzip — the real bytes came back from mega2's LFS object store, not a pointerThe stack speaks standard Git LFS (/info/lfs), so a stock git-lfs client works as-is. LFS write authorization shares git.push_auth with Git push — anonymous in this evaluation setup. Prerequisite: git-lfs on the host (check with git lfs version).
git clone http://127.0.0.1:9000/project
cd project
git lfs install --local # once per clone
mkdir -p lfs-demo && cd lfs-demo
git lfs track "*.bin" # writes .gitattributes
head -c 2097152 /dev/urandom > big.bin # a 2 MiB binary
git add .gitattributes big.bin
git commit -m "add LFS-tracked big.bin"
git push origin main # "Uploading LFS objects: 100% (1/1)" runs firstThe Git commit stores only an LFS pointer; the 2 MiB of bytes went to the object store during the push. Verify from a fresh clone of the subpath — checkout downloads the real content:
git clone http://127.0.0.1:9000/project/lfs-demo /tmp/verify-lfs
cmp lfs-demo/big.bin /tmp/verify-lfs/big.bin && echo identicalLFS objects live in the same object storage as Git blobs (RustFS in this stack), so everything said about volumes, down -v, and the persist override below covers them too.
The stack embeds an OCI Distribution registry at the same port under /v2 (enabled in the Compose files via MEGA_OCI__ENABLED=true; it also requires the storage-only morphology, which MEGA_GIT__PUSH_AUTH already implies). Any OCI client works — Docker treats 127.0.0.1 as an insecure registry by default, so plain HTTP is fine for this local evaluation:
docker pull alpine:3.21
docker tag alpine:3.21 127.0.0.1:9000/project/alpine:quickstart
docker push 127.0.0.1:9000/project/alpine:quickstart
# Read back: wipe the local tags and pull from mega2
docker rmi 127.0.0.1:9000/project/alpine:quickstart alpine:3.21
docker pull 127.0.0.1:9000/project/alpine:quickstart
docker run --rm 127.0.0.1:9000/project/alpine:quickstart cat /etc/alpine-release
# Or query the registry API directly
curl -s http://127.0.0.1:9000/v2/project/alpine/tags/listRegistry writes share git.push_auth — anonymous in this evaluation setup. Blobs and manifests land in the same RustFS object storage as everything else. When you expose the registry to other machines, plain HTTP is no longer accepted by default Docker clients: put TLS in front of it, or add the host to the client's insecure-registries.
Compiled binaries, release tarballs and other build outputs are stored as Artifact Sets under /api/v1/repos/{repo}/artifacts. The flow is discovery → batch → commit, and the byte transfer is presigned: mega2 only handles metadata and signs time-limited (1 h) S3 URLs — the client uploads/downloads the bytes directly to/from the object storage (RustFS here), without proxying through mega2.
REPO=project # single URL path segment; use %2F for "/" inside names
BASE=http://127.0.0.1:9000/api/v1/repos/$REPO/artifacts
# 0. Discovery: protocol version, limits, supported transfers
curl -s $BASE/discovery
# 1. Stage two release files; each object id is a client-generated UUID
head -c 1048576 /dev/urandom > app-1.0.0.tar.gz
shasum -a 256 app-1.0.0.tar.gz > SHA256SUMS
OID1=$(uuidgen | tr 'A-Z' 'a-z'); OID2=$(uuidgen | tr 'A-Z' 'a-z')
# 2. Batch: declare the objects, get a presigned PUT URL per object
curl -s -X POST -H 'Content-Type: application/json' $BASE/batch --data @- <<EOF
{"namespace": "releases", "object_type": "snapshot", "intent": "upload",
"objects": [
{"path": "app-1.0.0.tar.gz", "oid": "$OID1", "size": $(wc -c < app-1.0.0.tar.gz | tr -d ' '), "content_type": "application/gzip"},
{"path": "SHA256SUMS", "oid": "$OID2", "size": $(wc -c < SHA256SUMS | tr -d ' '), "content_type": "text/plain"}],
"metadata": {"run_id": "quickstart-demo"}}
EOF
# → {"transfer":"basic","objects":[{"oid":"...","exists":false,
# "actions":{"upload":{"href":"http://<rustfs>/...?X-Amz-Signature=...",
# "header":{"Content-Type":"..."},"expires_at":"..."}}}], ...}
# 3. Upload the bytes STRAIGHT to the object storage (not to mega2).
# Send the headers returned in actions.upload.header:
curl -X PUT -H 'Content-Type: application/gzip' --data-binary @app-1.0.0.tar.gz "<href for OID1>"
curl -X PUT -H 'Content-Type: text/plain' --data-binary @SHA256SUMS "<href for OID2>"
# 4. Commit: register the uploaded objects as one Artifact Set
curl -s -X POST -H 'Content-Type: application/json' $BASE/commit --data @- <<EOF
{"namespace": "releases", "object_type": "snapshot",
"files": [
{"path": "app-1.0.0.tar.gz", "oid": "$OID1", "size": $(wc -c < app-1.0.0.tar.gz | tr -d ' ')},
{"path": "SHA256SUMS", "oid": "$OID2", "size": $(wc -c < SHA256SUMS | tr -d ' ')}],
"metadata": {"run_id": "quickstart-demo"}}
EOF
# → {"artifact_set_id":"...","status":"ok","missing_objects":[]}missing_objects must be empty — any object listed there was not found in the object storage and the commit recorded only the rest. Read the set back; downloads are presigned too, so curl -L follows the 302 straight to RustFS:
# List sets / resolve a file to its object id
curl -s "$BASE/sets?namespace=releases&object_type=snapshot"
curl -s "$BASE/resolve-file?namespace=releases&object_type=snapshot&path=app-1.0.0.tar.gz"
# Download: 302 redirect to a presigned GET, or JSON with the link (?mode=link)
curl -L -o dl.tar.gz "$BASE/objects/$OID1"
cmp app-1.0.0.tar.gz dl.tar.gz && echo identical
curl -s "$BASE/objects/$OID2?mode=link"Artifact writes share the git.push_auth gate (anonymous here). The object_type vocabulary (snapshot, provenance, run, …) and the negotiated limits are advertised by the discovery payload.
# Follow the mega2 logs
docker compose -f $COMPOSE logs -f mega2
# Stop; the named volumes keep the Postgres / Redis / RustFS / mega2 data
docker compose -f $COMPOSE down
# Also delete the data volumes (destructive; wipes this local evaluation instance)
docker compose -f $COMPOSE down -vThe default stack uses Docker named volumes, so down -v deletes your data together with the volumes. If you plan to use this instance long-term and want the data to outlive the stack itself, switch the database, object storage, and mega2 data directories to bind mounts on the host — then even if the containers and volumes are deleted, the data stays in the local directory and is picked up again on the next up.
Create an override file mega2-compose.persist.yml in the repository root:
# Layered on top of your chosen compose file: replace the named volumes with host bind mounts
services:
postgres: # metadata database
volumes:
- ./mega2-data/postgres:/var/lib/postgresql
redis: # cache / queues
volumes:
- ./mega2-data/redis:/data
rustfs: # object storage (Git blobs / LFS)
volumes:
- ./mega2-data/rustfs:/data
mega2: # mega2 data directory (MEGA_BASE_DIR)
volumes:
- ./mega2-data/mega2:/var/lib/mega2Start with both -f flags (use the same two -f flags for stop and cleanup commands):
docker compose -f $COMPOSE -f mega2-compose.persist.yml up -d --waitFrom then on all data lives under ./mega2-data/:
docker compose ... down -vonly removes named volumes — it does not touch./mega2-data/; the nextupresumes from that directory with repositories, push history, and LFS objects intact.- Backup = stop the stack and archive
./mega2-data/. - The directory is written by in-container processes (owned by container users such as postgres); do not commit it to version control and do not edit its contents by hand.
The repository ships two more Compose files, both aimed at testing and development — neither replaces this evaluation stack:
docker/docker-compose-storage-only.yml(plus thedocker/docker-compose-storage-only.auth-none.ymloverride): a trunk lab stack that builds the image from source, uses token authentication, and needs aservice initbootstrap — used for deployment rehearsals and smoke tests. Usage:deployment.mdanddeploy-trunk.md§8.docker/docker-compose.test.yml: the integration-test (IT) data plane, seedevelopment.md.
- Day-to-day usage (clone / push / API / tags / LFS):
user-guide.md - Config keys, Profile, SecretRef, hot reload:
configuration.md; commented sample atconfig/config.toml - Deployment and operations (token management, morphology invariants, OCI, Agent Capture):
deployment.md,deploy-trunk.md - Local development and tests:
development.md