RedlineKnowledge base

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.sh account;
  • 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.

ValueCreated byPurposeDestination
GitLab OAuth application IDGitLabIdentifies this OAuth clientGITLAB_OAUTH_CLIENT_ID in service.env
GitLab OAuth application secretGitLabAuthenticates this OAuth client during token exchangesecrets/gitlab-oauth-client-secret
GitLab project access tokenGitLabRead-only history and repository API accesssecrets/gitlab-token
Registry deploy-token username and tokenGitLabAllows the host to pull this project’s container imageHost Docker credential store
Cloudflare Access team domainCloudflareJWT issuer used by the serviceACCESS_TEAM_DOMAIN in service.env
Cloudflare Access AUD tagCloudflareBinds JWTs to the yqa.fio.sh Access applicationACCESS_AUDIENCE in service.env
Cloudflare Tunnel tokenCloudflareConnects the cloudflared container to the managed Tunnelsecrets/cloudflared-tunnel-token
Session secretGenerated on the hostEncrypts GitLab OAuth state and browser session cookiessecrets/session-secret
Draft-signing secretGenerated on the hostSigns service-created merge-request metadata so arbitrary MRs cannot be adoptedsecrets/draft-signing-secret
Webhook secretGenerated on the hostAuthenticates GitLab webhook requestssecrets/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:10505 for 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:

  1. The network inspection succeeds.
  2. glab.fio.sh-server is attached to glab.fio.sh-bridge.
  3. The GitLab health request succeeds.
  4. The registry request returns 401 Unauthorized with a Docker-Distribution-Api-Version: registry/2.0 header. A 401 is 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:

  1. Open the fio-redline project in GitLab.
  2. Go to Build > Pipelines and open the successful push pipeline for the commit being deployed.
  3. Open the successful publish_container job.
  4. Find the final DEPLOY_IMAGE= line and confirm its sha- suffix matches the short SHA shown for the pipeline commit.
  5. 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:

  1. In the fio-redline project, select Settings > Repository.
  2. Expand Deploy tokens and select Add token.
  3. Set the name to fio-redline-host.
  4. Set an approved expiry date.
  5. Select only the read_registry scope.
  6. Select Create deploy token.
  7. Copy both the generated username and token immediately. Record them in your password manager as REGISTRY_DEPLOY_USER and REGISTRY_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.

  1. Sign in to https://glab.fio.sh as an administrator.

  2. Select Admin in the upper-right corner.

  3. In the Admin sidebar, select Applications.

  4. Select New application.

  5. Enter these values:

    FieldValue
    NameFridai Redline
    Redirect URIhttps://yqa.fio.sh/v1/auth/gitlab/callback
    ConfidentialSelected
    TrustedNot selected
    Scopeapi only

    Leave Trusted unselected so users see and approve the authorization request. The api scope is required because GitLab does not provide a narrower OAuth scope covering the required comment, branch, approval, and merge APIs.

  6. Select Save application.

  7. Copy the value labelled Application ID. Record it as GITLAB_OAUTH_CLIENT_ID.

  8. 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.

  1. Open the GitLab project containing the knowledge base. For YYZ, open fridai/fio-kb/yyz.

  2. Select Settings > Access tokens.

  3. Select Add new token.

  4. Enter these values:

    FieldValue
    Token namefio-redline-read
    DescriptionRead-only history access for Fridai Redline
    Expiration dateAn approved date within the instance maximum
    RoleReporter
    Scoperead_api only
  5. Select Create project access token.

  6. Copy the token immediately; GitLab displays it once. Store it temporarily in your password manager as GITLAB_TOKEN.

  7. 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.

  1. Sign in to the Cloudflare dashboard and select the account containing fio.sh.

  2. Go to Zero Trust > Access controls > Applications.

  3. Select Create new application.

  4. Select Self-hosted and private.

  5. Set the application name to Fridai Redline.

  6. Add the public hostname yqa.fio.sh. Leave the path blank so the policy covers every API route.

  7. Set an application session duration appropriate for the knowledge base.

  8. Add an Allow policy for the same users or identity-provider groups that may access the YYZ knowledge base.

  9. Do not add a Bypass policy.

  10. Select Create.

  11. Return to Access controls > Applications, find this application, and select Configure.

  12. Open Additional settings and copy the Application Audience (AUD) Tag. Record it as ACCESS_AUDIENCE.

  13. 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

  1. In Cloudflare Zero Trust, go to Networks > Tunnels & Mesh.
  2. Select Create a tunnel.
  3. Select the cloudflared connector type.
  4. Name the tunnel fio-redline and save it.
  5. On the connector installation screen, select Docker.
  6. Copy the generated Docker command into a temporary text editor. Copy only the long eyJ... value following --token and record it in the password manager as CLOUDFLARED_TUNNEL_TOKEN.
  7. Do not run the generated Docker command. Compose starts cloudflared later.
  8. 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.

  1. Leave the Tunnel form open in its browser tab.
  2. Open another tab, select Back to Fridai, open the fio.sh zone, and go to DNS > Records.
  3. Search for the exact name yqa.fio.sh.
  4. If the record was created during this setup and yqa.fio.sh had no earlier production owner, record its type and content in the change log, then delete that exact DNS record. Do not delete the Fridai Redline Access application.
  5. 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.
  6. 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 sectionCloudflare fieldValue
HostnameSubdomainyqa
HostnameDomainfio.sh
HostnamePathLeave empty
ServiceTypeHTTP
ServiceURLfio-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:

  1. Return to DNS > Records and search again for yqa.fio.sh.
  2. 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.
  3. Detach yqa.fio.sh from that old product only if this deployment is its approved replacement.
  4. Remove the released DNS record, return to the route form, and submit it once.

6.5 Verify the result

  1. Go to DNS > Records and confirm there is exactly one proxied record for yqa.fio.sh and that its target contains the Tunnel ID recorded in 6.1.

  2. Return to the tunnel’s Routes tab and confirm the mapping is:

    yqa.fio.sh -> http://fio-redline:8787
  3. Return to Access controls > Applications and confirm Fridai Redline Redline still protects yqa.fio.sh with 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-secret encrypts OAuth state and the browser’s GitLab session;
  • draft-signing-secret signs the hidden ownership record added to service-created merge requests;
  • gitlab-webhook-secret is 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-redline is healthy;
  • fio-redline-cloudflared is 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.

  1. Sign in through Cloudflare Access at yqa.fio.sh.

  2. 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
  3. Confirm that the browser is redirected to the GitLab authorization screen for Fridai Redline.

  4. Authorize the application.

  5. Confirm that GitLab redirects to https://yqa.fio.sh/v1/auth/gitlab/callback and then to the requested YYZ return URL.

  6. While still signed in through Access, open:

    https://yqa.fio.sh/v1/auth/gitlab/status
  7. Confirm the response contains "connected":true and 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.

  1. Open the knowledge-base project in GitLab.

  2. Select Settings > Webhooks.

  3. Select Add new webhook.

  4. Enter:

    FieldValue
    NameFridai Redline cache invalidation
    URLhttp://fio-redline:8787/v1/webhooks/gitlab
    Secret tokenContents of /srv/fio-redline/secrets/gitlab-webhook-secret
    Enable SSL verificationNot applicable to this internal HTTP URL
  5. Select these triggers:

    • Push events
    • Tag push events
    • Comments
    • Merge request events
    • Pipeline events
    • Job events
  6. Select Add webhook.

  7. From the webhook’s Test menu, select Push events.

  8. 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.

  1. Edit /srv/fio-redline/service.env.

  2. Change only:

    WRITE_ENABLED=true
  3. Keep "merging":false in SITES_JSON.

  4. Recreate the service container:

    cd /srv/fio-redline
    docker compose up --detach --force-recreate service
    docker compose ps
  5. Create a test document draft through the client UI.

  6. Confirm GitLab creates a branch and draft merge request under the connected user’s identity.

  7. Approve the test MR through the client UI.

  8. Confirm the approval is attributed to the connected GitLab user.

  9. 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:

  1. Edit /srv/fio-redline/.env and replace only FIO_REDLINE_IMAGE.

  2. Run:

    cd /srv/fio-redline
    docker compose pull service
    docker compose up --detach service
    docker compose ps
  3. 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-token interrupts reads until the host file is updated and the service container is recreated.
  • Rotating the registry deploy token requires another docker login on the host before the next image pull.
  • Renewing the GitLab OAuth application secret interrupts OAuth token exchange until gitlab-oauth-client-secret is updated and the service is recreated.
  • Rotating session-secret immediately invalidates all browser GitLab sessions. Users must connect GitLab again.
  • Rotating draft-signing-secret prevents the service from managing existing service-created MRs. Plan a migration or close those MRs first.
  • Rotating gitlab-webhook-secret requires updating both the host file and the GitLab webhook Secret token field.
  • Rotating the Cloudflare Tunnel token requires updating cloudflared-tunnel-token and recreating the cloudflared container.

For immediate containment:

  1. Set WRITE_ENABLED=false.
  2. Recreate the service container.
  3. Remove or disable the Cloudflare Tunnel route if browser ingress must stop.
  4. Revoke the GitLab OAuth application or project token if either is suspected compromised.
  5. Rotate only the affected credentials, then repeat the relevant verification steps.

Authoritative references

Git history

Loading the page's history…