Fridai Redline: GitLab-host deployment runbook
Use this runbook to deploy fio-redline on the same Docker host as
glab.fio.sh and publish it at https://yqa.fio.sh through Cloudflare Access
and Cloudflare Tunnel.
Follow the sections in order. Each section identifies the system being configured, the exact value to record, and where that value is used later.
Deployment result
When complete, the request paths are:
Browser -> Cloudflare Access -> yqa.fio.sh -> Cloudflare Tunnel
-> fio-redline:8787
fio-redline -> glab.fio.sh-bridge
-> http://glab.fio.sh-server
The service has no published host port. Only cloudflared can reach it from
the ingress network. Only the service—not cloudflared—joins the GitLab
network.
Before you begin
You need:
- shell access to the GitLab Docker host;
- permission to run Docker and Docker Compose on that host;
- GitLab administrator access for the instance OAuth application;
- Maintainer or Owner access to each knowledge-base project;
- Cloudflare Zero Trust administration access for the
fio.shaccount; - a password manager for the values that GitLab and Cloudflare display once;
- a trusted checkout of this repository containing the
ops/directory.
Do not paste credentials into GitLab issues, terminal commands, shell history, or this repository.
Values created during this runbook
The GitLab OAuth application creates only two values: an application ID and an application secret. The other secrets in this table are application runtime secrets created later on the Docker host. They are not fields in the GitLab OAuth application.
| Value | Created by | Purpose | Destination |
|---|---|---|---|
| GitLab OAuth application ID | GitLab | Identifies this OAuth client | GITLAB_OAUTH_CLIENT_ID in service.env |
| GitLab OAuth application secret | GitLab | Authenticates this OAuth client during token exchange | secrets/gitlab-oauth-client-secret |
| GitLab project access token | GitLab | Read-only history and repository API access | secrets/gitlab-token |
| Registry deploy-token username and token | GitLab | Allows the host to pull this project’s container image | Host Docker credential store |
| Cloudflare Access team domain | Cloudflare | JWT issuer used by the service | ACCESS_TEAM_DOMAIN in service.env |
| Cloudflare Access AUD tag | Cloudflare | Binds JWTs to the yqa.fio.sh Access application | ACCESS_AUDIENCE in service.env |
| Cloudflare Tunnel token | Cloudflare | Connects the cloudflared container to the managed Tunnel | secrets/cloudflared-tunnel-token |
| Session secret | Generated on the host | Encrypts GitLab OAuth state and browser session cookies | secrets/session-secret |
| Draft-signing secret | Generated on the host | Signs service-created merge-request metadata so arbitrary MRs cannot be adopted | secrets/draft-signing-secret |
| Webhook secret | Generated on the host | Authenticates GitLab webhook requests | secrets/gitlab-webhook-secret and GitLab’s Secret token field |
Do not create the host-generated secrets while filling out the GitLab OAuth form. They belong to Step 7.
1. Verify the GitLab host topology
This runbook was checked against
fridai/fio-infra/fio-glab-infra-mgmt commit
4c38c42e4b95e577fd66fb92abee7fe3fd9e78a6 from 2026-07-28. That revision
defines:
- GitLab container
glab.fio.sh-server; - external Docker network
glab.fio.sh-bridge; - internal GitLab HTTP origin
http://glab.fio.sh-server; - public GitLab origin
https://glab.fio.sh; - public registry
glab.fio.sh:5050; - direct host registry port
glab.fio.sh:10505for CI uploads.
Log in to the GitLab Docker host and run:
docker network inspect glab.fio.sh-bridge
docker inspect glab.fio.sh-server --format '{{json .NetworkSettings.Networks}}'
curl --fail --silent --show-error http://127.0.0.1:10080/-/health
curl --include --silent --show-error https://glab.fio.sh:10505/v2/
Expected results:
- The network inspection succeeds.
glab.fio.sh-serveris attached toglab.fio.sh-bridge.- The GitLab health request succeeds.
- The registry request returns
401 Unauthorizedwith aDocker-Distribution-Api-Version: registry/2.0header. A401is correct because no registry credentials were supplied.
Stop here if any result differs. Reconcile the runbook with the current infrastructure repository before continuing.
2. Select an immutable container image
Select the image produced for the exact commit approved for deployment:
- Open the
fio-redlineproject in GitLab. - Go to Build > Pipelines and open the successful push pipeline for the commit being deployed.
- Open the successful
publish_containerjob. - Find the final
DEPLOY_IMAGE=line and confirm itssha-suffix matches the short SHA shown for the pipeline commit. - Copy the complete value after
DEPLOY_IMAGE=. Do not choose an arbitrary tag from the Container Registry page.
The value has this form:
glab.fio.sh:5050/fridai/fio-dep/fio-redline/fio-redline:sha-<short-commit-sha>
The publish job uploads and verifies this immutable tag through the canonical
registry endpoint before printing DEPLOY_IMAGE. If that remote manifest
check fails, the job fails and does not produce a deployable image.
Create a dedicated credential for the deployment host:
- In the
fio-redlineproject, select Settings > Repository. - Expand Deploy tokens and select Add token.
- Set the name to
fio-redline-host. - Set an approved expiry date.
- Select only the
read_registryscope. - Select Create deploy token.
- Copy both the generated username and token immediately. Record them in your
password manager as
REGISTRY_DEPLOY_USERandREGISTRY_DEPLOY_TOKEN.
From a machine that can reach glab.fio.sh, authenticate with those values and
verify that the manifest exists:
docker login glab.fio.sh:5050 --username <REGISTRY_DEPLOY_USER>
docker manifest inspect glab.fio.sh:5050/fridai/fio-dep/fio-redline/fio-redline:sha-<short-commit-sha>
When docker login prompts for a password, paste REGISTRY_DEPLOY_TOKEN. Do
not put the token directly on the command line.
Replace the example tag with the selected tag. Do not deploy a branch,
main, next, or latest tag because those tags can move.
Registry endpoint roles
Deployment hosts and operators use only the canonical registry endpoint on
glab.fio.sh:5050. CI also verifies every published immutable tag through
that endpoint.
The runner currently uploads through the GitLab container’s host-published
TLS endpoint on glab.fio.sh:10505. This is an infrastructure boundary, not a
deployable image address: the host nginx proxy on :5050 drops the registry
port from upload-continuation URLs. Do not copy a :10505 reference into
Compose. The split can be removed after host nginx preserves $http_host or
GitLab Registry is configured to return relative upload URLs.
3. Create the GitLab OAuth application
This application allows each signed-in person to connect their own GitLab identity. GitLab then records comments, approvals, edits, and merges as that person—not as the service account.
-
Sign in to
https://glab.fio.shas an administrator. -
Select Admin in the upper-right corner.
-
In the Admin sidebar, select Applications.
-
Select New application.
-
Enter these values:
Field Value Name Fridai RedlineRedirect URI https://yqa.fio.sh/v1/auth/gitlab/callbackConfidential Selected Trusted Not selected Scope apionlyLeave Trusted unselected so users see and approve the authorization request. The
apiscope is required because GitLab does not provide a narrower OAuth scope covering the required comment, branch, approval, and merge APIs. -
Select Save application.
-
Copy the value labelled Application ID. Record it as
GITLAB_OAUTH_CLIENT_ID. -
Copy the value labelled Secret immediately. GitLab may not display it again. Store it temporarily in your password manager as
GITLAB_OAUTH_CLIENT_SECRET.
At the end of this step you should have exactly two values. Do not generate a session secret, draft-signing secret, or webhook secret in GitLab.
4. Create the read-only GitLab project token
This token serves repository history and merge-request data before a user connects their own GitLab account. It must not be used for writes.
-
Open the GitLab project containing the knowledge base. For YYZ, open
fridai/fio-kb/yyz. -
Select Settings > Access tokens.
-
Select Add new token.
-
Enter these values:
Field Value Token name fio-redline-readDescription Read-only history access for Fridai RedlineExpiration date An approved date within the instance maximum Role ReporterScope read_apionly -
Select Create project access token.
-
Copy the token immediately; GitLab displays it once. Store it temporarily in your password manager as
GITLAB_TOKEN. -
Add the expiration date to the operational calendar with reminders 30 and 7 days before expiry.
Do not select api, write_repository, or registry-write scopes for this
token. User mutations use OAuth tokens from Step 3.
5. Create the Cloudflare Access policy
Cloudflare Access is the outer login gate for yqa.fio.sh. The service also
validates the Access JWT at the origin. This step defines authentication and
authorization; Step 6 makes the hostname route to the container.
-
Sign in to the Cloudflare dashboard and select the account containing
fio.sh. -
Go to Zero Trust > Access controls > Applications.
-
Select Create new application.
-
Select Self-hosted and private.
-
Set the application name to
Fridai Redline. -
Add the public hostname
yqa.fio.sh. Leave the path blank so the policy covers every API route. -
Set an application session duration appropriate for the knowledge base.
-
Add an Allow policy for the same users or identity-provider groups that may access the YYZ knowledge base.
-
Do not add a Bypass policy.
-
Select Create.
-
Return to Access controls > Applications, find this application, and select Configure.
-
Open Additional settings and copy the Application Audience (AUD) Tag. Record it as
ACCESS_AUDIENCE. -
Go to Zero Trust > Settings and copy the team domain. Record the full HTTPS origin as
ACCESS_TEAM_DOMAIN, for example:https://your-team-name.cloudflareaccess.com
The AUD tag identifies this specific Access application. The team domain is the JWT issuer. They are not secrets, but both must match the JWT sent to the service.
Do not manually add a DNS record for yqa.fio.sh in this step. If the current
Cloudflare workflow creates or reuses a DNS record while adding the public
hostname, leave the Access application in place and reconcile that one DNS
record in Step 6. The Access application and the Tunnel route are separate
objects even though both refer to yqa.fio.sh.
6. Create the Cloudflare Tunnel and its DNS route
The Access application from Step 5 owns the authentication policy. The published application route created here owns both the Tunnel ingress mapping and the DNS CNAME. Do not try to make both workflows own the DNS record.
6.1 Create the tunnel and capture its token
- In Cloudflare Zero Trust, go to Networks > Tunnels & Mesh.
- Select Create a tunnel.
- Select the
cloudflaredconnector type. - Name the tunnel
fio-redlineand save it. - On the connector installation screen, select Docker.
- Copy the generated Docker command into a temporary text editor. Copy only
the long
eyJ...value following--tokenand record it in the password manager asCLOUDFLARED_TUNNEL_TOKEN. - Do not run the generated Docker command. Compose starts
cloudflaredlater. - Record the Tunnel ID shown on the tunnel overview. Its DNS target will be
<TUNNEL_ID>.cfargotunnel.com.
6.2 Remove the conflicting yqa.fio.sh DNS record
The Add published application route action automatically creates a DNS
record. Cloudflare rejects that action while any A, AAAA, or CNAME record
already owns the same hostname.
- Leave the Tunnel form open in its browser tab.
- Open another tab, select Back to Fridai, open the
fio.shzone, and go to DNS > Records. - Search for the exact name
yqa.fio.sh. - If the record was created during this setup and
yqa.fio.shhad no earlier production owner, record its type and content in the change log, then delete that exact DNS record. Do not delete theFridai RedlineAccess application. - If more than one DNS record has the exact name
yqa.fio.sh, remove each one only when all of them were created by this setup and have no earlier owner. - If any matching record predates this setup or has unknown ownership, stop and resolve ownership before deleting it.
Deleting the setup-created DNS record does not remove the Access application. During the short interval before the Tunnel route is saved, the hostname has no route rather than an unprotected route.
6.3 Complete the current Cloudflare route form
Return to Networks > Tunnels & Mesh, open fio-redline, and select
Routes > Add route > Published application. Fill the visible fields exactly
as follows:
| Form section | Cloudflare field | Value |
|---|---|---|
| Hostname | Subdomain | yqa |
| Hostname | Domain | fio.sh |
| Hostname | Path | Leave empty |
| Service | Type | HTTP |
| Service | URL | fio-redline:8787 |
Do not put http:// in the URL field; the separate Type control supplies
the protocol. The container name contains hyphens—never enter
fio-redline.
Leave Origin request and connection settings at their defaults. There is no
Protect with Access action required on this form. The Access application
created in Step 5 protects yqa.fio.sh, and the service validates the
Cf-Access-Jwt-Assertion itself.
Select Add route. Cloudflare should create one proxied CNAME whose target
is <TUNNEL_ID>.cfargotunnel.com.
6.4 Resolve a repeated duplicate-DNS error
If Cloudflare still reports A DNS record with this name already exists, do
not keep submitting the form:
- Return to DNS > Records and search again for
yqa.fio.sh. - If a same-name record reappeared, identify the Cloudflare product that owns
it before deleting anything:
- check Workers & Pages > Custom domains for
yqa.fio.sh; - check every existing Tunnel’s published application routes for
yqa.fio.sh; - check whether another administrator or automation recreated the record.
- check Workers & Pages > Custom domains for
- Detach
yqa.fio.shfrom that old product only if this deployment is its approved replacement. - Remove the released DNS record, return to the route form, and submit it once.
6.5 Verify the result
-
Go to DNS > Records and confirm there is exactly one proxied record for
yqa.fio.shand that its target contains the Tunnel ID recorded in 6.1. -
Return to the tunnel’s Routes tab and confirm the mapping is:
yqa.fio.sh -> http://fio-redline:8787 -
Return to Access controls > Applications and confirm
Fridai Redline Redlinestill protectsyqa.fio.shwith the intended Allow policy.
Do not change the existing glab.fio.sh A record; it must continue pointing to
the Tailscale address.
If you need to retrieve the token later, open the tunnel and select Add a replica, then copy the token from the displayed installation command.
7. Create the service runtime secrets
These three values are created by the service operator. They have no corresponding fields in the GitLab OAuth application:
session-secretencrypts OAuth state and the browser’s GitLab session;draft-signing-secretsigns the hidden ownership record added to service-created merge requests;gitlab-webhook-secretis copied into both a host file and GitLab’s webhook Secret token field.
On the GitLab Docker host, create the deployment directory and generate the three values:
sudo install -d -m 0700 -o "$USER" -g "$(id -gn)" /srv/fio-redline
install -d -m 0700 /srv/fio-redline/secrets
openssl rand -hex 32 -out /srv/fio-redline/secrets/session-secret
openssl rand -hex 32 -out /srv/fio-redline/secrets/draft-signing-secret
openssl rand -hex 32 -out /srv/fio-redline/secrets/gitlab-webhook-secret
chmod 0600 /srv/fio-redline/secrets/session-secret
chmod 0600 /srv/fio-redline/secrets/draft-signing-secret
chmod 0600 /srv/fio-redline/secrets/gitlab-webhook-secret
Each command writes 32 random bytes encoded as 64 hexadecimal characters. The service requires at least 32 characters for the first two values.
Create empty protected files for the three values copied from GitLab and Cloudflare:
install -m 0600 /dev/null /srv/fio-redline/secrets/gitlab-token
install -m 0600 /dev/null /srv/fio-redline/secrets/gitlab-oauth-client-secret
install -m 0600 /dev/null /srv/fio-redline/secrets/cloudflared-tunnel-token
Open each file with a host-side editor and paste only the matching value:
${EDITOR:-vi} /srv/fio-redline/secrets/gitlab-token
${EDITOR:-vi} /srv/fio-redline/secrets/gitlab-oauth-client-secret
${EDITOR:-vi} /srv/fio-redline/secrets/cloudflared-tunnel-token
An optional final newline is accepted. Do not add labels, quotes, or variable names to these files.
8. Install and configure the deployment files
From a trusted checkout of this repository, copy the three deployment files to the host:
install -m 0644 ops/docker-compose.yml /srv/fio-redline/docker-compose.yml
install -m 0600 ops/.env.example /srv/fio-redline/.env
install -m 0600 ops/service.env.example /srv/fio-redline/service.env
Edit /srv/fio-redline/.env. Replace the placeholder tag with the
immutable image selected in Step 2:
FIO_REDLINE_IMAGE=glab.fio.sh:5050/fridai/fio-dep/fio-redline/fio-redline:sha-5ac8b4d5
GLAB_DOCKER_NETWORK=glab.fio.sh-bridge
Edit /srv/fio-redline/service.env and replace every placeholder:
GITLAB_PUBLIC_URL=https://glab.fio.sh
GITLAB_INTERNAL_URL=http://glab.fio.sh-server
GITLAB_OAUTH_CLIENT_ID=<Application ID from Step 3>
PUBLIC_SERVICE_URL=https://yqa.fio.sh
ALLOW_ANONYMOUS_READ=false
REQUIRE_ACCESS=true
WRITE_ENABLED=false
ACCESS_TEAM_DOMAIN=<full HTTPS team domain from Step 5>
ACCESS_AUDIENCE=<AUD tag from Step 5>
SITES_JSON={"yyz":{"gitlabProjectId":5,"gitlabProjectPath":"fridai/fio-kb/yyz","defaultBranch":"main","contentRoots":["docs"],"allowedOrigins":["https://yyz.fio.sh"],"allowedAccessEmailDomains":["fridai.com","fio.com"],"features":{"comments":true,"editing":true,"approvals":true,"merging":false}}}
API_RATE_LIMIT=120
API_RATE_PERIOD_SECONDS=60
WRITE_RATE_LIMIT=30
WRITE_RATE_PERIOD_SECONDS=60
WEBHOOK_RATE_LIMIT=60
WEBHOOK_RATE_PERIOD_SECONDS=60
For a project other than YYZ, obtain its numeric project ID from the GitLab
project overview and replace gitlabProjectId, gitlabProjectPath,
contentRoots, and allowedOrigins. allowedOrigins contains the browser
origins allowed to call this API; it is not the GitLab URL.
Keep WRITE_ENABLED=false and features.merging=false for the initial
deployment.
Verify that every required file exists without printing its contents:
cd /srv/fio-redline
test -s secrets/gitlab-token
test -s secrets/gitlab-oauth-client-secret
test -s secrets/cloudflared-tunnel-token
test -s secrets/session-secret
test -s secrets/draft-signing-secret
test -s secrets/gitlab-webhook-secret
docker compose config --quiet
Every command must exit with status 0 before continuing.
9. Pull and start the containers
Use the registry deploy-token username and token created in Step 2 to log the host into the project registry:
cd /srv/fio-redline
docker login glab.fio.sh:5050 --username <REGISTRY_DEPLOY_USER>
docker compose pull
docker compose up --detach
docker compose ps
Expected state:
fio-redlineishealthy;fio-redline-cloudflaredis running;- neither container shows a published host port.
If the service is unhealthy, inspect only its recent logs:
docker compose logs --tail 100 service
Do not enable debug logging. Request headers can contain Access assertions, cookies, and authorization credentials.
10. Verify internal and external connectivity
Run these checks on the Docker host:
docker exec fio-redline node -e "fetch('http://127.0.0.1:8787/v1/health').then(async r=>{console.log(r.status,await r.text());process.exit(r.ok?0:1)})"
docker exec fio-redline node -e "fetch('http://glab.fio.sh-server/-/health').then(async r=>{console.log(r.status,await r.text());process.exit(r.ok?0:1)})"
docker logs --tail 100 fio-redline-cloudflared
The first two commands must print HTTP 200. In Cloudflare, open Networks >
Tunnels & Mesh, select fio-redline, and confirm the connector status
is Healthy.
Open a private browser window that has no Cloudflare Access session and visit:
https://yqa.fio.sh/v1/health
Cloudflare must present the Access login screen. After signing in as an allowed user, the endpoint must return JSON containing:
{"ok":true,"service":"fio-redline","apiVersion":"1"}
Stop and correct Access before continuing if the endpoint is anonymously reachable.
11. Test the GitLab OAuth flow
The user performing this test must be on Tailscale because the browser is
redirected to https://glab.fio.sh.
-
Sign in through Cloudflare Access at
yqa.fio.sh. -
Open this URL, replacing the return URL if necessary:
https://yqa.fio.sh/v1/auth/gitlab/start?returnTo=https%3A%2F%2Fyyz.fio.sh%2F -
Confirm that the browser is redirected to the GitLab authorization screen for Fridai Redline.
-
Authorize the application.
-
Confirm that GitLab redirects to
https://yqa.fio.sh/v1/auth/gitlab/callbackand then to the requested YYZ return URL. -
While still signed in through Access, open:
https://yqa.fio.sh/v1/auth/gitlab/status -
Confirm the response contains
"connected":trueand the expected GitLab username.
If GitLab reports a redirect mismatch, compare the callback character for character with Step 3. If the service reports an identity mismatch, confirm the same person completed both Cloudflare Access and GitLab authorization.
12. Create and test the GitLab webhook
Create the webhook only after the service is healthy. GitLab sends this hook
directly over glab.fio.sh-bridge; it does not traverse Cloudflare Access.
-
Open the knowledge-base project in GitLab.
-
Select Settings > Webhooks.
-
Select Add new webhook.
-
Enter:
Field Value Name Fridai Redline cache invalidationURL http://fio-redline:8787/v1/webhooks/gitlabSecret token Contents of /srv/fio-redline/secrets/gitlab-webhook-secretEnable SSL verification Not applicable to this internal HTTP URL -
Select these triggers:
- Push events
- Tag push events
- Comments
- Merge request events
- Pipeline events
- Job events
-
Select Add webhook.
-
From the webhook’s Test menu, select Push events.
-
Open Recent events and confirm the delivery received HTTP
202.
If GitLab blocks the internal URL, verify that the instance setting allowing requests from webhooks and integrations to the local network remains enabled. That setting is present in the referenced infrastructure configuration.
13. Enable write operations deliberately
Do not enable writes until Steps 9 through 12 pass.
-
Edit
/srv/fio-redline/service.env. -
Change only:
WRITE_ENABLED=true -
Keep
"merging":falseinSITES_JSON. -
Recreate the service container:
cd /srv/fio-redline docker compose up --detach --force-recreate service docker compose ps -
Create a test document draft through the client UI.
-
Confirm GitLab creates a branch and draft merge request under the connected user’s identity.
-
Approve the test MR through the client UI.
-
Confirm the approval is attributed to the connected GitLab user.
-
Add a new commit to the MR and confirm an approval using the old HEAD SHA is rejected.
Enable features.merging only after comments, replies, resolution, approval,
pipeline checks, stale-SHA rejection, and conflict handling have all passed.
14. Update and roll back
Record the current immutable image before every update. To deploy a new image:
-
Edit
/srv/fio-redline/.envand replace onlyFIO_REDLINE_IMAGE. -
Run:
cd /srv/fio-redline docker compose pull service docker compose up --detach service docker compose ps -
Repeat the health and OAuth status checks from Steps 10 and 11.
To roll back, restore the previous immutable image reference and repeat the same commands. The service has no persistent application volume; GitLab is the system of record.
15. Credential rotation and incident response
- Rotating
gitlab-tokeninterrupts reads until the host file is updated and the service container is recreated. - Rotating the registry deploy token requires another
docker loginon the host before the next image pull. - Renewing the GitLab OAuth application secret interrupts OAuth token exchange
until
gitlab-oauth-client-secretis updated and the service is recreated. - Rotating
session-secretimmediately invalidates all browser GitLab sessions. Users must connect GitLab again. - Rotating
draft-signing-secretprevents the service from managing existing service-created MRs. Plan a migration or close those MRs first. - Rotating
gitlab-webhook-secretrequires updating both the host file and the GitLab webhook Secret token field. - Rotating the Cloudflare Tunnel token requires updating
cloudflared-tunnel-tokenand recreating thecloudflaredcontainer.
For immediate containment:
- Set
WRITE_ENABLED=false. - Recreate the service container.
- Remove or disable the Cloudflare Tunnel route if browser ingress must stop.
- Revoke the GitLab OAuth application or project token if either is suspected compromised.
- Rotate only the affected credentials, then repeat the relevant verification steps.