Gitfence

Self-hosting

Install Gitfence on your servers with docker compose: sizing, your GitHub App, the offline license, encryption and Vault keys, Fenced CI, backups, upgrades.

Short answer: Self-hosted Gitfence is one container image (a compiled build) plus MongoDB, installed with docker compose on a Linux host you run. You create your own GitHub App, drop in a signed license file, and start. It never calls us. This is the full install and operations guide; the overview is on the self-hosted page.

Gitfence on your own infrastructure. This guide covers the install, the license, encryption, backups, upgrades and what to do when something refuses to start. Everything here stays on your servers.

1. What you get

  • One container image: the compiled Gitfence executable plus what it runs (git, bubblewrap, tini, CA certificates, the MongoDB database tools, zstd). No source tree, no Node.js. It serves the web app, git over HTTPS, the MCP endpoint for AI agents, and GitHub webhooks, on port 8080.
  • MongoDB, from the compose bundle (mongo:7, reachable only from Gitfence).
  • Optional Caddy for HTTPS, from the same bundle.

This is the same product as our hosted service at gitfence.dev, running as one organization, licensed with a signed file instead of a subscription.

2. What you provide

What Notes
A Linux host, x86_64 or arm64, with Docker (or Podman) and the compose plugin Debian 12 or Ubuntu 22.04/24.04 recommended. Size it with section 2.1. A host that runs only Gitfence is best (section 6).
A DNS name and TLS Either the bundled Caddy (a Let's Encrypt certificate; ports 80 and 443 must reach the host) or your own load balancer or ingress in front of port 8080.
A GitHub organization where you create Gitfence's GitHub App Your App, on your GitHub. Only your server ever holds its private key (section 3.3). github.com (GitHub Enterprise Cloud included) or GitHub Enterprise Server 3.9 or later (section 3.4).
Outbound HTTPS to GitHub Gitfence fetches from and pushes to GitHub through your App. Webhooks are best for prompt events. Without them, for example when Gitfence is inside a VPN, Gitfence can poll GitHub instead (see below).
Optional: an SSO identity provider and SCIM OIDC or SAML, and SCIM provisioning, are set up in the web UI by an owner, if your license includes them.
Optional: SMTP or Postmark For email (invitations, notifications). SMTP is a URL in the settings; Postmark an API key. Without email, invitation links are copied by hand.
Optional: an S3-compatible bucket For backups (section 7).
Optional: a HashiCorp Vault server For the customer-managed key (section 5).

No inbound webhooks needed

Webhooks are best for prompt updates. If GitHub cannot reach Gitfence, for example because Gitfence is inside a VPN, set GITFENCE_GITHUB_EVENTS=poll in gitfence.env. Gitfence then polls GitHub over outbound HTTPS. The default, GITFENCE_GITHUB_EVENTS=auto, uses webhooks first and falls back to polling when webhooks do not arrive. Set GITFENCE_GITHUB_POLL_SECONDS to change the full polling interval (default 60, minimum 15).

2.1 Sizing

Memory goes to packing a person's view of the repository (git's pack-objects, index-pack and upload-pack, about 0.7 KB per object in the view's history) plus 0.2 to 0.4 GB for Gitfence itself. Measured on public repositories (terraform, 45k commits; kubernetes, 163k commits and 31k files; see performance and sizing):

Repository Peak memory of the app Host Disk for Gitfence's data volume
Up to ~50k commits, ~10k files under 1 GB 2 vCPU, 4 GB the mirror + ~0.4 GB per restricted person
A large monorepo: ~50k commits with many files, or up to ~150k commits / ~30k files 1.0–1.2 GB, one heavy step at a time 4 vCPU, 8 GB, with 3 GB for the app the mirror (about the size of a fresh clone) + ~1 GB per restricted person + ~1 GB per group
~500k commits, ~100k files ~6 GB 8 vCPU, 16 GB the mirror + 3–4 GB per restricted person
  • Our recommendation for a ~50,000-commit monorepo: 4 vCPU, 8 GB RAM, the app limited to 3 GB (mem_limit: 3g under gitfence: in docker-compose.yml), so that two heavy steps at once (two people's first clones, a clone during a rule change) don't reach the limit and MongoDB keeps its memory.
  • Disk: an SSD volume of at least 2 × (clone size) + 1 GB × (restricted people) + 1 GB × (groups) + 10 GB. Example: a 2 GB repository, 30 contractors, 3 groups: 2 × 2 + 30 + 3 + 10 = 47 GB; take 100 GB. Each person gets their own copy of their view (git would otherwise serve them objects they may not see), and a removed person's copy is reclaimed when it is dropped. MongoDB needs little (about 0.4 KB per commit per group).
  • The volume must be ext4 with the encrypt feature for encryption at rest (section 5): sudo tune2fs -O encrypt /dev/<device> on the filesystem that holds /var/lib/docker (it can be done while it is mounted; check with sudo tune2fs -l /dev/<device> | grep features). XFS, btrfs and overlay-only volumes can't. Or set GITFENCE_ENCRYPTION=off and use full-disk encryption you manage (LUKS, an encrypted cloud volume).
  • CPU decides how long the first preparation takes, not whether it works: about 5 ms per commit for a terraform-sized tree and 12 ms for a kubernetes-sized one, once per group (and again after its rules change). For 50,000 commits, 4 to 10 minutes. Create the groups and settle their rules before inviting people: the first person to clone after a change waits for it (git shows nothing while it waits, then the clone runs).
  • Count your repository before the POC: git count-objects -vH (size-pack is the clone size, in-pack the objects) on a fresh clone, and git rev-list --count --all.
  • Your load balancer's timeouts: git requests stay alive while Gitfence prepares a view, but two admin actions on a large repository (creating a group, changing its rules) run inside one web request for minutes. Allow at least 15 minutes of idle time (AWS ALB: idle_timeout.timeout_seconds = 900), or the page shows a gateway timeout while the work completes. The bundled Caddy allows an hour.

3. Install

About 30 to 45 minutes for someone who knows Docker, most of it the GitHub App and DNS/TLS. Run everything as a user who can use sudo and docker. Inside the container Gitfence runs as user 1000: the files you give it (the license and the App's key) must be readable by that user. The steps say how.

3.1 Prepare the host

sudo apt-get update && sudo apt-get install -y docker.io docker-compose-v2 zstd   # or Docker's own packages
sudo tune2fs -O encrypt /dev/<device-holding-/var/lib/docker>                     # section 2.1; or GITFENCE_ENCRYPTION=off
sudo mkdir -p /srv/gitfence && sudo chown "$USER" /srv/gitfence && cd /srv/gitfence

3.2 Get the bundle and the image

We give you: gitfence-selfhost-compose-<version>.tar.gz (the compose bundle), SHA256SUMS, your license file, and either registry credentials or the image tarball gitfence-selfhost-<version>-amd64.tar.zst.

sha256sum -c SHA256SUMS --ignore-missing            # every file you received must say OK
tar -xzf gitfence-selfhost-compose-<version>.tar.gz --strip-components=1
cp .env.example .env                                # the version is already filled in
cp gitfence.env.example gitfence.env && chmod 600 gitfence.env

To verify the image signature, its digest, provenance, SBOM and the release files, see Verify a Gitfence release.

The image, one of:

  • Registry: echo <token> | docker login ghcr.io -u <user> --password-stdin with the read-only user and token we gave you, then docker compose pull. Keep GITFENCE_IMAGE=ghcr.io/hoot-codes/gitfence-selfhost in .env.
  • Tarball (air-gapped): zstd -dc gitfence-selfhost-<version>-amd64.tar.zst | docker load, then in .env set GITFENCE_IMAGE=gitfence/selfhost. (The arm64 tarball loads as gitfence/selfhost:<version>-arm64: set GITFENCE_VERSION=<version>-arm64.)

Below, <image> means gitfence/selfhost:<version> or ghcr.io/hoot-codes/gitfence-selfhost:<version>.

3.3 Create your GitHub App

The App lives in your GitHub organization; Gitfence (the company) never sees its credentials. You need to be an owner of that organization, and a browser.

mkdir -p secrets && sudo chown 1000:1000 secrets    # the container (user 1000) writes the App's files here
docker run --rm -it --network host -v "$PWD/secrets:/out" <image> \
  github-app create --org <your-github-org> --url https://gitfence.example.com --out /out

Flags: --org and --url (your GITFENCE_URL) are required; --name "Gitfence (Acme)" and --port 3457 are optional. On GitHub Enterprise Server add --github-url https://github.example.com (section 3.4).

The command prints Open http://127.0.0.1:3457/. That page is on the server's loopback (hence --network host; plain -p 3457:3457 does not work). From your laptop: ssh -L 3457:127.0.0.1:3457 <server>, then open http://127.0.0.1:3457/ in your browser. Click Continue to GitHub, review the App's settings there and click Create GitHub App. GitHub sends your browser back to the same page, and the command saves:

  • secrets/github-app.env: the App id, slug, client id, client secret and webhook secret, and GITHUB_APP_PRIVATE_KEY_PATH already pointing where the container sees the key;
  • secrets/github-app.pem: the App's private key (mode 0600, owned by user 1000).

If it can't write to secrets/, it stops before sending you to GitHub. Then:

  1. Copy the values from secrets/github-app.env into gitfence.env (they replace its empty GITHUB_APP_ lines). The webhook is already configured with the right URL and secret.
  2. Once, by hand: on the App's settings page, Permissions > Account permissions, set Email addresses: Read-only (used for invitations limited to a verified email domain). The command prints the link.
  3. Install the App on the repositories Gitfence should manage: the .../installations/new link the command prints. "Only select repositories" is fine.

3.4 Configure

In .env (read by compose itself): GITFENCE_VERSION and GITFENCE_IMAGE (section 3.2), and GITFENCE_DOMAIN, the host name people use, when you use the bundled Caddy.

In gitfence.env (read by Gitfence). At minimum:

  • GITFENCE_URL=https://gitfence.example.com: every link, clone URL, OAuth callback and email uses it.
  • GITFENCE_SECRET_KEY: at least 32 random characters (openssl rand -base64 48). Back it up somewhere safe: losing it makes saved integration secrets (Slack, webhooks) unreadable.
  • The GitHub App values from secrets/github-app.env. Leave GITHUB_APP_PRIVATE_KEY_PATH=/run/secrets/gitfence/github-app.pem as it is.
  • GITFENCE_TRUSTED_PROXIES=172.16.0.0/12 when you use the bundled Caddy (uncomment the line). Behind your own proxy: the addresses it connects from.
  • GITFENCE_ADMINS=you,colleague: GitHub logins that become admin when they sign in. Recommended. Unset: the first person to sign in becomes the admin, so sign in right after the first start.
  • Optional email: SMTP_URL (one of smtp://user:pass@host:587 with STARTTLS, smtps://user:pass@host:465, or smtp://relay:25 without authentication) or POSTMARK_API_KEY, plus EMAIL_FROM="Gitfence <[email protected]>" and optional EMAIL_REPLY_TO.
  • Encryption (section 5) and backup (section 7) settings when you use them.

GitHub Enterprise Server

On GitHub Enterprise Server (3.9 or later), tell Gitfence where it is; on github.com leave both unset.

  • GITHUB_WEB_URL=https://github.example.com: your server's address (https, no path). Sign-in, the App, repository links, clones and pull requests all use it.
  • GITHUB_API_URL: only if your REST API is not at <GITHUB_WEB_URL>/api/v3 (the default).

Create the App on it with github-app create ... --github-url https://github.example.com (section 3.3); the saved github-app.env then includes both settings. Gitfence refuses to start on a malformed value. The Gitfence server must reach your GitHub Enterprise Server over HTTPS. Your GitHub Enterprise Server does not have to reach Gitfence: without inbound webhooks, Gitfence polls it.

3.5 License and start

cp /path/to/your-license.gfl license.gfl && chmod 644 license.gfl   # read by user 1000; it holds no secret
docker compose --profile tls up -d    # with the bundled Caddy
docker compose up -d                  # with your own load balancer; Gitfence answers on 127.0.0.1:8080
docker compose logs -f gitfence

The first start takes about 20 seconds (MongoDB's first start). The log shows, in order: the version and data version, the license, encryption at rest, Gitfence listening on ..., and whether the Fenced CI sandbox is available (section 6). A problem with a setting or a file stops the start with one line saying what to fix (section 11). The container has a health check on /health for your load balancer.

Check the license: docker compose exec gitfence gitfence license show.

3.6 First repository, a restricted group, a first contractor

  1. Sign in at https://gitfence.example.com/app with GitHub (as a login in GITFENCE_ADMINS, or first).
  2. Connect the repository (the App must be installed on it, section 3.3): on Repositories, type <owner>/<repo> and press Connect. Gitfence finds the App's installation and mirrors the repository; for a large repository that takes as long as a fresh git clone from GitHub. Then, right away: make the recovery key (the reminder on the same page; section 5).
  3. Choose how contractors' work reaches GitHub: the repository's Settings > GitHub Actions & collaboration. Either a managed private fork (recommended when your workflows use secrets: contractors' branches never run them) or branches in the same repository. Until it is chosen, contractors' pushes are refused with a message saying so.
  4. Create a restricted group: the repository's Groups > New group, for example contractors, with the folders they must not see as hidden (and, if you like, folders they see as types only or read-only). On a large repository the group's first preparation runs now (section 2.1); create groups before inviting. The pull request filter (it keeps hidden code out of the conversations contractors read) is prepared now too: wait for Prepare the pull request filter in the setup checklist, or watch the repository's Pull requests page (Build now there if it stopped). About 2 minutes for 1,000 commits, 15–25 minutes for a 50,000-commit monorepo, with up to ~0.9 GB of memory beside the server. Without encryption at rest, docker compose exec gitfence gitfence filter prebuild <owner>/<repo> does the same and waits for it.
  5. Invite a contractor: People > Invite people, into that group. They get a link (by email, or copy it). The link sets up their clone and their coding agent's connection, with their own token.
  6. The contractor clones and pushes (git clone https://gitfence.example.com/<owner>/<repo>.git). They see the repository without the hidden folders, in every commit of its history. They push to branches named <their-login>/...; Gitfence checks every change against their access, pushes it to GitHub and opens the pull request through your App ("Opened by Gitfence on behalf of @"). Reviews and comments on GitHub come back to them, minus anything about hidden files.

The gitfence command line (docker compose exec gitfence gitfence ...) covers the same steps (repo add, view set, member add, token create; gitfence help), but with encryption at rest it can only change settings, not code: the key is held by the running server. Use the web app (or its admin API with an admin token) for connecting repositories and creating groups; the command line says so when it can't.

Rehearsed end to end on a fresh Docker host with the image tarball: load to first contractor pull request in 15 minutes of commands, plus the GitHub App and DNS/TLS.

4. The license

  • The license is a signed file. It is checked offline, at start and once a day. Gitfence makes no outbound calls to check it, and there is no license server.
  • In the compose bundle it is /run/gitfence/license.gfl. The GITFENCE_LICENSE_FILE setting changes the path; without it the default is $GITFENCE_DATA/license.gfl.
  • Renewals and seat increases: put the new file in place (write it in place, for example cat renewed.gfl > license.gfl, so the running container sees the new content). A running server takes a valid new file within a day, without a restart. A restart works too. Check a new file before replacing the old one with gitfence license check <file>.
  • A seat is a restricted person: someone whose access to a repository is less than full write access to everything (hidden folders, types only, read-only parts). One seat per person, across all repositories. Admins and full-access teammates are free. AI agents are not counted.
  • The server records the newest time it has seen, in the data volume, so turning the host clock back does not make a license younger.

Commands (prefix docker compose exec gitfence when run from the host):

Command What it does
gitfence license id This installation's id. A license can be bound to it.
gitfence license show The license in force: customer, seats, features, expiry, state, days left.
gitfence license check <file> Check a new file before putting it in place.
Situation What happens
Valid Everything works.
Expiring (from 30 days before the end) An admin banner reminds you. Everything works.
Expired, first 30 days (grace) An admin banner. Everything still works, for everyone.
Expired, after the grace Nobody new can be added as a restricted person, and new invitations stop. Everyone who has access keeps it. Admins and full-access teammates are unaffected, and removing people still works.
Over the seat count Nobody new as a restricted person until you are under the count again. Existing access is never cut.
Missing, edited, damaged, or made for another installation or address The server refuses to start, with a clear message. Your data is untouched.
Replaced with an invalid file while running The earlier valid license stays in force. Admins see the reason.

5. Encryption at rest

GITFENCE_ENCRYPTION=fscrypt (the example default): your git mirrors, filtered views and derived caches live in a directory the Linux kernel encrypts (fscrypt: AES-256-XTS for contents, AES-256-CTS for file names). The key is derived from one master key for your installation. The master key is never stored as it is: every credential (a git password, a browser session, the owner's recovery key) holds its own wrapped copy of it, and the server keeps only hashes of those secrets. A request with a valid credential opens the key into memory; it leaves memory again after 10 idle minutes and the files lock. Locked, contents and file names are unreadable to everyone on the server, root included.

  • fscrypt needs the data volume to be on ext4 with the encrypt feature. On the host: tune2fs -O encrypt /dev/<device>. If the filesystem cannot encrypt and fscrypt is required, Gitfence refuses to start (section 11). Without GITFENCE_ENCRYPTION set, it uses fscrypt when the filesystem allows and says so in the log when it cannot.
  • GITFENCE_ENCRYPTION=off: files are stored as they are, and you encrypt the disk yourself (LUKS, your cloud's volume encryption).

The first start. Gitfence makes the master key and moves any code already in the data volume into the encrypted directory. The log says:

Encryption at rest: on (fscrypt)
Encryption at rest: on (fscrypt); organization encrypted, NO recovery key yet: an admin makes one in the web UI (Settings, Encryption) before <date>

Until that date (14 days), the server also holds one copy of the master key wrapped with your GITFENCE_SECRET_KEY, so the people and tokens you set up in those days open it without extra steps; each gets its own copy the first time it is used. On that date the server's copy is deleted. From then on only your team's credentials and the recovery key open it. Do not change GITFENCE_SECRET_KEY during those 14 days.

Make the recovery key right after your first sign-in: the web UI shows a reminder on the Repositories page until you do (Settings, Encryption, "Make a recovery key"). It is shown once: download it and keep it with your other secrets. It opens the code if no browser or token of your team can (after a server move, for example). Gitfence never writes it to a log. The same page replaces the recovery key and locks the code now.

Check it at any time:

docker compose exec gitfence gitfence encryption status

It prints whether the organization has its master key and a recovery key, the end of the 14 days, and whether the files are encrypted on disk (the kernel's fscrypt policy on $GITFENCE_DATA/enc); it exits with an error when they are not. On the host, lsattr -d <data volume>/enc shows the E flag.

Customer-managed key (HashiCorp Vault)

With the customer-managed key on, every stored wrap of the master key is sealed once more with a data key that only your Vault can open. Vault only ever sees that 32-byte data key; nothing about your code leaves your server. The wrapped data key is kept in the data volume (cmk.json).

The Vault key is the kill switch: disable it in Vault and, within GITFENCE_CMK_CHECK_SECONDS (default 300), all code locks and no credential opens it again. Enable the key again and access resumes on the next check.

Settings in gitfence.env:

Variable Meaning
GITFENCE_CMK vault-transit
VAULT_ADDR For example https://vault.example.com:8200.
VAULT_TOKEN The token. Or VAULT_TOKEN_FILE instead (a Vault Agent or Kubernetes token file; reread on every call, so renewals are picked up).
VAULT_NAMESPACE Optional.
GITFENCE_CMK_KEY The transit key name, for example gitfence.
GITFENCE_CMK_MOUNT The transit mount (default transit).
GITFENCE_CMK_CHECK_SECONDS How often the key is checked (default 300).

Turn it on before the first start, on a new installation. Later it is refused with "GITFENCE_CMK can only be turned on before encryption keys exist (a new installation)."

A minimal Vault policy for the transit key gitfence:

path "transit/encrypt/gitfence" {
  capabilities = ["update"]
}
path "transit/decrypt/gitfence" {
  capabilities = ["update"]
}

AWS KMS and GCP KMS are not built in yet; ask us if you need them.

6. Tests in the fence (Fenced CI)

Restricted people and their AI agents can run the repository's own test suite against the real code, hidden parts included, without seeing it: Gitfence runs the tests on its own host, in a bubblewrap sandbox with no network, and returns as little as the repository's settings allow (by default only the verdict: passed or failed, counts, failing test names). An admin sets the commands per repository:

docker compose exec gitfence gitfence tests set <owner>/<repo> --command "go test ./..." [--install "<cmd>"] [--timeout 900]

--install runs first, with network, on the manifests and lockfiles only (to fetch dependencies); the tests then run with no network. People and agents call run_tests (an MCP tool), or the helper their setup link installed.

What works today, in this Docker install: bubblewrap inside the Gitfence container. Rehearsed on a fresh Docker 29 host: a contractor's run_tests returned the verdict in about 3 seconds. Two things are needed:

  1. The sandbox needs user namespaces and a fresh /proc in the container. Uncomment this line in docker-compose.yml, then docker compose up -d:

    security_opt: ["seccomp=unconfined", "apparmor=unconfined", "systempaths=unconfined"]
    

    The log then says Fenced CI sandbox: available. Without it: Fenced CI sandbox: NOT available, tests are never run (never outside the sandbox), and everything else works. The line turns off Docker's seccomp filter, its AppArmor profile and its masking of /proc for the Gitfence container: Gitfence's own sandbox (bubblewrap: new user, PID, network, mount and IPC namespaces, no network, only /usr read-only and the work folder) becomes the boundary around the tests, and the container's defaults no longer back it up. Our recommendation: give Gitfence a host or VM of its own (it is the simplest safe option: a test that escaped both would find only Gitfence), and make that trade with your security team. On Ubuntu 23.10 or later the host may also restrict unprivileged user namespaces (sysctl kernel.apparmor_restrict_unprivileged_userns is 1). If the log still says NOT available, set it to 0 on that dedicated host (/etc/sysctl.d/99-gitfence.conf), or ask us.

  2. Your toolchain in the image. The sandbox sees the image's /usr only. The image has git and Bun (the Gitfence executable runs JavaScript/TypeScript tests as bun); it has no Go, Python, Node.js or compilers. Add yours with a two-line image of your own, and use it as GITFENCE_IMAGE/GITFENCE_VERSION:

    FROM gitfence/selfhost:<version>
    USER root
    RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-venv golang-go \
     && rm -rf /var/lib/apt/lists/*
    USER gitfence
    

    Tools installed under /usr/local work too; call them by full path (the sandbox's PATH is /usr/bin:/bin), for example --command "/usr/local/go/bin/go test ./...". Rebuild it on each Gitfence upgrade.

Coming for self-hosting: a separate test runner (throwaway gVisor containers on a machine of their own), so the Gitfence container keeps Docker's default confinement. Tell us if you need it before your rollout.

7. Backups

Backups go to your own S3-compatible bucket (AWS S3, Hetzner Object Storage, MinIO, others). Set in gitfence.env: BACKUP_S3_ENDPOINT, BACKUP_S3_REGION, BACKUP_S3_BUCKET, BACKUP_S3_ACCESS_KEY_ID, BACKUP_S3_SECRET_ACCESS_KEY, and BACKUP_KEEP_DAYS (default 30; the newest 7 copies are always kept).

Schedule it once a day from the host, with cron:

15 3 * * * cd /srv/gitfence && docker compose exec -T gitfence gitfence-backup >> backup.log 2>&1

A systemd timer works the same way. The command exits non-zero when either part fails.

Alerts go to your own team, never to us: set ALERT_EMAIL (sent with your email settings) and/or ALERT_WEBHOOK_URL (a Slack or Discord style webhook). Gitfence checks the database, the data volume, free disk and encryption every minute, and alerts on a failed backup and on repeated server errors. GET /health/deep answers ok or unhealthy for your uptime monitor; with Authorization: Bearer <GITFENCE_HEALTH_TOKEN> it lists each check.

Backed up The MongoDB database (accounts, organizations, rules, tokens as hashes, the audit log), the documents/ folders (the agreements people signed, the uploaded PDFs), and instance.json (the installation id file).
Not backed up The git cache: mirrors and filtered views are fetched from GitHub again automatically. The per-repository view history: after a restore, restricted people clone again. No source code leaves your server in a backup.

Restore (the database restore replaces what is there, so take a moment first):

  1. docker compose stop gitfence
  2. Database: docker compose exec -T gitfence gitfence-restore-mongo [latest|<key>] [target-db-name]. Restoring into the live name (gitfence) replaces it. For a drill, restore into another name, for example gitfence_drill, and look at it; nothing else is touched.
  3. Documents: docker compose exec -T gitfence gitfence-restore-documents [latest|<key>] [dest] (default: the data volume). Safe on a running server: it only adds files that are not there.
  4. docker compose start gitfence

Keep instance.json, in the data volume and in your backups: a license bound to the installation id needs it.

8. Upgrades and rollback

  • Versions are pinned: GITFENCE_VERSION in .env. You move when you choose to.

  • Take a backup first (section 7).

  • Then change GITFENCE_VERSION and run docker compose pull && docker compose up -d.

  • Data migrations run at start, before anything is served. The log prints the data version and what changed. Each step is recorded as it finishes, so a failed upgrade resumes where it stopped on the next start.

  • Rollback: stop Gitfence, restore the backup taken before the upgrade, start the old version. An older Gitfence refuses data that a newer one has already migrated:

    This data was upgraded by Gitfence <version> (data version N); this is Gitfence <version> (data version N). Run that version or a newer one. To go back to this version, restore the backup you took before the upgrade.

  • Air-gapped: load the tarball you were given, then point GITFENCE_IMAGE at the loaded name (gitfence/selfhost):

    sha256sum -c SHA256SUMS --ignore-missing
    zstd -dc gitfence-selfhost-<version>-amd64.tar.zst | docker load
    

9. What we can and can't see

No telemetry, no usage reporting, ever. Gitfence never calls us: not to check the license, not to count seats, not to report errors or usage. There is no setting to turn it on because there is nothing to turn on. Your network policy can block all outbound traffic except GitHub (and your own mail relay, S3 and Vault) and Gitfence works exactly the same.

  • No phone-home, no license server. The license is checked locally, against a public key baked into the build.
  • Gitfence (the company) has no access to your servers, your GitHub App, your code or your data. The App is created in your GitHub organization and its private key never leaves your server.
  • What reaches us: the details of the license we issue for you (customer, seats, expiry, and, when the license is bound, the installation id or host name).
  • At run time your server talks only to these: github.com, through your App; your SMTP relay or Postmark, when email is on; your S3 bucket, for backups; your Vault, with the customer-managed key. One more, when you use Fenced CI: its install step (which you configure per repository) fetches your project's dependencies with network. The test itself runs with no network.
  • Support works from logs and screenshots you choose to share.

10. About the compiled build

The executable is compiled into one file without source maps or comments, and since 1.0.0-rc3 our own code in it is obfuscated to deter reverse engineering: identifiers are renamed, strings are moved into encoded tables, and control flow is flattened outside the clone, fetch and push paths. It is not a security boundary: a determined person with time can still recover how it works, because it has to run. Use is governed by the license agreement between us, not by cryptography; the signed license file and the baked-in key are a deterrent, not a wall. Obfuscation doesn't change what you can verify: it is deterministic, so the same source still gives byte-identical executables (VERIFY.md, section 6).

11. Troubleshooting

When the server refuses to start, read the reason: docker compose logs gitfence. The messages, and what to do:

Message What to do
Gitfence will not start: no license file at /run/gitfence/license.gfl. Put your license file at ./license.gfl, next to the compose file, and start again.
... is not valid: the file is not a Gitfence license / ... the file is damaged / ... its signature does not match: it was edited or damaged The file was truncated or changed. Use the file exactly as you received it. Check a file first: docker compose exec gitfence gitfence license check <file>.
... it was signed by a key this version of Gitfence does not trust The license was not made for this build. Ask Gitfence.
this license is for installation <id>, and this installation is <id> (see: gitfence license id) The license is bound to a different installation. Compare with docker compose exec gitfence gitfence license id.
this license is for <host>: set GITFENCE_URL to that address / this license is for <host>, and GITFENCE_URL is <host> Set GITFENCE_URL to the host the license names.
... the license file /run/gitfence/license.gfl cannot be read (permission denied) ... Gitfence runs as user 1000 in the container: chmod 644 license.gfl.
The GitHub App private key ... cannot be read (permission denied) / There is no GitHub App private key at ... sudo chown 1000:1000 secrets/github-app.pem, or put the App's key at ./secrets/github-app.pem (section 3.3).
Cannot write to /out as user 1000 (from github-app create) sudo chown 1000:1000 secrets and run it again. Nothing was created on GitHub.
The GitHub App is not installed on <owner>/<repo> (from repo add) Install your App on that repository (the .../installations/new link, or the App's page > Install App), then run it again.
GITFENCE_ENCRYPTION=fscrypt, but this filesystem cannot encrypt (...). It needs ext4 with the encrypt feature: tune2fs -O encrypt <device> Turn the encrypt feature on for the filesystem that holds the Docker volumes, move the data to such a filesystem, or set GITFENCE_ENCRYPTION=off and encrypt the disk yourself.
GITFENCE_CMK=<value> is not supported (supported: vault-transit) Use vault-transit, or unset GITFENCE_CMK.
GITFENCE_CMK=vault-transit needs VAULT_ADDR (or VAULT_TOKEN, GITFENCE_CMK_KEY) Set the named variable in gitfence.env and start again.
GITFENCE_CMK can only be turned on before encryption keys exist (a new installation). The customer-managed key must be set before the first start. Contact Gitfence about an existing installation.
This data was upgraded by Gitfence <version> (data version ...); this is Gitfence ... You are starting an older version than the data was migrated by. Start the newer version, or restore the backup taken before the upgrade (section 8).
This is the self-hosted build of Gitfence: it runs one organization. Remove GITFENCE_MODE=multi. Remove GITFENCE_MODE from gitfence.env.
GITFENCE_URL must be an http(s) address, e.g. https://gitfence.example.com Fix GITFENCE_URL: scheme and host.

While running:

Message (log or admin banner) What it means
License problem: <why>. The license checked earlier stays in force; replace the file with the one you received from Gitfence. The daily check could not take a new license from the file (removed, edited, not valid). The license checked earlier stays in force. Replace the file (section 4).
The customer-managed key is not available: Vault transit decrypt with key "..." was refused (HTTP ...). Every organization stays locked until it is. Check Vault: is the transit key enabled, is the token valid? When Vault answers again, access resumes on the next check.
Fenced CI sandbox: NOT available. ... The container can't build the sandbox: uncomment the security_opt line with all three values (section 6). Tests are not run; nothing else is affected.
Encryption at rest: organizations without a key stay UNENCRYPTED: GITFENCE_SECRET_KEY is not set (...) Set GITFENCE_SECRET_KEY (32 or more random characters) in gitfence.env and restart: the next start encrypts the organization and moves its code into the encrypted directory (section 5).
Encryption at rest: ... NO recovery key yet: an admin makes one in the web UI (Settings, Encryption) before <date> Sign in as an admin and make the recovery key (section 5).
This organization's code is locked. (web UI) Nobody's credential has opened it since the server started. Use Gitfence with a token or browser that opened it before, or paste the recovery key (admins).