|
| 1 | +# Deployment |
| 2 | + |
| 3 | +[← Back to README](../README.md) |
| 4 | + |
| 5 | +This page covers installing and running Meridian beyond `npm install -g`: NixOS/Nix flakes, the Home Manager service, and Docker. |
| 6 | + |
| 7 | +## NixOS / Nix Flake |
| 8 | +Meridian provides a Nix flake for declarative installation. |
| 9 | + |
| 10 | +**Add to your flake inputs:** |
| 11 | + |
| 12 | +```nix |
| 13 | +{ |
| 14 | + inputs.meridian.url = "github:rynfar/meridian"; |
| 15 | +} |
| 16 | +``` |
| 17 | + |
| 18 | +**Install the package** (via overlay or directly): |
| 19 | + |
| 20 | +```nix |
| 21 | +# Option A: overlay |
| 22 | +nixpkgs.overlays = [ meridian.overlays.default ]; |
| 23 | +environment.systemPackages = [ pkgs.meridian ]; |
| 24 | +
|
| 25 | +# Option B: direct reference |
| 26 | +environment.systemPackages = [ meridian.packages.${system}.meridian ]; |
| 27 | +``` |
| 28 | + |
| 29 | +**OpenCode plugin** -- the plugin file is included at `${pkgs.meridian}/lib/meridian/plugin/meridian.ts`. Since this path lives in the Nix store, you need to make it available to OpenCode: |
| 30 | + |
| 31 | +If you generate your OpenCode config from Nix (e.g. via Home Manager), interpolate the path directly: |
| 32 | + |
| 33 | +```nix |
| 34 | +# home-manager example |
| 35 | +xdg.configFile."opencode/opencode.json".text = builtins.toJSON { |
| 36 | + plugin = [ "${pkgs.meridian}/lib/meridian/plugin/meridian.ts" ]; |
| 37 | +}; |
| 38 | +``` |
| 39 | + |
| 40 | +If you don't manage your OpenCode config through Nix, symlink the plugin to a stable path and reference that instead: |
| 41 | + |
| 42 | +```nix |
| 43 | +# configuration.nix or home-manager |
| 44 | +environment.etc."meridian/plugin/meridian.ts".source = |
| 45 | + "${pkgs.meridian}/lib/meridian/plugin/meridian.ts"; |
| 46 | +``` |
| 47 | + |
| 48 | +Then in `~/.config/opencode/opencode.json`: |
| 49 | + |
| 50 | +```json |
| 51 | +{ "plugin": ["/etc/meridian/plugin/meridian.ts"] } |
| 52 | +``` |
| 53 | + |
| 54 | +> **Important:** Do not use `meridian setup` on NixOS. It writes an absolute Nix store path (e.g. `/nix/store/...-meridian-1.x.x/lib/...`) into your OpenCode config, which will break on the next `nixos-rebuild switch` or `home-manager switch` when the store path changes. Use one of the approaches above instead. |
| 55 | +
|
| 56 | +> **Note:** Meridian's package depends on the unfree `claude-code` from nixpkgs instead of bundling its own binary. The flake accepts the unfree license when it builds the package and exports the finished derivation, so consuming it through the overlay or `packages.<system>.meridian` does not re-run nixpkgs' unfree check and needs no `allowUnfree` setting. |
| 57 | +
|
| 58 | +**Home Manager service** -- run Meridian as a user systemd service: |
| 59 | + |
| 60 | +```nix |
| 61 | +# flake.nix |
| 62 | +{ |
| 63 | + inputs.meridian.url = "github:rynfar/meridian"; |
| 64 | +} |
| 65 | +
|
| 66 | +# home-manager config |
| 67 | +{ |
| 68 | + imports = [ meridian.homeModules.default ]; |
| 69 | +
|
| 70 | + services.meridian = { |
| 71 | + enable = true; |
| 72 | + settings = { |
| 73 | + port = 3456; |
| 74 | + host = "127.0.0.1"; |
| 75 | + # passthrough = true; |
| 76 | + # defaultAgent = "opencode"; |
| 77 | + # sonnetModel = "sonnet"; |
| 78 | + # Load plugins from the Nix store (rendered to a plugins.json manifest). |
| 79 | + # The official scrub plugins ship prebuilt via the meridian overlay: |
| 80 | + # pluginConfig = [ { path = pkgs.meridianPlugins.opencode-scrub.path; } ]; |
| 81 | + # pluginDir = "/path/to/extra/plugins"; |
| 82 | + }; |
| 83 | + # Extra env vars not covered by settings |
| 84 | + # environment = { |
| 85 | + # MERIDIAN_MAX_CONCURRENT = "20"; |
| 86 | + # }; |
| 87 | + }; |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +The service starts automatically on login. Manage it with `systemctl --user {start,stop,restart,status} meridian`. |
| 92 | + |
| 93 | +The module manages only the systemd user service — it does **not** put the `meridian` CLI on your `$PATH`. If you also want to run `meridian` from a shell, add the package yourself: |
| 94 | + |
| 95 | +```nix |
| 96 | +home.packages = [ config.services.meridian.package ]; |
| 97 | +``` |
| 98 | + |
| 99 | +The plugin path is also available as `config.services.meridian.opencode.pluginPath` for use in your OpenCode config: |
| 100 | + |
| 101 | +```nix |
| 102 | +xdg.configFile."opencode/opencode.json".text = builtins.toJSON { |
| 103 | + plugin = [ config.services.meridian.opencode.pluginPath ]; |
| 104 | +}; |
| 105 | +``` |
| 106 | +## Docker |
| 107 | + |
| 108 | +Claude Code authentication requires a browser, which isn't available inside containers. Authenticate on your local machine first, then mount the credentials into Docker. |
| 109 | + |
| 110 | +### Single account |
| 111 | + |
| 112 | +```bash |
| 113 | +# 1. Authenticate locally (one time) |
| 114 | +claude login |
| 115 | + |
| 116 | +# 2. Run with mounted credentials |
| 117 | +docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian |
| 118 | +``` |
| 119 | + |
| 120 | +Meridian refreshes OAuth tokens automatically — once the credentials are mounted, no further browser access is needed. |
| 121 | + |
| 122 | +> **macOS hosts:** mounting `~/.claude` does **not** carry credentials into the container — on macOS the CLI stores OAuth tokens in the Keychain, not in files, so the container sees an empty credential store and requests fail with an authentication error. Use an [OAuth-token profile](#oauth-token-profiles-in-docker-no-volume-mount) instead (recommended), or run `claude login` once inside the container (`docker exec -it <name> claude login`). |
| 123 | +
|
| 124 | +### Multiple profiles in Docker |
| 125 | + |
| 126 | +Authenticate each profile locally, then pass them to Docker via the `MERIDIAN_PROFILES` environment variable: |
| 127 | + |
| 128 | +```bash |
| 129 | +# 1. Authenticate each account locally |
| 130 | +meridian profile add personal |
| 131 | +meridian profile add work # sign out of claude.ai first, sign into work account |
| 132 | + |
| 133 | +# 2. Run Docker with profile configs pointing to mounted credential directories |
| 134 | +docker run \ |
| 135 | + -v ~/.config/meridian/profiles/personal:/profiles/personal \ |
| 136 | + -v ~/.config/meridian/profiles/work:/profiles/work \ |
| 137 | + -e 'MERIDIAN_PROFILES=[{"id":"personal","claudeConfigDir":"/profiles/personal"},{"id":"work","claudeConfigDir":"/profiles/work"}]' \ |
| 138 | + -e MERIDIAN_DEFAULT_PROFILE=personal \ |
| 139 | + -p 3456:3456 meridian |
| 140 | +``` |
| 141 | + |
| 142 | +Switch profiles at runtime via the `x-meridian-profile` header or `meridian profile switch` (see [Multi-Profile Support](profiles.md)). |
| 143 | + |
| 144 | +### OAuth-token profiles in Docker (no volume mount) |
| 145 | + |
| 146 | +If you'd rather not mount a credential directory, generate a long-lived OAuth token on the host with `claude setup-token` and pass it as a profile. There's nothing to mount — the token alone is the credential: |
| 147 | + |
| 148 | +```bash |
| 149 | +docker run \ |
| 150 | + -e 'MERIDIAN_PROFILES=[{"id":"ci","oauthToken":"sk-ant-oat01-..."}]' \ |
| 151 | + -e MERIDIAN_DEFAULT_PROFILE=ci \ |
| 152 | + -p 3456:3456 meridian |
| 153 | +``` |
| 154 | + |
| 155 | +This is the recommended path for CI runners, ephemeral containers, and cross-host deployments where browser-based login isn't reachable. Treat the token like any other secret — inject it via your platform's secret store rather than committing it to your image or compose file. |
0 commit comments