dragoman
A web server for scholarly metadata with full DOI Resolution and DOI content negotiation. Send a DOI as the URL path; receive a redirect to the landing page or metadata in any supported format depending on the Accept header.
Installation
Prerequisites
- Rust 1.75+ (rustup.rs)
Install
This builds a release binary and installs it to ~/.cargo/bin/dragoman. Make sure ~/.cargo/bin is on your PATH (the Rust installer adds this automatically).
Local SQLite database
dragoman can serve metadata directly from a local SQLite database in the commonmeta format, bypassing the live Crossref/DataCite APIs. This dramatically reduces latency and API load for high-traffic deployments.
Database format
The database is a SQLite3 file with a single works table whose columns map one-to-one to the commonmeta v1.0 schema. The id column is the canonical DOI URL (e.g. https://doi.org/10.5281/zenodo.1234). Complex fields (contributors, references, …) are stored as JSON text.
You can build a database from any commonmeta-supported source using the commonmeta CLI.
Running the server
Start
# Default port 3456
# Custom port
# With a local database
# Write a PID file so the process can be stopped later
# All options together
TLS
The --domain flag controls whether and how dragoman serves HTTPS.
--domain value |
Behaviour |
|---|---|
| (not set) | Plain HTTP on 127.0.0.1:<port>. Use this behind a reverse proxy. |
localhost |
HTTPS on 127.0.0.1:<port> using a locally-trusted certificate from mkcert. |
| any public hostname | HTTPS on 0.0.0.0:<tls-port> with automatic certificate provisioning via Let's Encrypt. Requires --email. |
Local development (mkcert)
Install mkcert once and add its CA to your trust store:
Then start dragoman with --domain localhost:
dragoman runs mkcert automatically on the first start; subsequent starts reuse the cached certificate. Certificates are stored in --acme-cache (default /var/lib/dragoman/acme).
Production (Let's Encrypt)
dragoman provisions and renews certificates automatically using the ACME TLS-ALPN-01 challenge. No separate HTTP challenge server or reverse proxy is needed.
- Binds HTTPS on
--tls-port(default 443) with automatic certificate provisioning. - Binds HTTP on
--portwith a permanent redirect tohttps://commonmeta.org/…. - Stores certificates in
--acme-cache(default/var/lib/dragoman/acme) so they survive restarts.
Use --acme-staging while testing to avoid Let's Encrypt rate limits:
Options can also be supplied as environment variables (flags take precedence):
PORT=8080 COMMONMETA_DB=/data/commonmeta-2026-06-15.sqlite3 RUST_LOG=dragoman=debug
During development you can use cargo run in place of the installed binary:
# Run from the project root — the sqlite3 file in the root is loaded by filename
# Or with a full path
Error: port already in use
If the chosen port is already in use, the server logs an error and exits:
ERROR dragoman: failed to bind port=3456 error=Address already in use (os error 48)
Choose a different port with --port or stop the process that holds the port.
Database file not found
If the resolved database path does not exist, dragoman logs a warning and starts without a local database, falling back to the live API for all requests:
WARN dragoman: sqlite file not found, running without local database path=…
To use a local database, place the SQLite file at the platform default path or set COMMONMETA_DB to its location.
Stop
# Stop using the default PID file location (/tmp/dragoman.pid)
# Stop using a custom PID file
dragoman stop sends SIGTERM to the running process. The server handles the signal gracefully: it finishes in-flight requests and removes the PID file before exiting. Pressing Ctrl-C has the same effect.
CLI reference
dragoman <COMMAND>
Commands:
start Start the server (runs in the foreground)
stop Stop a running server by sending SIGTERM to its PID file
help Print help
dragoman start [OPTIONS]
-p, --port <PORT> TCP port to listen on [env: PORT] [default: 3456]
-d, --db <PATH> Local commonmeta SQLite3 database [env: COMMONMETA_DB]
--pid-file <PATH> Write PID to this file on startup [env: DRAGOMAN_PID_FILE]
--domain <DOMAIN> Domain for HTTPS. Use `localhost` for local dev (mkcert) or a public hostname for Let's Encrypt [env: DRAGOMAN_DOMAIN]
--email <EMAIL> ACME contact email for Let's Encrypt (required for public domains) [env: DRAGOMAN_EMAIL]
--acme-cache <PATH> Directory for ACME certificate storage [env: DRAGOMAN_ACME_CACHE] [default: /var/lib/dragoman/acme]
--acme-staging Use Let's Encrypt staging (testing only) [env: DRAGOMAN_ACME_STAGING]
--tls-port <PORT> HTTPS port when --domain is set [env: DRAGOMAN_TLS_PORT] [default: 443]
dragoman stop [OPTIONS]
--pid-file <PATH> PID file to read [env: DRAGOMAN_PID_FILE] [default: /tmp/dragoman.pid]
Environment variables
All flags can also be supplied as environment variables; flags take precedence. See env.example for an annotated template.
| Variable | Default | Description |
|---|---|---|
PORT |
3456 |
TCP port to listen on. |
COMMONMETA_DB |
(platform default) | Path to a local commonmeta SQLite3 database. Served before falling back to the live API. Platform defaults: macOS → ~/Library/Application Support/commonmeta/commonmeta.sqlite3; Linux → /var/lib/commonmeta/commonmeta.sqlite3. |
DRAGOMAN_CACHE_DB |
(platform default) | Path to the dragoman cache database for API fallback results. Platform defaults: macOS → ~/Library/Application Support/commonmeta/cache.sqlite3; Linux → /var/lib/commonmeta/cache.sqlite3. |
DRAGOMAN_PID_FILE |
(none) | Path for the PID file written by start and read by stop. |
DRAGOMAN_DOMAIN |
(none) | Domain for HTTPS. localhost → mkcert; public hostname → Let's Encrypt. Unset → plain HTTP on 127.0.0.1 (reverse proxy mode). |
DRAGOMAN_EMAIL |
(none) | ACME contact email. Required when DRAGOMAN_DOMAIN is a public hostname. |
DRAGOMAN_ACME_CACHE |
(platform default) | Directory for ACME/mkcert certificates. Platform defaults: macOS → ~/Library/Application Support/dragoman/acme; Linux → /var/lib/dragoman/acme. |
DRAGOMAN_ACME_STAGING |
(false) | Set to any non-empty value to use the Let's Encrypt staging environment. |
DRAGOMAN_TLS_PORT |
443 / 3456 |
HTTPS port when DRAGOMAN_DOMAIN is set. Defaults to 3456 for localhost, 443 for public domains. |
DRAGOMAN_VIKTORIALOGS_URL |
(none) | VictoriaLogs OTLP/HTTP endpoint. When unset, logs go to stdout only. |
DRAGOMAN_METRICS_ALLOW |
(none) | Comma-separated IPs allowed to scrape /metrics. When unset, the route is not registered. |
RUST_LOG |
dragoman=info |
Log filter (see tracing-subscriber). Use dragoman=debug for per-request cache hits. |
Usage
Redirect (HTML / browser)
When the Accept header prefers text/html or is absent, dragoman redirects to the DOI's landing page:
# Follow the redirect
# Inspect the redirect target without following
# https://zenodo.org/record/1089100
Content negotiation
Send an Accept header to receive metadata instead of a redirect.
BibTeX
RIS
CSL (Citeproc) JSON
Crossref
Crossref XML
DataCite
Schema.org JSON-LD
InvenioRDM
Formatted citation
text/x-bibliography accepts optional style= and locale= parameters. Style names come from the CSL style repository; locale codes from the CSL locales repository.
# APA (default)
# Vancouver in French
Query parameter overrides
Use ?format= instead of an Accept header:
Format-specific URL prefixes
Two URL prefixes return a fixed format without requiring an Accept header or ?format= parameter:
| Prefix | Output format | Content-Type |
|---|---|---|
/dois/{doi} |
DataCite JSON | application/vnd.datacite.datacite+json |
/works/{doi} |
Crossref JSON | application/vnd.crossref+json |
These routes are equivalent to /{doi}?format=datacite and /{doi}?format=crossref respectively. The ?source= override is still accepted if you need to force a specific registration agency.
Supported formats
| Accept header | ?format= value |
Notes |
|---|---|---|
application/x-bibtex |
bibtex |
|
text/x-bibliography |
citation |
style= and locale= params |
application/vnd.commonmeta+json |
commonmeta |
|
application/vnd.crossref+json |
crossref |
|
application/vnd.crossref.unixref+xml |
crossref_xml |
|
application/vnd.crossref.unixsd+xml |
crossref_xml |
alias |
application/vnd.citationstyles.csl+json |
csl |
|
application/vnd.datacite.datacite+json |
datacite |
|
application/vnd.datacite.datacite+xml |
datacite_xml |
|
application/vnd.inveniordm.v1+json |
inveniordm |
|
application/x-research-info-systems |
ris |
|
application/vnd.schemaorg.ld+json |
schemaorg |
|
text/html / (absent) |
— | 307 redirect to landing page |
HTTP status codes
| Code | Meaning |
|---|---|
| 200 | Metadata returned |
| 307 | Redirect to landing page |
| 404 | DOI not found |
| 406 | Requested content type not supported |
| 502 | Upstream API error |
Deployment (macOS)
Installation via Homebrew
dragoman can be installed from the front-matter Homebrew tap:
This builds dragoman from source (requires Rust, installed automatically as a build dependency) and places the binary at $(brew --prefix)/bin/dragoman.
Place the database
The platform default path on macOS is ~/Library/Application Support/commonmeta/commonmeta.sqlite3. Place the database there and no further configuration is needed:
To use a different path, set COMMONMETA_DB in the launchd plist or pass --db on the command line.
Run as a background service (launchd)
# Start at login and keep alive
# Check status
# View logs
# Stop the service
brew services start installs a launchd plist in ~/Library/LaunchAgents/ and starts the service immediately. It restarts automatically on crash and at login.
To run as a system-level daemon (starts at boot, not tied to a user login), use sudo brew services start dragoman. This installs the plist in /Library/LaunchDaemons/ instead.
Configuration
To change the port or other settings, edit the service environment variables and restart:
# Open the generated plist for editing
Manual installation (without Homebrew)
# Install Rust if not already installed
|
Intel Macs: replace
/opt/homebrewwith/usr/localin all paths below.
Run as a launchd daemon
The bundled com.front-matter.dragoman.plist targets Apple Silicon paths.
Check logs:
Stop and disable:
Updating
With Homebrew
Manual
Deployment (Debian / systemd)
This section covers running dragoman as a persistent system service on a Debian 13 server.
1. Build the binary
On the server, install Rust and install the binary:
|
Or cross-compile locally and copy the binary:
# macOS → Linux x86-64 (requires cross)
2. Create system user and directories
3. Place the SQLite database
The platform default path on Linux is /var/lib/commonmeta/commonmeta.sqlite3. Place the database there and no further configuration is needed:
To use a different path, set COMMONMETA_DB in the environment file instead.
4. Create the environment file
Copy the bundled example and edit it for your deployment:
For a production server with Let's Encrypt, at minimum set:
DRAGOMAN_DOMAIN=commonmeta.org
DRAGOMAN_EMAIL=admin@example.org
See Environment variables for the full reference.
5. Install and enable the systemd unit
Check the service is running:
Updating the binary
Updating the database
The database file can be replaced while the service is running without restarting. dragoman opens a new read-only SQLite connection per request, so an atomic rename takes effect immediately for new requests while in-flight requests finish against the old file.
Copy the new file to a staging path on the same filesystem, then rename it over the live file:
If the source database uses WAL mode (has accompanying -wal/-shm files), produce a clean single-file snapshot first with VACUUM INTO before copying:
Reverse proxy
When running behind a reverse proxy, do not set --domain. Let the proxy handle TLS termination; dragoman listens on plain HTTP at 127.0.0.1:3456 (or whatever --port you set).
Telemetry
Logs — VictoriaLogs
Set --viktorialogs-url (or DRAGOMAN_VIKTORIALOGS_URL) to the VictoriaLogs OTLP/HTTP endpoint. All tracing events are shipped in batches; the exporter is flushed on graceful shutdown.
Without the flag, logs are written to stdout only (the default).
Metrics — VictoriaMetrics / Grafana
Set --metrics-allow (or DRAGOMAN_METRICS_ALLOW) to a comma-separated list of IP addresses permitted to scrape the /metrics endpoint. The route is only registered when this flag is set; all other callers receive 403 Forbidden.
# curl http://localhost:3456/metrics # works from 127.0.0.1
The /metrics endpoint is served on the same port as the main application. The client IP is resolved from the X-Real-IP header first, then the first address in X-Forwarded-For, making it compatible with nginx, Caddy, and Traefik.
Metrics recorded:
| Metric | Labels | Description |
|---|---|---|
http_requests_total |
method, route, status |
Request count by route and HTTP status code |
http_request_duration_seconds |
method, route |
Request latency histogram |
Routes are normalised to templates (/dois/:doi, /works/:doi, /orcid/:id, /{pid}) to keep cardinality low.
License
MIT