Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/local-binary-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,52 @@ jobs:
subject-name: ${{ steps.tags.outputs.image }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

# Points the flake's `maple` package (nix/release.json) at the published bundles.
# Runs after every platform finished, even if one failed: the script leaves out a
# platform whose bundle never made it to the release. Pre-releases keep the pin.
nix-release:
name: Nix release pin
needs: [preflight, build]
if: ${{ always() && needs.preflight.outputs.publish == 'true' && !contains(needs.preflight.outputs.version, '-') }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
actions: write # dispatch the Nix check on the pin branch
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
ref: main

- name: Open the pin PR
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
# Never pin a re-run of an older tag over a newer one.
newest="$(git ls-remote --tags --refs origin 'v*' | sed 's|.*refs/tags/||' | { grep -ve '-' || true; } | sort -V | tail -1)"
if [ "$TAG" != "$newest" ]; then
echo "::notice::$TAG is not the newest stable tag ($newest); keeping the pin."
exit 0
fi
scripts/update-nix-release.sh "$TAG"
if git diff --quiet -- nix/release.json; then
echo "::notice::nix/release.json already pins $TAG."
exit 0
fi
branch="nix/release-$TAG"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -c "$branch"
git commit -m "chore(nix): pin maple $TAG" -- nix/release.json
git push --force origin "$branch"
if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then
gh pr create --base main --head "$branch" \
--title "chore(nix): pin maple $TAG" \
--body "Points the flake's \`maple\` package at the $TAG bundles (\`scripts/update-nix-release.sh $TAG\`)."
fi
# A push made with this token starts no workflows, so run the check explicitly.
gh workflow run nix.yml --ref "$branch"
50 changes: 50 additions & 0 deletions .github/workflows/nix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Nix

# Builds the flake's `maple` package and runs the NixOS VM test (nix/test.nix):
# the service starts, enforces its secrets, ingests a span and serves it back.

on:
pull_request:
paths:
- flake.nix
- flake.lock
- nix/**
- .github/workflows/nix.yml
push:
branches: [main]
paths:
- flake.nix
- flake.lock
- nix/**
- .github/workflows/nix.yml
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false

# The VM test needs KVM, which the runner has but does not expose to users.
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- uses: cachix/install-nix-action@448cde739abec1eaf0646249e3d25bb8d9bc7702 # v31.11.2
with:
extra_nix_config: system-features = nixos-test benchmark big-parallel kvm

- name: Check the flake
run: nix flake check -L --show-trace
1 change: 1 addition & 0 deletions apps/landing/src/content/docs/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,6 @@ This serves plain HTTP on every interface, which is fine on a private network. O
- [Deploy](/docs/self-hosting/deploy): Docker Compose, container platforms, and TLS with a reverse proxy.
- [Render](/docs/self-hosting/platforms/render): one-click deploy from a blueprint.
- [Railway](/docs/self-hosting/platforms/railway): one-click deploy from a template.
- [NixOS](/docs/self-hosting/platforms/nixos): a NixOS module from the Maple flake.
- [Connect](/docs/self-hosting/connect): send telemetry, sign in, and query from the CLI.
- [Operations](/docs/self-hosting/operations): backups, upgrades, crash recovery and rotating secrets.
76 changes: 76 additions & 0 deletions apps/landing/src/content/docs/self-hosting/platforms/nixos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: "NixOS"
description: "Run self-hosted Maple as a NixOS service from the flake in the Maple repository, with its secrets read from files."
group: "Platforms"
order: 3
---

The Maple repository is a Nix flake. It packages the released `maple` binary and ships a NixOS module, `services.maple`, which runs the same server as the [container image](/docs/self-hosting) under systemd.

## Add the module

In your system flake:

```nix
{
inputs.maple.url = "github:MapleTechLabs/maple";

outputs = { nixpkgs, maple, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
maple.nixosModules.maple
./configuration.nix
];
};
};
}
```

Then in `configuration.nix`:

```nix
services.maple = {
enable = true;
host = "127.0.0.1";
ingestKeyFile = "/run/secrets/maple-ingest-key";
accessTokenFile = "/run/secrets/maple-access-token";
};

services.caddy = {
enable = true;
virtualHosts."maple.example.com".extraConfig = "reverse_proxy localhost:4318";
};
```

Each secret file holds one of the [two secrets](/docs/self-hosting#the-two-secrets), at least 16 characters. Create them with `openssl rand -hex 32`, or manage them with sops-nix or agenix. The service reads them through systemd credentials, so they never enter the Nix store. Listening on anything other than loopback requires both files.

## Options

| Option | Default | Meaning |
| -------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
| `host` | `127.0.0.1` | Address to listen on. Use `0.0.0.0` without a proxy on a private network |
| `port` | `4318` | OTLP ingest, the dashboard and the query API |
| `advertiseHost` | none | The hostname exporters and browsers use, when it differs from `host` |
| `openFirewall` | `false` | Open `port` in the firewall |
| `ingestKeyFile` | none | File holding the ingest key |
| `accessTokenFile` | none | File holding the access token |
| `checkpointInterval` | `30m` | How often to refresh the [recovery checkpoint](/docs/self-hosting/operations), or `off` |
| `onDirtyStore` | automatic | After an unclean stop: restore the last checkpoint once one exists, start fresh before that (as the image) |
| `extraArgs` | `[]` | Extra arguments for `maple start` |

Telemetry and checkpoints live in `/var/lib/maple`, owned by the `maple` system user. Back it up as described in [Operations](/docs/self-hosting/operations), and run commands that change the store as that user:

```bash
sudo -u maple HOME=/var/lib/maple maple checkpoint
```

Read-only commands such as `maple services` can run as any user with `MAPLE_LOCAL_URL=http://127.0.0.1:4318` and `MAPLE_LOCAL_ACCESS_TOKEN` set.

## Upgrades

The flake follows Maple releases. Run `nix flake update maple` and rebuild. `maple update` is disabled in a Nix install. Roll back with your previous generation like any other NixOS change, but note that a store opened by a newer version may refuse to start on an older one.

## Without NixOS

`nix run github:MapleTechLabs/maple -- start` runs Maple Local on any Linux machine with Nix, and on Apple Silicon macOS when the pinned release has a bundle for it. Maple's license is the [FSL](https://fsl.software), which nixpkgs counts as unfree: the flake's own package builds without any setting, but the `overlays.default` overlay needs `maple` allowed in your `allowUnfreePredicate`.
17 changes: 17 additions & 0 deletions docs/local-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,23 @@ MAPLE_TARGET=linux-arm64 scripts/build-local-binary.sh # cross-compile (linux-
apps/cli/docker/build-image.sh maple-local:dev && apps/cli/docker/image-smoke.sh maple-local:dev
```

## Nix flake

`flake.nix` packages the released bundle (`nix/package.nix`, pinned by
`nix/release.json`) and exports `nixosModules.maple` (`nix/module.nix`), the
systemd counterpart of the container entrypoint: a static `maple` user,
secrets from files via `LoadCredential`, `HOME=/var/lib/maple`, `--offline`, the same dirty-store
policy and `RestartPreventExitStatus=78`. The package wraps `maple` with
`MAPLE_NO_UPDATE_CHECK=1` and refuses `maple update`;
autoPatchelf keeps Bun's appended payload intact, but stripping would not, so
`dontStrip` stays. User docs: `apps/landing/src/content/docs/self-hosting/platforms/nixos.md`.

- `nix/test.nix` is a NixOS VM test (ingest key, access token, a span through
OTLP and back through the CLI, a restart). The `Nix` workflow runs
`nix flake check` on changes to the flake.
- The release workflow's `nix-release` job runs `scripts/update-nix-release.sh <tag>`
after a stable release and opens a PR with the new pin. Run it by hand to re-pin.

## Release signing

A `.sha256` published next to a bundle only proves the download is intact: the
Expand Down
27 changes: 27 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
description = "Maple: self-hosted OpenTelemetry ingest, storage and dashboard (Maple Local)";

inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

outputs =
{ self, nixpkgs }:
let
lib = nixpkgs.lib;
release = lib.importJSON ./nix/release.json;
# Platforms the pinned release has a bundle for, among those nixpkgs still
# supports (unstable dropped x86_64-darwin in 26.11).
systems = lib.intersectLists [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
] (builtins.attrNames release.assets);
linuxSystems = lib.filter (lib.hasSuffix "-linux") systems;
forAll = list: f: lib.genAttrs list (system: f system);
# Maple is FSL-1.1-ALv2, which nixpkgs counts as unfree; building it from
# this flake should not require a global allowUnfree.
mapleFor = pkgs: pkgs.callPackage ./nix/package.nix { };
pkgsFor =
system:
import nixpkgs {
inherit system;
config.allowUnfreePredicate = pkg: lib.getName pkg == "maple";
};
in
{
packages = forAll systems (
system:
let
maple = mapleFor (pkgsFor system);
in
{
inherit maple;
default = maple;
}
);

# Builds from the consumer's nixpkgs, so it needs maple allowed there:
# nixpkgs.config.allowUnfreePredicate = p: lib.getName p == "maple";
overlays.default = final: _prev: { maple = mapleFor final; };

nixosModules.maple = import ./nix/module.nix {
defaultPackage = pkgs: self.packages.${pkgs.stdenv.hostPlatform.system}.maple;
};
nixosModules.default = self.nixosModules.maple;

checks = forAll linuxSystems (system: {
package = self.packages.${system}.maple;
nixos = import ./nix/test.nix {
pkgs = pkgsFor system;
module = self.nixosModules.maple;
};
});

formatter = forAll systems (system: (pkgsFor system).nixfmt);
};
}
Loading
Loading