systemd and the Linux packages¶
The .deb and .rpm on the releases page install the server as a systemd service. Installation has the install commands; this page is what comes after. The unit confines the process, which is defence in depth: authentication stays the boundary, and the security model says what an authenticated caller then reaches.
What the package installed¶
| Path | What it is | Owner |
|---|---|---|
/opt/markdown-vault-mcp/venv |
The server, installed from PyPI at the package's own version (a release candidate installs the wheel attached to its GitHub release) | root |
/usr/lib/systemd/system/markdown-vault-mcp.service |
The unit | root |
/etc/markdown-vault-mcp/env.example |
Every variable, commented out, generated from the server's configuration surface; replaced by an upgrade | root |
/etc/markdown-vault-mcp/env |
Your configuration, copied from env.example on first install, readable by root only (600), never touched by an upgrade |
root |
/var/lib/markdown-vault-mcp |
The state directory, the one path the service may write | markdown-vault-mcp:markdown-vault-mcp |
The package also creates the markdown-vault-mcp system user and group the service runs as, with /var/lib/markdown-vault-mcp as home and no login shell.
Configure¶
Edit /etc/markdown-vault-mcp/env. It holds deviations from the defaults only: every line arrives commented out, and the server starts on its defaults when nothing is set. Remove the # from the lines you change. The unit reads the file at start (EnvironmentFile=), so an edit needs a restart to take effect. Secrets belong here and nowhere else; the file is already mode 600.
# /etc/markdown-vault-mcp/env
MARKDOWN_VAULT_MCP_SERVER_NAME=notes
MARKDOWN_VAULT_MCP_LOG_LEVEL=INFO
The unit starts markdown-vault-mcp serve --transport http, so the server listens on MARKDOWN_VAULT_MCP_HOST and MARKDOWN_VAULT_MCP_PORT, 127.0.0.1:8000 unless set. To serve other machines, configure authentication first and put a reverse proxy in front; the configuration reference lists every variable.
Start and watch¶
The package installs the unit but does not enable it; starting on boot is your decision:
sudo systemctl enable --now markdown-vault-mcp
systemctl status markdown-vault-mcp
journalctl -u markdown-vault-mcp -f
sudo systemctl restart markdown-vault-mcp # after editing the env file
Under journald each record is one JSON object: the unit sets no log format, so the server picks its JSON renderer because journald is not a terminal. MARKDOWN_VAULT_MCP_LOG_FORMAT=rich in the env file switches to the coloured renderer. Restart=on-failure brings a crashed server back after five seconds.
What the unit allows¶
The unit runs the server as the markdown-vault-mcp user with these directives, among others:
| Directive | Effect |
|---|---|
ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes |
The filesystem is read-only except the paths listed below; /home, /root and /run/user are not visible; a private /tmp |
StateDirectory=markdown-vault-mcp, ReadWritePaths=/var/lib/markdown-vault-mcp |
The one writable path, created and owned for the service |
NoNewPrivileges=yes, an empty CapabilityBoundingSet= |
No privilege escalation and no capabilities |
PrivateDevices=yes, RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 |
No devices; only Unix, IPv4 and IPv6 sockets, so outbound connections to other services work |
SystemCallFilter=@system-service |
Only the system calls an ordinary service needs |
MemoryDenyWriteExecute=no |
Left open because Python needs writable, executable memory |
UMask=0027 |
New files are group-readable, so an administrator in the markdown-vault-mcp group can inspect the state directory |
To let the server read or write a path outside /var/lib/markdown-vault-mcp, add a drop-in rather than editing the unit, which the next package upgrade replaces:
sudo systemctl edit markdown-vault-mcp
[Service]
ReadWritePaths=/srv/markdown-vault-mcp
systemctl edit reloads the unit definitions; restart the service afterwards. The other directives stay as they are. On a host with SELinux enforcing, the directory also needs a label the service may use (semanage fcontext, then restorecon).
Upgrade¶
Install the new package the same way as the first one. Its postinstall installs the new version into the existing virtual environment and restarts the service when it is running. /etc/markdown-vault-mcp/env is left alone; env.example is replaced, so compare the two for variables the release added:
diff /etc/markdown-vault-mcp/env.example /etc/markdown-vault-mcp/env
What a release changes for your clients and your data is on the Upgrade page.
Without a package¶
On a distribution the packages do not cover, mirror what the package does. The unit and the environment template live in the repository's packaging/ directory at every release tag. Before you run the block, set EXTRAS to the value packaging/scripts/postinstall.sh gives it at the same tag: the package installs those extras, and an install without them lacks the features they bring.
sudo groupadd --system markdown-vault-mcp
sudo useradd --system --gid markdown-vault-mcp --no-create-home \
--home-dir /var/lib/markdown-vault-mcp --shell /usr/sbin/nologin markdown-vault-mcp
sudo mkdir -p /opt/markdown-vault-mcp /etc/markdown-vault-mcp /var/lib/markdown-vault-mcp
sudo python3 -m venv /opt/markdown-vault-mcp/venv
EXTRAS="" # set from packaging/scripts/postinstall.sh first, such as "[all]"
sudo /opt/markdown-vault-mcp/venv/bin/pip install "markdown-vault-mcp${EXTRAS}==X.Y.Z"
sudo curl -fsSL -o /usr/lib/systemd/system/markdown-vault-mcp.service \
https://raw.githubusercontent.com/pvliesdonk/markdown-vault-mcp/vX.Y.Z/packaging/markdown-vault-mcp.service
sudo curl -fsSL -o /etc/markdown-vault-mcp/env.example \
https://raw.githubusercontent.com/pvliesdonk/markdown-vault-mcp/vX.Y.Z/packaging/env.example
sudo cp /etc/markdown-vault-mcp/env.example /etc/markdown-vault-mcp/env
sudo chmod 600 /etc/markdown-vault-mcp/env
sudo chown markdown-vault-mcp:markdown-vault-mcp /var/lib/markdown-vault-mcp
sudo systemctl daemon-reload
Then configure and start as above. Python 3.11 or newer with the venv module is the one system requirement.
If it does not start¶
journalctl -u markdown-vault-mcp -n 50 --no-pagershows the last records; a configuration error names the variable.- Run the command as the service user to see the error directly:
sudo -u markdown-vault-mcp /opt/markdown-vault-mcp/venv/bin/markdown-vault-mcp serve --transport http. - A permission error on a path outside
/var/lib/markdown-vault-mcpis the confinement above: add the drop-in, or move the data. systemd-analyze verify /usr/lib/systemd/system/markdown-vault-mcp.servicechecks the unit after an edit.
Where this server keeps its files¶
The environment file leaves MARKDOWN_VAULT_MCP_SOURCE_DIR commented out. Its default, /data/vault, is outside what the unit may write, so set it, and put this server's other files under the state directory too:
MARKDOWN_VAULT_MCP_SOURCE_DIR=/var/lib/markdown-vault-mcp/vault
MARKDOWN_VAULT_MCP_INDEX_PATH=/var/lib/markdown-vault-mcp/index.db
MARKDOWN_VAULT_MCP_EMBEDDINGS_PATH=/var/lib/markdown-vault-mcp/embeddings/embeddings
MARKDOWN_VAULT_MCP_FASTEMBED_CACHE_DIR=/var/lib/markdown-vault-mcp/fastembed
- A writable vault outside the state directory needs its own
ReadWritePaths=drop-in, as above. The server's change-tracking file sits beside the index in the state directory, not in the vault.ProtectHome=yeskeeps/homefrom the service. A vault under/srv, like the one in the drop-in above, stays clear of it. - Without
INDEX_PATHthe index lives in memory and is rebuilt at every start. - Without
FASTEMBED_CACHE_DIRFastEmbed keeps its model in the temporary directory. The unit'sPrivateTmp=yesdiscards that at every stop, so the model downloads again at each start.
For a vault that commits and pushes through the server, an empty SOURCE_DIR plus the variables in Git integration is enough: the server clones at the first start.