# Installation gitpocket installs either as a container built from this repository or as a single binary under systemd. Both keep every piece of state in one data directory, `/srv/gitpocket` by default, and both expect nginx or another TLS proxy in front and the host's own sshd for git over SSH. ## Requirements A Linux host with git 2.38 or newer, which `merge-tree --write-tree` needs, and OpenSSH. Building from source needs Go 1.27 or newer, and the container route needs Docker with the compose plugin. 512 MB of memory is enough for a small instance. ## Prepare the host Create the account that owns the data and receives SSH logins. Its name appears in clone URLs, so `git` is the usual choice. ``` useradd -r -m -d /srv/gitpocket -s /bin/sh git ``` ## Docker The compose file runs the server as the `git` account with a read only root filesystem, no capabilities, `no-new-privileges`, a 512 MB memory ceiling and the port bound to loopback only. It mounts `/srv/gitpocket` from the host, so the data outlives the container. ``` git clone https://gitpocket.io/~gitpocket/gitpocket /opt/gitpocket cd /opt/gitpocket printf 'GITPOCKET_HOST=git.example.org\nGITPOCKET_SSH_HOST=ssh.git.example.org\nGITPOCKET_UID=%s\nGITPOCKET_GID=%s\n' $(id -u git) $(id -g git) > deploy/.env docker compose -f deploy/compose.yaml up -d --build ``` The server trusts `X-Forwarded-For` from loopback and from the compose network gateway, `172.31.255.1`, which is where connections from the host proxy appear. Change the subnet in `deploy/compose.yaml` if it collides with one you already use. SSH logins are answered on the host, so copy the binary out of the image to the same path it has inside it. Repository hooks name that path, and the container and the host must agree on it. ``` docker cp gitpocket:/usr/local/bin/gitpocket /usr/local/bin/gitpocket ``` ## Binary ``` git clone https://gitpocket.io/~gitpocket/gitpocket && cd gitpocket CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o gitpocket . install -m 755 gitpocket /usr/local/bin/gitpocket install -m 644 deploy/gitpocket.service /etc/systemd/system/ ``` Replace `git.example.org` in `/etc/systemd/system/gitpocket.service` with your hostname, then start it. ``` systemctl daemon-reload systemctl enable --now gitpocket ``` The unit sets `GOMEMLIMIT` to 192 MiB, caps the server and every git it starts at 512 MB, and allows writes only beneath `/srv/gitpocket`. ## Reverse proxy `deploy/nginx.conf` is a complete site that expects to sit behind the Cloudflare proxy, so the address of the host never appears in DNS and every request that does not arrive from a Cloudflare network is dropped by the included snippet before it reaches the server. ``` mkdir -p /etc/gitpocket /etc/nginx/snippets openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 3650 -subj /CN=origin -keyout /etc/gitpocket/origin.key -out /etc/gitpocket/origin.pem cp deploy/cloudflare.conf /etc/nginx/snippets/gitpocket-cloudflare.conf cp deploy/nginx.conf /etc/nginx/sites-available/gitpocket ln -s ../sites-available/gitpocket /etc/nginx/sites-enabled/ nginx -t && systemctl reload nginx ``` In the Cloudflare zone the records for `git.example.org` and `*.git.example.org` are proxied with the SSL mode set to Full, which lets the edge hold the public certificates while the origin keeps a long lived self signed pair that never needs renewing. A Cloudflare origin certificate can replace that pair when the zone runs in Full (strict) mode instead. The wildcard also covers `gist.git.example.org`, the cookie-less origin for raw gist files, and since `gist` is a reserved name it can never belong to a user site. Cloudflare only caches common static extensions by default, so a Cache Rule matching the host `gist.git.example.org` and the path wildcard `/raw/*/*/*`, set to eligible for cache with the origin's TTLs, makes pinned raw files load from the edge while unpinned ones still pass through. SSH cannot pass through the proxy, so publish an unproxied `ssh.git.example.org` record and start the server with `-ssh-host ssh.git.example.org` to show that name in clone URLs while accepting that this one record reveals the address. Custom domains reach the origin through Cloudflare for SaaS, where each verified domain becomes a custom hostname whose certificate Cloudflare issues once the owner points a CNAME at their subdomain. The script below runs every five minutes, writes the verified domains into `/etc/nginx/conf.d/gitpocket-domains.conf` and keeps the custom hostnames of the zone in step with them, reading `CF_TOKEN` and `CF_ZONE` from `/etc/gitpocket/cloudflare`, where the token needs the SSL and Certificates edit permission and `CF_SKIP` may list domains that are already proxied by a zone of your own. ``` install -m 755 deploy/domains.sh /usr/local/sbin/gitpocket-domains cp deploy/gitpocket-domains.service deploy/gitpocket-domains.timer /etc/systemd/system/ systemctl enable --now gitpocket-domains.timer ``` Whatever proxy you use has to forward `Host` and `X-Forwarded-Proto`, set `X-Forwarded-For` to the client address as recovered from `CF-Connecting-IP`, turn request buffering off and allow large bodies, otherwise bigger HTTPS pushes are cut short. The server takes the last address in `X-Forwarded-For`, so a client cannot pick its own rate limit bucket. ## SSH Add this to the end of `/etc/ssh/sshd_config` and reload sshd. ``` Match User git AuthenticationMethods publickey PasswordAuthentication no AuthorizedKeysFile none AuthorizedKeysCommand /usr/local/bin/gitpocket -root /srv/gitpocket keys %u %h %t %k AuthorizedKeysCommandUser root PermitTTY no DisableForwarding yes ``` `keys` opens the database read only and answers with a forced command of `gitpocket shell `, which checks the requested repository and permission before it execs `git upload-pack` or `git receive-pack`. Nothing is left running after the push. If the account is not called `git`, pass `-ssh-user` to `serve` so the clone URLs match. ## First run Open the site, sign up, then grant yourself administration and choose how others join. ``` sudo -u git gitpocket admin you sudo -u git gitpocket registration invite ``` Under Docker the same commands run as `docker exec gitpocket gitpocket admin you`. Registration starts open, `invite` hands out single use links from `/admin`, and `closed` leaves `gitpocket user ` as the only way in, with the password read from stdin. ## Configuration `serve` takes its settings as flags. | Flag | Default | Meaning | |---|---|---| | `-host` | `localhost` | public hostname used in clone URLs, mail and feeds | | `-name` | `gitpocket` | site name shown in the header | | `-listen` | `127.0.0.1:3200` | address the HTTP server binds | | `-ssh-user` | `git` | account shown in SSH clone URLs | | `-proxy` | none | comma separated networks, besides loopback, whose `X-Forwarded-For` is trusted | The data directory holds a few optional files, each read once at startup. `logo.png` replaces the header logo and `icon.png` the favicon. `smtp.json` enables mail, and `gitpocket quota 2G` sets a per owner disk quota that pushes are checked against. ## Mail Mail is optional. Without it nobody receives notifications and password resets go through `gitpocket passwd `. Plain SMTP takes `host`, `port`, `user`, `password`, `from` and `starttls`. Many VPS providers block outbound submission ports, so `api` and `key` post to a Resend compatible endpoint instead, and `account` and `token` use the Cloudflare email API. ``` {"api": "https://api.resend.com/emails", "key": "re_...", "from": "gitpocket "} ``` Save it as `/srv/gitpocket/smtp.json`, readable only by `git`, restart the server and send a test with `gitpocket mail you@example.org`. ## Backups `gitpocket backup ` writes a dated copy of the database with `vacuum into`, keeps the last fourteen, and bundles every repository whose refs changed since its previous bundle and copies new image attachments. The supplied timer runs it nightly into `/srv/gitpocket/backups`, which should then be copied off the host. ``` install -m 644 deploy/gitpocket-backup.service deploy/gitpocket-backup.timer /etc/systemd/system/ systemctl enable --now gitpocket-backup.timer ``` To restore, stop the server, put the newest database back as `gitpocket.db`, and `git clone --mirror` each bundle to `repos//.git` and copy `attachments` back. The server reinstalls repository hooks when it starts. ## Upgrading Back up first. Schema changes are applied automatically when the server starts. ``` cd /opt/gitpocket && git pull docker compose -f deploy/compose.yaml up -d --build docker cp gitpocket:/usr/local/bin/gitpocket /usr/local/bin/gitpocket ``` For a binary install, rebuild, `install` the new binary over the old one and `systemctl restart gitpocket`.