Skip to content
Configuration

Configuration

Every setting is a command-line flag and an environment variable, so no deployment target needs a config file or a code change. A flag wins over the matching variable; unset falls back to the default.

Everything is validated at startup, all at once. A misconfigured deployment reports every problem it has in one message rather than one per restart.

Required

The app refuses to start without these six.

FlagEnvironmentWhat it is
--gitlab-urlGITLAB_URLBase URL of the GitLab instance to mirror, e.g. https://gitlab.example.com
--gitlab-read-tokenGITLAB_READ_TOKENToken with read_api scope, used for all data fetching
--gitlab-write-tokenGITLAB_WRITE_TOKENToken with api scope on an instance-admin account, used to manage webhooks and achievements. Must differ from the read token
--achievements-namespaceACHIEVEMENTS_NAMESPACEFull path of the group owning the achievement definitions, e.g. achievements
--webhook-secretWEBHOOK_SECRETShared secret GitLab presents on every delivery, at least 32 characters, e.g. from openssl rand -hex 32
--public-urlPUBLIC_URLPublic base URL of this app, used for GitLab OAuth redirects and browser sessions

--database-dsn / DATABASE_DSN is not in this list: left unset, it defaults to a local SQLite file, see Databases.

Where the two credentials come from, and what trusting the admin one implies, is in GitLab-side setup.

Optional

FlagEnvironmentDefaultWhat it does
--database-dsnDATABASE_DSNsqlite:///var/lib/gitlab-achievements/data.dbWhere state is kept, see Databases
--webhook-urlWEBHOOK_URLPUBLIC_URLBase URL GitLab reaches for webhooks. Trailing slashes are stripped and the webhook path is appended. Set this to an in-cluster URL when GitLab can reach the cluster but users cannot
--listen-addrLISTEN_ADDR:8080Address the HTTP server binds to
--metrics-addrMETRICS_ADDR:9464Address Prometheus metrics are served on, apart from --listen-addr. off serves none. See Metrics
--log-levelLOG_LEVELinfodebug, info, warn, error
--hook-scopeHOOK_SCOPEautoauto, group, project, see Webhooks
--hook-rateHOOK_RATE20Groups or projects per second the hourly registration sweep works through
--backfillBACKFILLautoauto, off, force, see Historical backfill
--backfill-sinceBACKFILL_SINCE(unset)How far back history is walked: a date (2024-01-01) or a duration (720h). Unset walks everything
--backfill-rateBACKFILL_RATE5Requests per second the historical walk may issue. Also caps the reconciliation sync
--reconcileRECONCILEautoauto, off, see Reconciliation
--reconcile-intervalRECONCILE_INTERVAL24hHow often recent activity is re-read to heal lost webhook deliveries
--reconcile-lookbackRECONCILE_LOOKBACK48hHow far back each pass reaches. Must exceed --reconcile-interval
--api-authAPI_AUTHnonenone, or gitlab to verify a GitLab token on every API request
--oauth-client-idOAUTH_CLIENT_ID(unset)Client ID of an OAuth application you registered by hand. Unset lets the app register a public one for itself
--oauth-client-secretOAUTH_CLIENT_SECRET(unset)Secret for that client ID, making it a confidential client. Requires --oauth-client-id
--exclude-botsEXCLUDE_BOTStrueLeave out accounts GitLab flags as bots. See Who takes part
--exclude-usersEXCLUDE_USERS(unset)Usernames or GitLab user IDs to leave out, comma-separated
--exclude-username-patternEXCLUDE_USERNAME_PATTERN(unset)Regular expression; matching usernames are left out
--allow-opt-outALLOW_OPT_OUTtrueLet users leave themselves out through the API. Needs --api-auth gitlab
--session-secretSESSION_SECRET(required with API_AUTH=gitlab)Key the GitLab tokens browser sessions hold are encrypted under, at least 32 characters, e.g. from openssl rand -base64 48. Changing it signs everybody out
--session-lifetimeSESSION_LIFETIME168hHow long a browser stays signed in, greater than 0. Sessions renew their GitLab token as it runs out
--api-auth-cache-ttlAPI_AUTH_CACHE_TTL1mHow long a verified credential is trusted before GitLab is asked again, and so how long a revoked token keeps working. 0s disables, at most 1h
--api-allowed-groupAPI_ALLOWED_GROUP(unset)Full path of a group; only its members and instance admins may use the API. Needs --api-auth gitlab
--oauth-return-originsOAUTH_RETURN_ORIGINS(unset)Origins besides --public-url’s that a login may return to, comma-separated, e.g. https://rankings.example.com
--frontend-dirFRONTEND_DIR(unset)Directory of a built static frontend served at / in place of the built-in one. See The frontend
--frontend-cspFRONTEND_CSP(the built-in frontend’s)Content Security Policy sent with FRONTEND_DIR; off sends none

Subcommands

All of them take the same flags and environment as the server.

CommandWhat it does
gitlab-achievementsThe server: bootstrap, ingestion, backfill, reconciliation, the API
gitlab-achievements backfillBootstraps, then walks history once and exits. See Historical backfill
gitlab-achievements reconcileRe-reads recent activity once and exits. Does not bootstrap. See Reconciliation
gitlab-achievements uninstallRemoves the webhooks and achievements from GitLab. See Uninstalling

Databases

The DBMS is picked from the DSN’s scheme:

SchemeExample
postgres://, postgresql://postgres://user:password@localhost:5432/achievements?sslmode=require
sqlite://, sqlite3://sqlite:///var/lib/gitlab-achievements/data.db
mysql://mysql://user:password@tcp(localhost:3306)/achievements?parseTime=true
sqlserver://sqlserver://user:password@localhost:1433?database=achievements

SQLite is the default: nothing to stand up alongside the app, at the cost of being a single file rather than a service, so it belongs on backed-up storage rather than in a container that will be replaced (see the Helm chart’s PVC for the Kubernetes case). PostgreSQL, MySQL, and SQL Server are the scale-up path once one process and one file stop being enough, or where operational convention already points at a managed database.

SQLite is opened with busy_timeout(10000), journal_mode(WAL) and _txlock=immediate unless the DSN sets them itself. The app always has several writers at once (the webhook workers, the backfill, the sweeps), and without these SQLite answers contention with SQLITE_BUSY rather than a wait. Override any of them in the DSN’s query string, e.g. sqlite:///data/data.db?_pragma=journal_mode(DELETE) on a filesystem that cannot hold WAL’s shared-memory file (some network filesystems). The SQLite driver stops reading the query string at _time_format, so a DSN that sets it loses _txlock whether the app added it or you did.

Migrations run at startup against whichever you pick. The schema is created on first start, so an empty database is all that has to exist.

Tuning notes

--hook-rate is what keeps the hourly sweep from being a permanent background load on your instance. On Free/CE it touches every project there is. Lower it on a busy instance; a slower sweep only means a hook deleted out of band is repaired a little later.

--backfill-rate is low for a reason. The walk is the heaviest read workload the app ever runs against an instance it does not own, and it has no deadline. Raising it shortens the cold start at the cost of API capacity your instance exists to serve.

--reconcile-lookback has to be wider than --reconcile-interval so consecutive windows overlap. Windows that merely abut lose anything GitLab timestamps on the far side of the boundary, and nothing would report the loss, so the app refuses to start on a configuration where they do.

Endpoints

Everything is served on --listen-addr, on one port. The list is in The API.