Troubleshooting
This guide covers common issues you may encounter with the SCF Controls Platform and their solutions.
Quick Diagnostics
Section titled “Quick Diagnostics”Before diving into specific issues, check these common areas:
- Browser console - Open DevTools (F12) and check the Console tab for errors
- Network tab - Check for failed API requests
- Clear cache - Hard refresh with Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac)
Authentication Issues
Section titled “Authentication Issues”By default a self-hosted install authenticates with the master API_KEY and shows no sign-in
screen — see Authentication. The Google-specific issues below
apply only when the optional Google Sign-In is enabled (GOOGLE_AUTH_ENABLED=true).
Google Sign-In Not Working
Section titled “Google Sign-In Not Working”Symptom: “Sign in with Google” button does nothing or shows an error.
Check browser console for errors.
Common causes:
| Error | Solution |
|---|---|
popup_closed_by_user | User closed popup — retry |
invalid_client | Check the OAuth client configuration in your Google Cloud console (GOOGLE_CLIENT_ID / VITE_GOOGLE_CLIENT_ID) |
| Origin not allowed | Add your deployment’s URL to the OAuth client’s authorised JavaScript origins |
idpiframe_initialization_failed | Clear browser cookies, disable ad blocker |
Sign-In Popup Closes Immediately
Section titled “Sign-In Popup Closes Immediately”Cause: The popup was closed before completing authentication.
Solution: Try again and wait for the Google sign-in screen to fully load before selecting your account.
“Popup Blocked” Message
Section titled ““Popup Blocked” Message”Cause: Your browser is blocking the sign-in popup.
Solutions:
- Look for a popup blocker icon in your browser’s address bar
- Click it and allow popups for this site
- Try signing in again
Users Can Sign In But See Errors
Section titled “Users Can Sign In But See Errors”Symptom: User authenticates but sees empty pages or errors.
Check:
- Browser console for JavaScript errors
- Network tab for failed API requests
- Ensure you’re using a supported browser (Chrome, Firefox, Safari, Edge)
Frontend Issues
Section titled “Frontend Issues”Blank Page After Loading
Section titled “Blank Page After Loading”Symptom: Frontend loads but shows blank white page.
Check browser console (F12 → Console).
Common causes:
| Console Error | Solution |
|---|---|
Failed to fetch | Check your internet connection, try refreshing |
Unexpected token | Clear browser cache and refresh |
| Network errors | Check if you’re behind a VPN or firewall that may be blocking access |
Try these steps:
- Hard refresh: Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac)
- Clear browser cache and cookies for this site
- Try a different browser
- Try incognito/private browsing mode
Changes Not Appearing
Section titled “Changes Not Appearing”Symptom: Made changes but don’t see them in the app.
Solutions:
- Click refresh — the refresh button in the header, or press
R. Waiting will not help: the platform polls for a change notice every 20 seconds but never reloads your data on its own - Look for “Updates available” in the header — that is the poll telling you there is something to pull in
- Hard refresh — Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac)
API Errors in Console
Section titled “API Errors in Console”Symptom: Network requests failing with 4xx/5xx errors.
Debug:
- Open Network tab in browser DevTools
- Find the failing request
- Check the response body for error details
- If errors persist, contact support with the error details
Data Issues
Section titled “Data Issues”Data Not Loading
Section titled “Data Not Loading”Symptom: Pages show “Loading…” indefinitely or display no data.
Solutions:
- Check your internet connection
- Refresh the page — Click the refresh button or hard refresh
- Check if the service is operational — Visit the platform status page if available
- Clear browser cache — May resolve stale data issues
Data Sync Issues
Section titled “Data Sync Issues”Symptom: Changes made by one user not appearing for another.
How synchronisation works:
- Every 20 seconds the platform polls for a change notice; it does not refetch data
- When the poll sees a change the header shows “Updates available”
- Switching back to the tab polls immediately and refetches the queries on screen
- The refresh button (or
R) is what actually reloads everything
If sync seems stuck:
- Click the refresh button in the header
- Hard refresh the browser page
- Sign out and sign back in
Missing Controls or Evidence
Section titled “Missing Controls or Evidence”Symptom: Expected controls or evidence items not appearing.
Check:
- Filters — Ensure no filters are hiding the items
- Search — Clear any search terms
- Organisation — Verify you’re in the correct organisation
- Permissions — Confirm you have access to view the data
Performance Issues
Section titled “Performance Issues”Slow Page Loads
Section titled “Slow Page Loads”Symptom: Pages take > 3 seconds to load.
Solutions:
- Check your internet connection — Run a speed test
- Clear browser cache — Old cached data can slow things down
- Reduce open tabs — Close unnecessary browser tabs
- Try a different browser — Some browsers perform better than others
- Disable browser extensions — Ad blockers or other extensions may interfere
Browser Becomes Unresponsive
Section titled “Browser Becomes Unresponsive”Symptom: Browser freezes or becomes slow when using the platform.
Solutions:
- Close other tabs — Free up browser memory
- Hard refresh — Ctrl+Shift+R to reload the page
- Restart browser — Close and reopen your browser
- Check system resources — Close other applications if memory is low
Account Issues
Section titled “Account Issues”Cannot Access Organisation
Section titled “Cannot Access Organisation”Symptom: Signed in but can’t see your organisation.
Solutions:
- Check organisation selector — If you belong to multiple organisations, ensure the correct one is selected
- Contact your admin — You may not have been added to the organisation yet
- Verify email — Ensure you’re signed in with the account the invitation was sent to
Role-Related Questions
Section titled “Role-Related Questions”Roles are enforced. Organisation-scoped endpoints declare the minimum role they require and refuse anything below it with a 403, so “Access denied: This action requires ‘admin’ role or higher” means exactly what it says — the account’s role is too low for that action, not that something is broken.
If somebody cannot do what they expect: check their role in User Management first. Admin outranks Editor, which outranks Viewer, and an admin can change the role from the member’s row. Consultants are checked against the role their client relationship carries rather than an organisation membership, so that is the thing to check for them.
Browser Compatibility
Section titled “Browser Compatibility”Supported Browsers
Section titled “Supported Browsers”The platform works best with:
| Browser | Version |
|---|---|
| Chrome | Latest 2 versions |
| Firefox | Latest 2 versions |
| Safari | Latest 2 versions |
| Edge | Latest 2 versions |
Known Limitations
Section titled “Known Limitations”- Internet Explorer — Not supported
- Very old browsers — May experience visual or functional issues
- Mobile browsers — Basic support; desktop recommended for full functionality
Self-Hosted Deployment Issues
Section titled “Self-Hosted Deployment Issues”These apply to the self-hosted Docker Compose stack only — see Deployment.
Dashboard Is Empty on a Fresh Deploy
Section titled “Dashboard Is Empty on a Fresh Deploy”Symptom: The stack starts and you can sign in, but the control library, frameworks, and dashboards are all empty. It looks broken.
Cause: The SCF catalogue has not been imported yet. The open-source build ships the importer, not SCF’s licensed content, so a fresh install starts with an empty catalogue.
Fix: Open the app — on a fresh install it shows a catalogue onboarding screen; upload your
SCF .xlsx there and it is seeded live. (Upload requires single-tenant mode and a platform admin.)
For a scripted or air-gapped install you can instead seed it before first boot with the headless importer:
docker compose --profile init run --rm catalog-importerContainers Exit at Start Without an .env
Section titled “Containers Exit at Start Without an .env”Symptom: Postgres refuses to initialise without a superuser password, minio-init exits with a
FATAL credential message, or the backend refuses to start without an API key.
Cause: The stack was started without running the installer, so there is no .env and no secret
files. Every compose variable has a default, so Compose itself prints no warning.
Fix: Run the installer before starting. It generates every credential, writes the secret files and
the .env:
scripts/install.shIf the checkout already has a hand-written .env, run scripts/install.sh --import-env instead to
move its credentials into secret files.
On an install that already has a hand-edited .env, use scripts/install.sh --import-env instead —
a bare run refuses when a .env exists, so it cannot mint a new DB_PASSWORD against an
already-initialised Postgres volume. See Credentials and secrets.
celery-beat Restarts in a Loop, or Catalogue Import Fails with Permission denied
Section titled “celery-beat Restarts in a Loop, or Catalogue Import Fails with Permission denied”Symptom: docker ps shows cg-scf-celery-beat in Restarting (1) with
Permission denied: 'celerybeat-schedule' in its logs — or the catalogue import fails part-way
through and leaves the database unseeded, so a fresh install cannot get past first login:
Task catalog.import[...] failed: [Errno 13] Permission denied: '/app/data/json/control_guidance.json' File "/app/tasks_catalog.py", line 80, in import_catalogCause: webclient/public/data is bind-mounted read-write into the worker, which writes the
generated catalogue JSON there. A fresh clone is owned by the cloning user — never uid 1001,
since no such account exists on a normal host — so the container falls into the “other” class,
which 0755 and 0775 both leave without write. This affects every operator whose uid is not
exactly 1001, which is the ordinary case, not an edge one.
The one-shot CLI importer is affected too: it runs as container root, but cap_drop: ALL removes
CAP_DAC_OVERRIDE, so root gets no exemption from the mode check either.
Fix: Update to a current release. celery-beat now writes its schedule to a container-local
path, which removes the crash loop; scripts/install.sh group-owns webclient/public/data to
SCF_APP_GID (1001 by default) with the setgid bit so imported files keep the group; and
catalog-importer carries the matching group_add. scripts/upgrade.sh then re-checks that
directory on every upgrade and re-groups it if it has drifted, naming what it found before it fixes
it — so on an installer-provisioned host this should not recur silently. On an install provisioned by
hand, do the directory once yourself:
docker run --rm -v "$PWD/webclient/public/data:/d" alpine:3 \ sh -c 'chgrp -R 1001 /d && chmod 2775 /d'Check it from inside the container rather than from the host, since the host’s own uid tells you nothing:
docker compose exec celery-worker sh -c 'id; touch /app/data/json/.probe && echo WRITABLE && rm /app/data/json/.probe || echo "NOT WRITABLE"'A Background Job Left a succeeded Line but Nothing Happened
Section titled “A Background Job Left a succeeded Line but Nothing Happened”Symptom: a vendor assessment, recipe generation or evidence assessment shows as failed in the
app, or simply never produces its output, while docker compose logs celery-worker shows only:
Task tasks_vendor_assessment.run_vendor_assessment[...] succeededCause: most background jobs catch their own errors so the failure can be written to the record
the UI reads, then return a payload saying what went wrong instead of raising. Celery treats
“nothing was raised” as success, so releases before this fix logged a success line for a payload
that said "status": "failed" — for every task in the platform, not just these. The worker log
could not be used to tell whether the work was actually done.
Fix: update to a current release. A job that returns a failure now logs it as an error, with the task’s own reason on the same line:
Task tasks_vendor_assessment.run_vendor_assessment[...] returned a failure payload: status=failed Could not resolve authentication methodThe success line is unchanged, so anything already watching for it keeps working.
A job that finishes but does not fully succeed is now called out too. Each task declares which of
its own status values mean that; today vendor research declares partial and the evidence-store
copy declares completed_with_errors. Those log a WARNING carrying any message and counts the
task put in its result:
Task tasks_evidence_storage_copy.copy_evidence_store[...] returned a partial-success payload: status=completed_with_errors copied=3 failed=1 skipped=0These runs are not failures, and the job record in the app remains the source of truth for what
happened to each item. The line exists so that reading the worker log never leaves you believing a
partly-finished run went through cleanly. Evidence assessments are not affected: their partial is
a verdict on the evidence, not on the run, and they still log succeeded.
A Default Port Is Already in Use
Section titled “A Default Port Is Already in Use”Symptom: docker compose up fails with bind: address already in use (8000 and 9000 are
common defaults for other tools).
Fix: Remap the host side of any published port in .env — the container side never
changes:
BACKEND_PORT=8080MINIO_PORT=9010 # EVIDENCE_PUBLIC_ENDPOINT's default follows this automaticallyThe full variable list lives in Deployment → Service ports.
For deeper compose changes, use a docker-compose.override.yml overlay so upgrades keep a
clean tree — but list it in COMPOSE_FILE if that variable is set, or compose will not load it
(why).
500 on First Write, or Risk Register Spins Forever
Section titled “500 on First Write, or Risk Register Spins Forever”Symptom: Creating a system/risk returns a 500, or the Risk Register loads indefinitely on a fresh org.
Cause: Older builds before the single-tenant audit/auth fixes. The static API key had no backing user row (audit writes failed), and the risk register retried the failing create in a loop.
Fix: Update to a current release and ensure OSS_SINGLE_TENANT=1 is set. Recent versions seed a
service-account user and pin the master key’s admin grant to your one organisation, so audited
writes succeed.
Cross-Tenant Admin / Security Warning at Startup
Section titled “Cross-Tenant Admin / Security Warning at Startup”Symptom: Logs show OSS_SINGLE_TENANT was disabled, or a logger.critical about
ENVIRONMENT=development.
Cause (by design): OSS_SINGLE_TENANT is fail-closed — it refuses to grant the master key
admin if more than one organisation or human member exists, to avoid a cross-tenant admin hole.
ENVIRONMENT=development weakens error handling and must not run on a network-exposed host.
Fix: Keep a self-hosted deployment to a single organisation, leave ENVIRONMENT=production
(the default), and only enable development on a trusted local machine.
“No evidence store is configured for this organisation”
Section titled ““No evidence store is configured for this organisation””Symptom: Uploading or downloading an evidence file is refused, and the message names
Settings, Evidence storage. The API answers 409 with
{"error": "evidence_storage_not_configured", "message": ...}. Everything else in the platform works
normally, and other organisations may be unaffected.
Cause: this is not a fault. This organisation has no active evidence storage configuration, the
installation has no platform-wide one either, and the process environment names no usable store. It is
the state a --no-minio install starts in. Retrying will not help and neither will a restart.
Fix: an organisation administrator configures a store under Settings, Evidence storage — create a configuration, run the connection test, then activate it. Activation dials the store and refuses if it cannot write, read back and delete, so a configuration that goes live has been proved reachable. To give every organisation a default instead, set a platform-wide store on the same screen as an operator.
To see which of the three the installation is in, read the platform-wide answer from /health:
curl -s http://localhost:8000/health | jq '.components.evidence_storage'"status": "unconfigured" means there is no platform store to fall back to. See
Monitoring for the full set of states.
Evidence Upload Fails
Section titled “Evidence Upload Fails”Symptom: Uploading an evidence file errors, or the download link does not resolve. The error is not the “no evidence store is configured” refusal above.
Cause: Object storage misconfiguration. By default the stack uses the bundled MinIO; the browser-facing presign endpoint must be reachable from your machine.
Fix: For a store configured in the application, open Settings, Evidence storage and run the
connection test — it reports which of address, write, read-back and delete failed. For an
installation still running on its environment settings, confirm the AWS_* / EVIDENCE_* block in
.env. For bundled MinIO, EVIDENCE_PUBLIC_ENDPOINT must be a host the browser can reach
(e.g. http://localhost:9000).
Deploying on a remote Docker host? localhost won’t resolve from your browser — set it to
the host’s reachable address instead, e.g. http://<docker-host>:9000.
See Deployment → Evidence storage.
Activation is refused with an unstamped_files count
Section titled “Activation is refused with an unstamped_files count”Symptom: Activating an evidence storage configuration answers 409 with an unstamped_files
count instead of going live. The connection test on the same configuration passes.
Cause: the store currently in force is the one named in the installation’s environment
(legacy_env), and this organisation has evidence files that are not associated with any
configuration row — they were written before the configuration existed, so there is no row to
associate them with. Activating anyway would leave those files resolving to the new store, which does
not hold them.
Fix: seed the platform configuration, which gives those files a row to belong to, then retry the activation. The installer does this on a bundled install; an installation upgraded from before the feature may not have one. Afterwards, use the copy tooling on the same screen to move the existing objects to the new store before retiring the old one.
Azure Blob evidence storage stopped working after an upgrade
Section titled “Azure Blob evidence storage stopped working after an upgrade”Symptom: an installation that stored evidence in Azure Blob is now writing somewhere else, or
refusing uploads, and the backend log carries a warning naming AZURE_STORAGE_ACCOUNT_NAME.
Cause: Azure Blob is retired as a storage backend. AZURE_STORAGE_ACCOUNT_NAME,
AZURE_STORAGE_ACCOUNT_KEY and EVIDENCE_CONTAINER are read only to emit that warning. The setting
used to override every S3 setting in the file, so removing its precedence changes which store is in
force on any installation that had both.
Fix: configure an S3-compatible store under Settings, Evidence storage and copy the existing
objects into its bucket. Evidence records name the object key rather than the endpoint, so records
stay valid once the objects are in place. Then clear the three Azure variables from .env to silence
the warning.
A docker-compose.override.yml Has No Effect
Section titled “A docker-compose.override.yml Has No Effect”Symptom: you add docker-compose.override.yml, run docker compose up -d, and nothing
changes — the same error, the same image, the same limit. No warning, no mention of the file.
Cause: compose auto-discovers docker-compose.override.yml only when COMPOSE_FILE is
unset. scripts/install.sh writes
COMPOSE_FILE=docker-compose.yml:docker-compose.secrets.ymlinto .env, so on every installer-provisioned install auto-discovery is already off and the
override is silently ignored. Confirm with a one-liner — the override is loaded if and only if it
is listed:
docker compose config --no-interpolate | head -1 # compose prints nothing about files it skippedgrep '^COMPOSE_FILE=' .envFix: append it to the list, last so it wins:
COMPOSE_FILE=docker-compose.yml:docker-compose.secrets.yml:docker-compose.override.ymlpull access denied for minio/minio or minio/mc
Section titled “pull access denied for minio/minio or minio/mc”Symptom: on a fresh deploy, docker compose up never starts the storage tier:
✘ minio Error pull access denied for minio/minio, repository does not exist or may require 'docker login'✘ minio-init Error pull access denied for minio/mc, repository does not exist or may require 'docker login'Cause — and it is not the one the message suggests. MinIO unpublished both repositories from
Docker Hub. Docker Hub deliberately reports an absent or private repository as 401 UNAUTHORIZED
rather than 404, and the daemon renders that as “may require docker login”. A Docker Hub
credential cannot fix this — there is no repository left to authenticate against. Minting a Hub
access token is wasted effort.
Fix: upgrade to a release whose docker-compose.yml pulls these two images from quay.io, which
still serves them. The digests are unchanged, so this is a registry re-point and not a different
image. If you cannot upgrade yet, override the two image references locally — do not drop the
@sha256: pins:
services: minio: image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e minio-init: image: quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727Check COMPOSE_FILE in your .env first: if it is set, compose stops auto-loading docker-compose.override.yml and this file does nothing — see below.
Then docker compose --profile storage pull minio minio-init.
The bundled MinIO is end-of-life and receives no security patches
Section titled “The bundled MinIO is end-of-life and receives no security patches”This is not a failure mode. Nothing breaks, nothing logs an error, and the container starts and
serves evidence exactly as it always has. It is here because an operator who reads the entry above
will reasonably conclude that re-pointing to quay.io fixed the problem. It fixed the pull. It did
not fix what the image contains.
What happened upstream. MinIO stopped publishing community builds and then archived the project:
| Date | Event |
|---|---|
| 2025-09-07 | RELEASE.2025-09-07T16-13-09Z — the last community container image. This is the image this platform pins, and it is what latest on quay.io/minio/minio still resolves to. |
| 2025-10-15 | RELEASE.2025-10-15T17-29-55Z — a source-only release. It shipped no binaries, no packages and no container image; there is no image for it on quay.io at all. |
| 2026-04-25 | github.com/minio/minio archived and made read-only. The README reads “THIS REPOSITORY IS NO LONGER MAINTAINED” and points readers at the commercial AIStor product. |
What that means for the pinned image. Eight security advisories have been published against
minio/minio since that last community image, and the pinned release falls inside the vulnerable
range of every one of them:
| CVE | Severity | Published | Fix shipped in |
|---|---|---|---|
| CVE-2025-62506 | High | 2025-10-16 | RELEASE.2025-10-15T17-29-55Z — source only, no image |
| CVE-2026-33322 | Medium | 2026-03-19 | A release tag that does not exist in the public repository |
| CVE-2026-33419 | Medium | 2026-03-20 | A release tag that does not exist in the public repository |
| CVE-2026-34204 | High | 2026-03-27 | A release tag that does not exist in the public repository |
| CVE-2026-39414 | High | 2026-04-07 | MinIO AIStor (commercial) |
| CVE-2026-40344 | High | 2026-04-11 | A release tag that does not exist in the public repository |
| CVE-2026-41145 | High | 2026-04-14 | A release tag that does not exist in the public repository |
| CVE-2026-42600 | Medium | 2026-04-25 | A release tag that does not exist in the public repository |
Every “fixed in” tag from 2026 returns 404 against the public repository — the fixes exist only in
the commercial product. There is no community release to upgrade to. That is the whole finding:
this is not a patch you have not applied yet, it is a patch that does not exist.
Two of the 2026 HIGHs are the ones to read first, because they are unauthenticated object write
against a default deployment — CVE-2026-40344 (missing signature verification in Snowball
auto-extract) and CVE-2026-41145 (query-string credential signature bypass on unsigned-trailer
uploads). Both name a vulnerable range beginning RELEASE.2023-05-18T00-05-36Z.
Why the quay.io re-point does not help. It is the same image. The digests were unchanged by the
re-point precisely so that it would be a registry change and nothing else; quay.io simply still
serves what Docker Hub stopped serving.
Why the .hotfix.* tags on quay.io are not the answer either. quay.io/minio/minio carries
tags such as RELEASE.2025-09-07T16-13-09Z.hotfix.7aa24e772, pushed well after the community
end-of-life. These are per-customer commercial builds: MinIO’s own documentation describes a hotfix
as a build “based on a specific release tag with one or more targeted patches cherry-picked”,
requested by subscribers through their support portal. The embedded commit id does not exist in the
public repository, there is no changelog, and the source is not published. Pinning one means running
a binary you cannot reproduce, whose contents are undocumented, built for someone else’s support
contract. It predates most of the CVEs above in any case.
What to do.
- Treat the bundled store as evaluation-only. It is there so a first run works without a cloud account. It is not a place to keep real evidence.
- Point the platform at your own object storage. Configure an S3-compatible bucket, GCS or your own MinIO deployment in Settings → Evidence storage, then migrate what is already there — see Evidence Storage Settings. The platform verifies every object by size and SHA-256 as it copies.
- If you must stay on the bundled store for now, keep the S3 API off untrusted networks. Bind it
to an interface only your users reach, put it behind your own reverse proxy with TLS, and set
EVIDENCE_PUBLIC_ENDPOINTto match. Do not expose${MINIO_PORT:-9000}to the internet. - Remember the backups follow the store.
scripts/backup.shandscripts/upgrade.shtar the bundledminio_datavolume. Evidence in your own bucket is outside that — see Backup & Restore.
MinIO refuses its own credentials, or minio-init says they are missing
Section titled “MinIO refuses its own credentials, or minio-init says they are missing”Symptom: on a fresh install with the secrets overlay, the storage profile never comes up:
cg-scf-minio | FATAL Unable to validate credentials inherited from the secret file(s): Invalid credentialscg-scf-minio | HINT: Access key length should be at least 3, and secret key length at least 8 characterscg-scf-minio-init | FATAL: minio-init refuses to start: /run/secrets/MINIO_ROOT_USER or MINIO_ROOT_PASSWORD is missing or empty.The files are there and correctly sized. minio crashloops; minio-init exits 1.
Cause: not the credentials — the containers cannot read the files, so the values arrive
empty. The installer leaves them mode 0640, group SCF_APP_GID. minio and minio-init run as
container root with cap_drop: ALL, which takes CAP_DAC_OVERRIDE away, so root is subject to the
ordinary mode check, matches neither owner nor group, and lands in “other”. See
Credentials and secrets
for the full table of who reads them how.
Fix: update to a current release — minio and minio-init now carry
group_add: ["${SCF_APP_GID:-1001}"] like the Keycloak services already did. To confirm it is
this and not a genuinely wrong credential, read the file as the container does:
docker compose exec minio sh -c 'ls -ln /run/secrets/MINIO_ROOT_USER; cat /run/secrets/MINIO_ROOT_USER >/dev/null && echo READABLE || echo "DENIED"'DENIED on a file that sudo cat shows as correct is this problem. If you cannot upgrade yet, an
untracked docker-compose.override.yml does the same thing:
services: minio: group_add: ["1001"] minio-init: group_add: ["1001"]Check COMPOSE_FILE in your .env first: if it is set, compose stops auto-loading docker-compose.override.yml and this file does nothing — see below.
minio-init exits with Killed after the policy attach succeeds
Section titled “minio-init exits with Killed after the policy attach succeeds”Symptom: the bucket and policy are created, then:
minio-init: policy ready: scf-evidence-rw (bucket evidence only)/scf/minio-init.sh: line 136: 58 Killed mc admin policy attach ...FATAL: minio-init: could not attach scf-evidence-rw to the application user: Attached Policies: [scf-evidence-rw]Note that the FATAL quotes mc’s own success output — the attach worked.
Cause: the container hit its memory limit and was SIGKILLed before mc could exit cleanly,
so the script saw a non-zero status. mc is a Go binary whose runtime sizes its heap against the
cgroup limit; this workload peaks at roughly 150 MiB, and the limit used to be 128m. Confirm:
docker inspect cg-scf-minio-init --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}'OOMKilled=true is the only honest signal — nothing in the log says “memory”.
Fix: update to a current release, where minio-init is mem_limit: 256m. On an older one,
raise it in docker-compose.override.yml and re-run docker compose up -d minio-init. The step is
idempotent, so re-running after a partial failure is safe.
Check COMPOSE_FILE in your .env first: if it is set, compose stops auto-loading docker-compose.override.yml and this file does nothing — see below.
The MinIO container is not running at all
Section titled “The MinIO container is not running at all”Symptom: docker compose ps shows no minio service, docker compose logs minio says “no such
service”, and every evidence operation is refused. The rest of the stack is healthy.
Cause: minio and minio-init sit behind the storage compose profile. A profile that is not
listed in COMPOSE_PROFILES makes its services absent rather than broken, so there is nothing in
any log to find. Either the stack was installed with --no-minio, or an upgrade from before the
profile existed did not add it.
Fix: check the value, and add storage if the install does have a bundled MinIO:
grep -E '^(COMPOSE_PROFILES|EVIDENCE_STORAGE_BOOTSTRAP)=' .envdocker compose config --services | grep minio # silent when the profile is offscripts/upgrade.sh adds the profile and EVIDENCE_STORAGE_BOOTSTRAP=bundled_minio for you when it
finds a real MINIO_ROOT_USER. Adding them by hand means one line each, then
docker compose up -d. A --no-minio install is meant to look like this: configure your own
S3-compatible bucket rather than adding the profile.
minio-init says the application credential is the root account
Section titled “minio-init says the application credential is the root account”Symptom: docker compose logs minio-init prints “the application credential IS the MinIO root
account” and exits 0.
Cause: the install was provisioned before the scoped MinIO account existed, so
AWS_ACCESS_KEY_ID is byte-identical to MINIO_ROOT_USER. This is not an error, and the stack works
exactly as it did.
Fix: optional, and an operator step by design — an upgrade will not change a credential underneath a running install. Follow the migration note in Credentials and secrets.
Installer and credentials
Section titled “Installer and credentials”These apply to scripts/install.sh and the file-backed credentials it writes — see
Credentials and secrets.
The wizard shut itself down after five wrong setup tokens
Section titled “The wizard shut itself down after five wrong setup tokens”Symptom: scripts/install.sh exits with:
the wizard shut itself down after five wrong setup tokens. Nothing was written. Re-run this script for a fresh token and paste it exactly.Cause: The wizard accepts at most five bad provisioning tokens and then exits (exit code 5)
rather than stay up to be guessed at. Nothing has been written to the secrets directory or to .env.
Fix: Re-run scripts/install.sh. It mints a fresh token, prints it once to the console, and
starts a new wizard. Paste the token exactly, with no surrounding whitespace, into the first field.
“this checkout already has a .env”
Section titled ““this checkout already has a .env””Symptom: scripts/install.sh refuses immediately with:
this checkout already has a .env. Run './scripts/install.sh --import-env' to move its credentials into <dir>, or move the file aside first.Cause: A bare run would generate a fresh DB_PASSWORD against an already-initialised Postgres
volume, and a fresh SCF_SECRET_KEY that cannot decrypt anything already stored. The installer
refuses rather than risk either.
Fix: On an existing install, run scripts/install.sh --import-env to move the current values
verbatim into 0600 files. For a genuinely fresh install, move the stale .env aside and run the
installer again.
The wizard port is already in use
Section titled “The wizard port is already in use”Symptom: Docker fails to publish 127.0.0.1:8765 with address already in use, or nothing
answers at the printed address.
Cause: Something else on the host is listening on the default wizard port.
Fix: Pick another port. The wizard is still published on loopback only, and the Host check
follows the port you choose:
scripts/install.sh --port 9765 # then open http://127.0.0.1:9765/HTTP 421 “Misdirected Request” when opening the wizard
Section titled “HTTP 421 “Misdirected Request” when opening the wizard”Symptom: Instead of the wizard, the browser shows a JSON error with status 421 and
this installer answers only on its own loopback address.
Cause: The wizard answers only requests whose Host header is exactly 127.0.0.1:<port> or
localhost:<port> for the port it was started on. Opening it through the machine’s hostname, a
different port, a reverse proxy, or a DNS name that points at loopback is refused. That is the
DNS-rebinding defence and it is deliberate.
Fix: Open exactly the address the script printed — http://127.0.0.1:<port>/ — in a browser on
the host, or through the SSH tunnel described below. If you started the wizard with --port, use
that port.
403 “cross-site requests are refused” from the wizard’s API
Section titled “403 “cross-site requests are refused” from the wizard’s API”Symptom: The page loads, but every action fails with a 403 and cross-site requests are refused.
Cause: The wizard refuses any request that a cross-site context initiated, other than opening the page itself. That happens when the page is embedded in another site (an iframe, a browser extension’s page, or a tab whose origin the browser attributes to another site) and its scripts call the wizard’s API.
Fix: Open http://127.0.0.1:<port>/ directly in a normal browser tab. Do not embed it and do
not drive it from another origin. Do not put the token in the address either: a token query
parameter is refused with a 400, so the token can never land in a log. The wizard also answers a
plain wrong token with a 401 — that counts towards the five-token lockout above.
Opening the wizard on a remote host
Section titled “Opening the wizard on a remote host”Symptom: You ran the installer on a server over SSH, and http://127.0.0.1:8765/ opens nothing
on your own machine.
Cause: The wizard is published on the server’s loopback interface only. It is never reachable from the network, by design.
Fix: Tunnel the port and browse locally. The Host check passes because your browser still
addresses 127.0.0.1:<port>:
ssh -L 8765:127.0.0.1:8765 <user>@<host> # then open http://127.0.0.1:8765/Keep the SSH session open until the wizard reports Setup complete; the installer stops itself and deletes the token at that point.
External database validation fails
Section titled “External database validation fails”Symptom: The Database step shows one or more failed checks under Test this connection and will not continue. Nothing is written until every check passes.
Cause and fix, by check:
| Check | Cause | Fix |
|---|---|---|
dns | The host name does not resolve from inside the installer container | Use a resolvable name or an IP address. Loopback and link-local (169.254.0.0/16) addresses are refused outright — loopback is the installer container itself, not your database |
connect | Nothing answered on that host and port within 5 seconds | Check the port and firewall, and that the instance accepts connections from this host. Managed PostgreSQL (Cloud SQL, RDS, Azure Database) usually needs a public IP or a proxy sidecar reachable from the Docker network |
tls | The server refused TLS at the requested mode, or the connection came up unencrypted despite it | Choose a TLS mode the server supports: require, verify-ca or verify-full. disable is refused unless you also tick the acknowledgement that it sends the database password in cleartext on every boot (allow_plaintext: true in an unattended config) |
auth | The server rejected the credentials, or pg_hba.conf refused the client | Check the user and password, and that pg_hba.conf admits this user from this host with the TLS mode you chose |
version | The server is older than PostgreSQL 15 | The platform requires PostgreSQL 15 or newer. Upgrade the server or point at a newer instance |
create_table | The user cannot create tables in the target database — or cannot create the database when it does not exist yet | Grant the user CREATE on the database (or CREATEDB on the role if the database is still to be created), then test again |
Connection-string parameters that change where or how the driver connects (options, service,
passfile, sslkey, sslcert, sslrootcert, krbsrvname, gssencmode) are rejected as
unsupported connection parameter. Remove them and set the host, port, user and TLS mode
explicitly.
Backend refuses to start on a placeholder credential
Section titled “Backend refuses to start on a placeholder credential”Symptom: The backend container exits straight away with:
Refusing to start with ENVIRONMENT=production: API_KEY is unset or still a shipped placeholderThe same message can name DB_PASSWORD, AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY, or say that
SCF_SECRET_KEY is not a valid Fernet key.
Cause: Outside ENVIRONMENT=development and test, the backend checks its credentials at boot
and refuses to run on a value that is missing, empty, or one of the changeme-* / minioadmin
placeholders shipped in .env.example. Booting production on a placeholder would be a silent
compromise, so it is a hard stop.
Fix: Run scripts/install.sh on a fresh install, or scripts/install.sh --import-env on an
existing one, so every credential is a real generated value in a file. If you manage credentials
yourself, set the named variable — or its NAME_FILE counterpart — to a real value and recreate the
service. ENVIRONMENT=development silences the check, but only ever set that on a trusted local
machine, never on a network-exposed host.
Stack fails database authentication after adopting the secrets overlay
Section titled “Stack fails database authentication after adopting the secrets overlay”Symptom: After switching an existing install to docker-compose.secrets.yml, Postgres itself is
healthy but the backend, the Celery workers and Keycloak all fail to authenticate to it.
Cause: POSTGRES_PASSWORD_FILE is read by initdb only. On a populated postgres_data
volume the entrypoint skips initdb, so a newly generated DB_PASSWORD is never applied to the
cg role: the database keeps its old password while every other service presents the new one.
Fix: Seed the secrets directory with the password the database already has. The supported way
is scripts/install.sh --import-env, which copies the current DB_PASSWORD out of .env
verbatim. If new files have already been generated, either write the old value back into
$SCF_SECRETS_DIR/DB_PASSWORD, or change the role to match the new file and recreate the services:
docker compose exec postgres psql -U cg -d cg_scf -c "ALTER ROLE cg WITH PASSWORD '<value in the DB_PASSWORD file>';"docker compose up -d --force-recreate backend celery-worker celery-beat keycloakChange the role first. Writing the file first leaves the services unable to authenticate. See Credentials and secrets, The database password.
Getting Help
Section titled “Getting Help”If you can’t resolve an issue:
-
Check the FAQ: See the FAQ for common questions
-
Contact Support:
- Email: support@scfcontrolsplatform.com
- Include: Browser type, error messages, steps to reproduce
-
Documentation:
- Review relevant guides in this documentation
- Check for any service status announcements
Related Guides
Section titled “Related Guides”- Authentication — Sign-in help
- User Management — Managing users and roles
- FAQ — Frequently asked questions

