Running a server
git-pages is an application that serves static websites from either filesystem or an S3 compatible object store and updates them when directed by the site author through an HTTP request. The server scales linearly from a personal instance running on a Raspberry Pi to a highly available, geographically distributed cluster powering Grebedoc. It is written in Go and does not depend on any other services, although most installations will use a reverse proxy like Caddy or Nginx to serve sites using the https:// protocol.
This document explains how to configure and operate a git-pages server.
Installation
You can install git-pages from a binary build or source code, or use a Docker image.
... from a binary build
git-pages provides binary builds for multiple architectures and operating systems on the release index. Download the right binary for your platform and place it in a directory on your PATH (or anywhere else convenient). Builds for the following platforms are currently available:
| Operating system | Hardware | Binary name |
|---|---|---|
| Linux | 64-bit Intel | git-pages.linux-amd64 |
| Linux | 64-bit ARM | git-pages.linux-arm64 |
| Windows | 64-bit Intel | git-pages.windows-amd64.exe |
| macOS | M1 and later | git-pages.darwin-arm64 |
If you'd like us to provide binary builds for a platform not listed above, file an issue.
Warning
In addition to building binaries for release versions, the binaries built from the latest commit to the main branch should be available in a pre-release called latest, but this usually fails to materialize due to a bug in Forgejo.
... from source code
git-pages requires Go 1.25 or newer. Once you install Go and Git, install the application as follows:
This command will install the latest released version; you can pick a different version by replacing latest with the desired version number, e.g. v0.3.0.
... via Docker
git-pages provides an OCI-compliant (Docker) container image built for each release as well as from the latest commit to the main branch.
Bug
Due to an issue with the Forgejo Actions runner provided by Codeberg, this installation method is currently not available. We are working on addressing this.
Configuration
git-pages has no required configuration options and can be launched as simply as:
$ git-pages -no-config
time=2025-12-07T05:21:42.045Z level=INFO msg="memlimit: was 8.0 EB now 31.3 GB"
time=2025-12-07T05:21:42.046Z level=INFO msg="fs: has atomic CAS"
time=2025-12-07T05:21:42.046Z level=INFO msg="serve: ready"
However, most installations will use configuration options. By default, the configuration is read from a TOML file named config.toml in the current directory; alternately the location of the configuration file may be specified using the -config /path/to/config.toml command line option.
Environment variables
In addition to the config.toml file, git-pages may be configured via environment variables. Every TOML configuration option has a corresponding environment variable name. For example, the following two configurations are equivalent:
The -print-config-env-vars command line option lists every accepted environment variable and its default value:
$ git-pages -print-config-env-vars
PAGES_INSECURE bool = "false"
PAGES_FEATURES []string = "[]"
PAGES_LOG_FORMAT string = "text"
PAGES_LOG_LEVEL string = "info"
PAGES_SERVER_PAGES string = "tcp/:3000"
...
Whenever both the TOML file and an environment variable specify a value for some configuration option, the value of the environment variable is used. The -print-config command line option displays the final configuration (after taking into account the default values, the configuration file, and the environment variables):
$ go run . -print-config
features = []
log-format = 'text'
log-level = 'info'
wildcard = []
[server]
pages = 'tcp/localhost:3000'
caddy = 'tcp/localhost:3001'
metrics = 'tcp/localhost:3002'
...
Storage backends
git-pages needs a place to store the contents of the sites it manages, which is called a backend. Currently, two backends are provided: local filesystem and S3 object store.
Filesystem
By default, git-pages uses the filesystem backend and stores the site contents in the data subdirectory of the current directory. The only configuration option for this backend is the path where the data will be stored:
S3 object store
Storing site contents in an AWS S3 compatible object store is useful when git-pages is deployed in a cluster, or simply to reduce storage costs. At a minimum, it is necessary to configure an endpoint, a bucket name, a region, an access key, and a secret:
Refer to the documentation of your S3 object store service provider for how to obtain these credentials. The credentials above are valid for the public demo service provided by MinIO, to be used for evaluation only.
The S3 object store backend in git-pages is conservative with the requests it makes, and may be used with virtually any S3-compatible service or application. It has been verified to work with AWS S3, Wasabi, Tigris, MinIO, and Garage. We do not recommend any specific service or application, and you should pick the option that fits your needs the best.
Note
For partial update operations (PATCH requests), git-pages will attempt to use the conditional writes function of the S3 object store (specifically, PutObject requests are issued with both If-Match: and If-Unmodified-Since: headers). Object stores that provide this function include AWS S3, Tigris, and MinIO. If conditional writes are not implemented natively by the object store, git-pages will emulate them using HeadObject, which has a small window during which a race condition may cause an update operation to be lost.
In most cases, the presence or lack of conditional write support should not affect your choice of an S3 object store. You may ignore this section unless you expect a very high frequency of partial updates.
To improve page load times and reduce the amount of requests to the S3 object store, git-pages implements an in-memory cache in front of it. The maximum size of this cache may be adjusted according to the amount of available memory; the defaults are conservative and should be acceptable for most deployments. The maximum age and stale period of the site cache determine the speed at which updates to the site contents reach the visitors; the meaning of these values is the same as in the HTTP Cache-Control header.
Systemd unit file
Typically, git-pages will run as a system service. The following systemd unit file can be used as a starting point for running it on a Linux system:
[Unit]
Description=git-pages static site server
After=network-online.target
Requires=network-online.target
[Install]
WantedBy=multi-user.target
[Service]
ExecStart=git-pages -config /etc/git-pages/config.toml
StateDirectory=git-pages
DynamicUser=true
PrivateTmp=true
Restart=on-failure
RestartSec=5s
Configuration may be provided via Environment keys in addition to, or instead of the TOML configuration file:
[Service]
ExecStart=git-pages -no-config
...
Environment="PAGES_STORAGE_S3_ACCESS_KEY_ID=ABCDEFGHIJKLMNOP1234"
...
TLS termination
Today, it is expected that every website will be served TLS encrypted from an https:// URL. git-pages itself does not implement TLS encryption and requires a reverse proxy server to terminate TLS (decrypt incoming traffic, pass it to git-pages, then encrypt the response and send it back).
The recommended reverse proxy server is Caddy; other options include Nginx and HAProxy. The advantage of using Caddy is that it is easy to configure and it fully automates acquisition of TLS certificates from Let's Encrypt for an open set of domains (where uploading a site to git-pages enables Caddy to acquire a TLS certificate on demand). If you plan to use git-pages with only a few domains that are known in advance, you may use any reverse proxy server you like.
... with Caddy
The minimal Caddyfile below is suitable for a deployment of git-pages on a limited set of domains:
The following Caddyfile shows how to deploy git-pages on an open set of domains using On-Demand TLS:
{
on_demand_tls {
permission http http://localhost:3001
}
}
https:// {
tls {
on_demand
}
reverse_proxy http://localhost:3000
}
These Caddy configurations use the default ports for the pages and caddy endpoints. The http://localhost:3000 (pages) endpoint is used to serve site contents and process updates, while the http://localhost:3001 (caddy) endpoint is used by Caddy to check whether it should acquire a TLS certificate for a given domain. Both of these endpoints are configurable:
To use a Unix domain socket instead of TCP, configure an endpoint as unix//path/to/endpoint.sock in both git-pages and Caddy.
If you are deploying git-pages on a single machine, the configuration above is sufficient. In a cluster deployment, it is convenient to configure Caddy to store key material in the same S3 object store as the site contents. This can be done by building Caddy with the certmagic-s3 plugin and configuring it to use the same environment variables as git-pages itself uses for S3 credentials:
{
storage s3 {
host "{env.PAGES_STORAGE_S3_ENDPOINT}"
access_id "{env.PAGES_STORAGE_S3_ACCESS_KEY_ID}"
secret_key "{env.PAGES_STORAGE_S3_SECRET_ACCESS_KEY}"
bucket "{env.PAGES_STORAGE_S3_BUCKET}"
prefix "ssl"
}
}
Success
Congratulations! You can now publish a site to your git-pages server. While you can stop here, the sections below explain how to adjust your configuration further.
Wildcard domains
git-pages can be used to publish personal sites for each user of a git forge like Forgejo. To do this, it is necessary to set up a wildcard DNS record for a domain used to publish the sites, and configure a mapping between its subdomains and git repositories hosted on the forge.
Note
While wildcard domains can be configured via environment variables, the resulting values are difficult to read and maintain; the examples will use TOML only.
Let's consider a configuration for a forge https://mygit.forge where the sites are published under https://mygit.page:
[[wildcard]]
domain = "mygit.page"
clone-url = "https://mygit.forge/<user>/<project>.git"
index-repos = ["<user>.mygit.page"] # (1)!
index-repo-branch = "main" # (2)!
- While multiple index repositories may be specified, we strongly recommend only using one.
- The default is
index-repo-branch = "pages", and we recommend keeping it.
Warning
For security reasons, you must not use the second-level domain of your forge (mygit.forge in this example) to host user-authored sites; use a different second-level domain (mygit.page in this case) to isolate your forge and safeguard sensitive credentials.
This configuration creates two distinct mappings:
- Every site URL of the form
https://USER.mygit.page/corresponds to a repositoryhttps://mygit.forge/USER/USER.mygit.page.gitand branchmain; - Every site URL of the form
https://USER.mygit.page/PROJECT/corresponds to a repositoryhttps://mygit.forge/USER/PROJECT.gitand branchpages.
Sites using wildcard domains may be updated in one of the three following ways:
- A webhook
POSTrequest delivered by a forge (Forgejo, Gitea, Gogs, or GitHub) causes git-pages to clone the repository and publish the contents of the corresponding branch; - A
PUTrequest containing a git repository URL causes git-pages to clone the repository and publish the contents of the corresponding branch; - A
PUTorPATCHrequest containing an archive causes git-pages to publish the contents of the archive.
In all three cases, only someone with write access to the corresponding repository is able to change site contents. Uploading an archive with the output of a static site generator (typically from a CI workflow) increases efficiency and reduces unnecessary git repository updates; it requires using an authorization token with write access to the repository obtained from the forge. The supported forges are Forgejo, Gitea, and Gogs, and the method must be enabled as follows:
Multiple wildcard domains may be configured by repeating the [[wildcard]] section in the configuration file.
Resource limits
git-pages offers configurable limits on its operation so as to operate robustly in an adversarial environment of the open internet. The default values are conservative and suitable for most deployments, but may need to be changed when publishing larger sites.
Go heap size limit
The Go language uses garbage collection, meaning that the memory allocated by the application is not released to the OS right away, but only after the need arises. Almost all operations performed by git-pages require only a small amount of RAM, with the exception of site updates where the entire site may have to be loaded into memory all at once. This may cause the machine to run out of memory and kill the git-pages process, causing an outage.
The maximum heap size ratio sets a soft limit for heap size past which the Go runtime will aggressively attempt to free memory even at the cost of reduced performance. It is configured as a ratio of total available memory (excluding swap); the default value of 0.5 means that Go will only use more than 50% of system memory if absolutely necessary.
This configuration option is most useful for resource-constrained machines; git-pages will run with less than 64 MB of RAM, but it will not be able to publish large sites in such a configuration.
Site size limits
git-pages offers fine-grained limits for user-authored content through four closely related configuration options.
The update timeout limits the amount of time a site update may take. It applies equally to updates from a git repository and updates from an uploaded archive. This limit may need to be raised when dealing with slow connections, slow git repository hosts, or very large sites.
The maximum site size option limits how much storage in total a single site may consume, and is applied throughout the processing of the site; for example, when a ZIP archive is uploaded, the limit first applies to the size of the compressed request body, and then to the total size of every archive member. This limit is applied before compression and deduplication to ensure its transparency and fairness.
The maximum manifest size option limits how large an individual site manifest can get. The site manifest needs to be loaded in whole every time the site is accessed, so its size should be quite small; typically no more than a few MB. This option indirectly limits how many files can there be in a single site; as a rough approximation, 1 MB of manifest size fits about 5000 files.
The maximum inline file size limit changes how big a file may become until it is stored in a separate blob rather than directly in the manifest. This limit should not be less than the size of a blob reference, which is 71 bytes long. Increasing this limit enlarges manifests and reduces the amount of very small blobs. This limit should only be changed if storing small blobs is expensive or to reduce latency for sites with many small files.
Functional limits
git-pages also offers configurable restrictions on its functionality whose purpose is to prevent accidental or intentional misuse.
The custom header allowlist enumerates headers configurable via the _headers file. The following headers are considered so critical that they cannot be included in this allowlist: Accept-Ranges, Age, Allow, Alt-Svc, Connection, Content-Encoding, Content-Length, Content-Range, Date, Location, Server, Trailer, Transfer-Encoding, Upgrade. The allowlist applies both when a site is updated (where a header not in the allowlist causes a diagnostic to be produced and then gets discarded) and when a page is served (where using a header not in the allowlist causes the page load to fail with a 500 Internal Server Error).
The repository URL prefix allowlist restricts the set of repositories from which sites may be uploaded. Since it is not possible to determine what repository (if any) an archive was built from, archive uploads are prohibited, except for wildcard domains with forge authorization.
The forbidden domain list defines domains to which a site may not be uploaded under any circumstances. This includes subdomains; the configuration below prohibits uploading sites to both internal.mygit.page and metrics.internal.mygit.page. Sites that have been uploaded before the option has been set will continue to be available.
The maximum symlink depth option limits how much time will be spent resolving symbolic links. The primary purpose of this limit is to avoid infinite loops caused by resolving a symlink that refers to itself (directly or indirectly). The default value should be suitable for virtually all deployments.
Observability
git-pages provides four modes of insight into its runtime operation: logs, counters, errors, and traces. It offers multiple ways of exposing this information.
As a policy, IP addresses are never included in this data; they are only included verbatim in audit logs if explicitly configured for collection.
Console logs
git-pages will send application logs to the standard error stream by default. The possible log formats are none (no output), text (default; human-readable text), and json (one JSON object per log line).
Syslog
git-pages will send application logs to a syslog daemon like syslog-ng or VictoriaLogs when the SYSLOG_ADDR environment variable is set to a valid destination. Note that this environment variable does not have an equivalent TOML configuration option.
- Local destinations are specified using
unixgram//path/to/endpoint.sockortcp/localhost:port. - Network destinations are specified using
tcp+tls/host:portortcp/host:port.
Prometheus
git-pages contains many statistical counters that can be used to monitor application health, analyze resource usage, build pretty Grafana dashboards and more. The values of these counters are available via the http://localhost:3002 (metrics) endpoint in the Prometheus format (both text and binary). This endpoint is configurable:
The purpose of each counter is documented as a part of the Prometheus format; it should be displayed in your observability software, or you can explore the output of curl http://localhost:3002.
Sentry
git-pages will report certain data to Sentry if the SENTRY_DSN environment variable is set. It will always send issue reports (on panics and unhandled errors); it will send application logs if the SENTRY_LOGS environment variable is set to 1 or true; and it will send execution traces if the SENTRY_TRACING environment variable is set to 1 or true. The ENVIRONMENT environment variable (development by default) can be set to an arbitrary value to help distinguish multiple git-pages instances using the same DSN; additionally, setting it to development or staging disables reporting of execution traces.
If tracing is enabled, the slow response threshold configuration option controls how long a response needs to take in order to be traced unconditionally. This will skew the statistical inferences Sentry makes on your behalf, but will also alert you to likely frustration of site visitors.
Operation
git-pages provides a rich set of administrative commands, the most important of which are described below. All of these commands expect options to be provided as a TOML configuration and/or environment variables. Whenever a git-pages command appears in this section, you should ensure that it receives the appropriate configuration for your environment.
Audit log
git-pages includes an audit function that records every substantial event affecting the backend data and keeps accurate historical snapshots of every site; deduplication of storage makes these snapshots inexpensive. Each git-pages process that uses the same backend configuration must be assigned a unique node ID (a number from 0 to 63) to prevent identifier collisions, after which the collection of audit records may be enabled:
By default, IP addresses are not collected, making the audit records essentially anonymous. To enable collection of IP addresses, pick the source of this information: RemoteAddr to use the peer address of the socket, X-Forwarded-For to use the rightmost entry in the corresponding header. Use the X-Forwarded-For source when using git-pages with a reverse proxy server:
The substantial events that trigger the creation of an audit record are currently:
- creating or updating a site (
CommitManifestevent); - deleting a site (
DeleteManifestevent); - freezing a domain (
FreezeDomain) event; - unfreezing a domain (
UnfreezeDomainevent).
Note
Audit records are created before the corresponding substantial event, and if the creation of an audit record fails, the operation does not complete; that is, if the event has occurred, then a record of it will have been created. But the inverse is not true: if creation of an audit record succeeds but the operation that caused it fails afterwards, the audit record will remain in the log.
The audit log is best considered a record of intent: if an audit record exists, it means that someone attempted to perform the recorded action. To be sure whether the attempt had succeeded, examine also the application logs at the relevant timestamp.
To retrieve audit logs, use the -audit-log command line option:
$ git-pages -audit-log
0000019da0073000 2025-12-06T00:28:37Z 2001:db8::f00d grebedoc.dev/.index CommitManifest
0000019e44566000 2025-12-06T00:39:50Z 2001:db8::f00d grebedoc.dev/.index CommitManifest
000001ede967b000 2025-12-06T23:51:43Z 198.51.100.11 git-pages.org/.index CommitManifest
000001f5a2c58000 2025-12-07T02:06:43Z <cli-admin> problematic.site/.index DeleteManifest
To drill down into a specific event, use the -audit-read command line option:
$ git-pages -audit-read 000001ede967b000
$ ls
000001ede967b000-archive.tar
000001ede967b000-event.json
000001ede967b000-manifest.json
$ cat 000001ede967b000-event.json
{
"id": "2121334763520",
"timestamp": "2025-12-06T23:51:43.995096981Z",
"event": "CommitManifest",
"principal": {
"ipAddress": "198.51.100.11"
},
"domain": "git-pages.org",
"project": ".index"
}
The information extracted from this audit event includes the event description (*-event.json file), site manifest (*-manifest.json file), and archive of site contents (*-archive.tar file); the latter two files will only be present for CommitManifest events. This information may aid in recovering lost data, or help you make a decision on an abuse report concerning data that has been overwritten or deleted since.
Tip
Currently, git-pages will never delete audit records itself.
If you are using an S3 object store, it may offer configurable object lifecycle rules. Consider setting up a 30 day retention policy for the audit/ prefix.
Content scanning
git-pages can be configured to scan site contents in the background for known threats as the updates are published. This is done by enabling audit logs, configuring an audit notify endpoint, and running an audit server. The audit server receives notifications whenever an audit record gets created, in response to which it runs an arbitrary executable to make a decision.
First, configure the notify endpoint. Note that the pages server will only perform GET requests to it, and the audit server ignores everything but the query string.
Then, start the audit server. In this example, it runs on the same machine as the pages server, so the endpoint is tcp/localhost:3004. It is also possible to use a Unix domain socket, e.g. unix//path/to/audit.sock.
Whenever a notification arrives, the audit server runs the deciding executable ./autoscan.sh with any additional command line arguments provided (none in this case), followed by the audit record ID in hexadecimal and the event name; for example, ./autoscan.sh 000001ede967b000 CommitManifest. The current directory will point to a newly created temporary directory, and the contents of the temporary directory will be the same as if -audit-read was executed in it beforehand. The deciding executable may run for as long as necessary and complete with any exit code, but if the exit code is not successful (i.e. non-zero), it will be restarted after a short delay.
The deciding executable will asynchronously examine the event and take any necessary action. For example, the following shell script (which depends on jq) will scan site contents with ClamAV whenever an update is published, and take enforcement action if the scan comes back positive:
#!/bin/bash -e
config=/etc/git-pages/config.toml # (1)!
export PAGES_AUDIT_NODE_ID=63 # (3)!
if [[ "$2" = "CommitManifest" ]]; then
if ! clamdscan $1-archive.tar; then
domain=$(jq -r .domain <$1-event.json)
project=$(jq -r .project <$1-event.json)
echo '<h1>Threat Automatically Removed</h1>' >index.html # (2)!
echo '/* /index.html 410!' >_redirects
tar cf site.tar index.html _redirects
git-pages -config $config -update-site "$domain/$project" site.tar
git-pages -config $config -freeze-domain "$domain"
fi
fi
- Since the audit server changes the current directory, the location of the configuration file must be specified explicitly. Environment variables may also be used instead.
- Remember that every automatic scanner has false positives! The placeholder page must include a clear explanation of the reason for removal, as well as contact information. This example is not appropriate for production use.
- All deciding executables will use the same node ID (63), which will prevent audit record ID collisions with pages server processes.
Warning
Keep in mind that every deployment is unique, and it is impossible to provide a one-size-fits-all automated scanning solution. This section explains how to build a custom solution tailored to your needs; the script above demonstrates the principles but should not be used as-is.
Note
The audit server does not implement queueing, retries, or exponential backoff; it runs the deciding executable once per GET request and responds with the status and captured standard output/error. The pages server, however, will keep resubmitting the notification (with exponential backoff and added random jitter) until either the request succeeds or the pages server is restarted.
This means that the audit server may be restarted at any time without loss of audit notifications, but restarting the pages server will cause all pending audit notifications to be lost. If you need stronger guarantees than these, you should implement them as a part of your deciding executable.
Responding to abuse
If you discover that a site managed by git-pages contains abusive material (spam, phishing, illegal downloads, etc) then the recommended course of action is to replace every page of the site with a placeholder returning the 410 Gone status and then freeze the domain, preventing any further updates of any site on that domain. This can be done from the command line as follows:
cd $(mktemp -d)
echo "<h1>Gone</h1>" >index.html # (1)!
echo "/* /index.html 410!" >_redirects # (2)!
tar cf placeholder.tar index.html _redirects
git-pages -update-site problematic.site placeholder.tar
git-pages -freeze-domain problematic.site
- Customize this HTML template to include the reason for removal, your contact information, and anything else relevant.
- The
410 Gonestatus ensures that the site is delisted from search engine results and removed from the cache of malware scanners.
If you later decide that the domain should no longer be restricted, you can unfreeze it to restore normal functionality:
The freeze and unfreeze operations will append records to the audit log (if it is enabled).
Note
The freeze operation is done per-domain and not per-site since the publisher of the abusive material must have access to DNS records for the domain, and therefore complete control over it, to publish a site in the first place.
The freeze operation will prevent you from making administrative updates to sites on that domain as well. Remember to examine other sites on the same domain before freezing it.