Open-source project
mbrubeck/agate avatar
mbrubeck/agate

Agate serves a Gemini capsule from a folder and stops there

Very simple server for the Gemini hypertext protocol

754 stars42 forksRustApache-2.0

At a glance

What is it?
Agate is a deliberately small Rust server that answers Gemini requests with static files, generates its own self signed certificates, and rejects any path with a dot segment. It fits a small capsule and nothing that needs to compute a response.
Who is it for?
Agate fits operators who want a Gemini capsule served from a directory of .gmi files and nothing more. It is small, it terminates TLS on its own with self signed certificates, and its mapping from URL to file is easy to reason about.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Agate answers Gemini requests with files and nothing else

Agate is a single Rust binary whose whole job is answering Gemini requests with files from a directory you control. The scope is narrow on purpose. The README says Agate has very few features and can only serve static files, and the crate description calls it a very simple server for the Gemini hypertext protocol. Nothing in the dependency list suggests a template engine, a script runtime or a database. The manifest pulls in configparser, env_logger, futures-util, getopts, glob, log, mime_guess, percent-encoding, rcgen, tokio-rustls, tokio and url, and that list is the whole story. This is what you want if the plan is a capsule of .gmi pages published from a folder of plain files, and it is exactly why the project will not fit a site that computes anything per request. The audience is correspondingly small: operators who have a host name, a directory, and readers willing to accept a self signed certificate on first contact.

A request path becomes a file, and a dot segment becomes status 52

Path handling is the part worth understanding before you commit. When a client asks for gemini://example.com/foo/bar, Agate serves the file at path/to/content/foo/bar, a direct substitution of the content root for the host part of the URL. A directory in that position is resolved by looking for index.gmi inside it, so the default landing page is a file you create rather than a setting you toggle. Any path segment that starts with a dot returns status code 52, whether or not the file exists, which keeps hidden files out of reach without leaking their presence. Two switches change that behavior: --serve-secret, and a per file entry in the .meta configuration file. There is no documented fallback chain beyond that index.gmi lookup, so a capsule that expects a rewrite rule will not find one here, and the operator has to place files by hand.

First run: content directory, host name, address and language code

Running it for the first time takes one command line. The README lists the arguments for the location of the content directory, the IP address and port to listen on, the host name to expect in request URLs, and the default language code to include in the MIME type for text/gemini files:

bash
agate --content path/to/content/ \
      --addr [::]:1965 \
      --addr 0.0.0.0:1965 \
      --hostname example.com \
      --lang en-US

Every argument is optional, and the program shows the defaults it uses when you ask it for help. On the first run, with no key of your own, Agate generates a private key and a self signed certificate for each host name you pass. Port 1965 is the one Gemini clients expect. The two --addr entries in the example bind the wildcard IPv6 address and the wildcard IPv4 address, which matters on a dual stack host where binding one family alone leaves clients on the other family with no route.

The container image wants two volumes and forgets its certificates

The container route takes two volumes and one extra flag:

bash
$ docker run \
  -p 1965:1965 \
  -v your/path/to/gmi:/gmi \
  -v your/path/to/certs:/certs \
  ghcr.io/mbrubeck/agate:latest \
  --hostname example.org

The Dockerfile in the repository confirms the layout. It exposes port 1965, declares volumes for /gmi and /certs, and sets the entry point to agate with the address, the content path and the certificates path already filled in. Recent builds are published as OCI images on GitHub's Container Registry, tagged with the major version, the major and minor version, the full version string and latest, so a pinned tag such as 3.3 keeps a capsule on one release line while latest moves with every build. The trap is the certificate volume. Start the container without it and Agate still comes up, but the key is regenerated on every boot and visitors get a certificate warning each time the server restarts.

Directory listings need a marker file while index.gmi always wins

Directory listings are opt in, per directory, and they never leak the entries you would rather keep out of sight. Placing a file called .directory-listing-ok in a directory enables a basic listing for that directory only, with no effect on its sub directories. The marker file must be UTF-8 text and may be empty, and whatever text it holds is prepended to the listing, which makes a heading its natural use. Files and directories whose names start with a dot stay out of the listing, and that covers the marker file itself, the .meta configuration file and the parent directory entry. One precedence rule matters more than the rest: a file called index.gmi always takes precedence over a directory listing, so a directory can advertise its contents without losing its own landing page.

Certificates cover host names only, and TLS 1.2 stays on by default

TLS is Agate's own responsibility, and it inherits some awkwardness from the protocol. By default the server supports TLSv1.2 and TLSv1.3, and the flag --only-tls13, short form -3, disables the older one. The documentation is blunt that this is not recommended, because the Gemini specification requires TLSv1.2 compatibility for now, since not every platform has good support for TLSv1.3. Certificates are generated for each host name given, and using an IP address for certificates is intentionally not supported, which rules out serving a capsule from a bare address without a name. The alternative to Agate handling TLS itself is terminating it in front of the server with publicly trusted certificates, but then Agate is no longer what the client trusts on first contact, and the trust on first use model that Gemini is built around disappears.

Metadata presets live in a .meta file with one global switch

Metadata presets add one file format on top of plain file serving. A file called .meta in any content directory stores metadata about the adjacent files, and it must be UTF-8 encoded. The flag -C, or the long version --central-conf, switches Agate to a single global file: the server then always looks for .meta in the content root directory and ignores .meta files in other directories. The entry syntax itself is only partly described in the README, so the exact parsing rules for individual entries are not something the published documentation settles, and they are worth checking against your own file before you rely on them. What is clear is the scope of the switch, and the fact that a .meta entry for a specific file is one of only two ways to make a path that would otherwise answer with status 52 reachable.

A dual licence covers the server while the test directory uses the GPL

The licence has two layers, and the second one only appears if you look at the file list. Agate itself is free software under either the Apache License version 2.0 or the MIT licence, and the manifest declares the same choice in the package metadata:

toml
[package]
name = "agate"
version = "3.3.24"
license = "MIT OR Apache-2.0"
edition = "2024"

The tests directory is different. Code there is available under the GNU GPL version 3 or later, and it is not included in the packaged or pre-compiled versions of Agate, so it never reaches the binaries most people download. That is a licensing boundary rather than a restriction on running the server. The same packaging applies to the crate itself: the Cargo package excludes tools, .github, Cross.toml, content and several documentation files, so what arrives from crates.io is the server rather than the whole repository.

Editorial conclusion

Agate fits operators who want a Gemini capsule served from a directory of .gmi files and nothing more. It is small, it terminates TLS on its own with self signed certificates, and its mapping from URL to file is easy to reason about. Skip it if the capsule needs generated pages or a query string, because only static files are served. Before adopting it, confirm your client tolerates the self signed certificate on first contact, check that every directory you want reachable already contains an index.gmi, and decide where the certificate directory lives, since an unmounted one in Docker produces a new key on every restart.

Frequently asked questions

How do I install Agate on Linux?

The README lists several routes: a pre-compiled binary from the GitHub releases page, the Nix package manager with nix-env -i agate, an Arch Linux AUR package named agate-bin for a pre-compiled binary or agate to build from source, a Guix service type, cargo install agate from crates.io if you have the Rust toolchain, and OCI images published as ghcr.io/mbrubeck/agate. Building from source means running cargo build --release and taking the binary from target/release/agate.

How does Agate handle dot files and hidden paths?

If any segment of the requested path starts with a dot, Agate responds with status code 52 whether the file exists or not. You can change that with the --serve-secret flag or with a .meta entry for the specific file.

Can Agate serve dynamic pages or scripts?

No. The README states that Agate has very few features and can only serve static files, so anything that has to be computed per request needs a different server.

Which TLS versions does Agate support and can I turn off TLS 1.2?

TLSv1.2 and TLSv1.3 are supported by default, and the flag --only-tls13, or -3, disables TLSv1.2 support. The documentation says this is not recommended, since the Gemini specification requires TLSv1.2 compatibility for now.

How do I get directory listings in Agate?

Create an empty or text UTF-8 file named .directory-listing-ok in the directory you want listed. The listing applies to that directory only, hides entries whose names start with a dot, and a file named index.gmi in the same directory takes precedence over the listing.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. mbrubeck/agate on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mbrubeck-agate.svg)](https://hysenlabs.com/projects/mbrubeck-agate)

Was this useful?