How to Run Keycloak in a Sandbox Behind an HTTPS URL
Updated October 27, 2026 .md
Short answer: Run the official Keycloak image with start, --hostname set to the sandbox's https hosted URL, --proxy-headers xforwarded and --http-enabled true, then host port 8080 with sbx host. Without the hostname setting, Keycloak builds http:// links, so the admin console fails on mixed content and tokens carry the wrong issuer.
What we ran#
Everything below ran on production on October 27, 2026, in one small sandbox (2 vCPU, 4 GB RAM, 12 GB disk, $0.018/hour) with Keycloak 26.4.7 from quay.io/keycloak/keycloak:26.4. Secrets are replaced with <token>, <password> and <secret>. Times are single runs unless the table says otherwise.
| Check | Result |
|---|---|
sbx new --type small |
3.2 s to ready |
Keycloak start (start, production profile) |
started in 13.017s the first time, about 7 s after that |
| Memory | 565 MiB for the container |
| Admin console with no hostname settings | Loads, then shows somethingWentWrong (mixed content) |
Admin console with --hostname |
Signed in, 0 browser console errors |
client_credentials token request from a laptop |
200, median 0.194 s over 20 runs |
Token iss claim |
https://<subdomain>-8080.box.sandboxer.com/realms/demo, signature verified against the JWKS |
sbx stop --wait, then sbx resume |
2.2 s and 1.3 s, memoryRestored: true, next token 200 |
1. Create the sandbox and host port 8080 first#
You need the hosted URL before Keycloak starts, because it goes into --hostname. A hosted port with nothing listening yet is fine:
sbx new --name keycloak --type small --no-env
id=sb_ycojzw9amt # the id sbx new printed; its subdomain was prime-smart-elm
sbx host $id 8080 --title Keycloak > host.json{"ok":true,"type":"port.hosted","sandboxId":"sb_ycojzw9amt","port":8080,"url":"https://prime-smart-elm-8080.box.sandboxer.com?_token=<token>","access":"private","isProtected":true,"localhost":false}The URL is https://<subdomain>-8080.box.sandboxer.com. The subdomain stays the same across stop and resume, so the Keycloak setting below stays valid. The URL is private: without the token or the __sbx_auth cookie it sets, the proxy answers 401 and Keycloak never sees the request. See Hosting.
2. What goes wrong with the defaults#
We first ran Keycloak the way its container guide starts a test server, with start-dev and no hostname settings. Through the hosted URL, with the cookie:
B=https://prime-smart-elm-8080.box.sandboxer.com
C="Cookie: __sbx_auth=<token>"
j() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; } # tiny JSON reader
curl -s -H "$C" $B/realms/master/.well-known/openid-configuration | j "['issuer']"
curl -s -D - -o /dev/null -H "$C" $B/admin/ | grep -i ^location
curl -s -H "$C" $B/admin/master/console/ | grep authServerUrlhttp://prime-smart-elm-8080.box.sandboxer.com/realms/master
location: http://prime-smart-elm-8080.box.sandboxer.com/admin/master/console/
"authServerUrl": "http://prime-smart-elm-8080.box.sandboxer.com",The host name is right, because the proxy passes the Host header through. The scheme is wrong. TLS ends at the Sandboxer edge, and Keycloak sees a plain HTTP request on port 8080, so every URL it builds starts with http://.
A plain http:// request to the box domain gets a 308 to https://, so a redirect alone recovers. The admin console does not. We loaded it in headless Chromium:
body: Danger alert: somethingWentWrong somethingWentWrongDescription tryAgain
console errors: 5
Mixed Content: The page at 'https://prime-smart-elm-8080.box.sandboxer.com/admin/master/console/' was loaded over HTTPS, but requested an insecure resource 'http://prime-smart-elm-8080.box.sandboxer.c…
Framing 'http://prime-smart-elm-8080.box.sandboxer.com/' violates the following Content Security Policy directive: "frame-src 'self'". The request has been blocked.Tokens have the same problem: their iss claim is the http:// URL, so any client that checks the issuer against the https:// URL rejects them.
3. Start Keycloak with the hostname and proxy settings#
Docker is preinstalled in every sandbox. This is the command we kept:
sbx exec $id --timeout 600 "docker run -d --name kc --restart unless-stopped \
-p 8080:8080 -v kc-data:/opt/keycloak/data \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD='<password>' \
quay.io/keycloak/keycloak:26.4 start \
--hostname https://prime-smart-elm-8080.box.sandboxer.com \
--hostname-backchannel-dynamic true \
--proxy-headers xforwarded \
--http-enabled true
until curl -fs -o /dev/null localhost:8080/realms/master; do sleep 0.5; done"
sbx exec $id "docker logs kc 2>&1 | grep -o 'started in [0-9.]*s'"started in 13.017sWhat each option does here:
| Option | Why |
|---|---|
start |
Production profile. start-dev relaxes checks you want on. |
--hostname https://…-8080.box.sandboxer.com |
A full URL, so Keycloak uses https and this exact host for every front-channel link and for the issuer. See Configuring the hostname. |
--proxy-headers xforwarded |
Trust the X-Forwarded-* headers. With only this flag (no --hostname), the issuer already came out as https://…, so the Sandboxer proxy sends X-Forwarded-Proto: https. See Configuring a reverse proxy. |
--http-enabled true |
Production mode refuses plain HTTP otherwise. The edge does TLS. |
--hostname-backchannel-dynamic true |
Lets code inside the sandbox talk to localhost:8080 (section 6). |
-v kc-data:/opt/keycloak/data |
Keeps the built-in database across container restarts. |
--restart unless-stopped |
Starts Keycloak again whenever Docker starts. |
--timeout 600 is for the image pull. sbx exec stops a command after 30 seconds by default.
The built-in dev-file database is fine for a test server. For anything you keep, Keycloak's docs say to use a real database: see Configuring the database and our Postgres guide.
With --hostname pinned, a forged header can't change the issuer. We sent X-Forwarded-Host: evil.example and X-Forwarded-Proto: http through the hosted URL, and the issuer stayed https://prime-smart-elm-8080.box.sandboxer.com/realms/master.
4. The admin console, end to end#
Open the full ?_token= link once, then go to /admin/. We scripted that in headless Chromium: open the token link, open /admin/, fill in admin and the password, click Sign in.
console url: https://prime-smart-elm-8080.box.sandboxer.com/admin/master/console/ ms 5392
title: Keycloak Administration Console
body: You are logged in as a temporary admin user. To harden security, create a permanent admin account and delete the temporary one. … admin Keycloak Current realm Manage realms …
console errors: 0 []The KC_BOOTSTRAP_ADMIN_* account is temporary, as the banner says. Create a permanent admin in the console and delete the bootstrap one before you share the URL.
5. Request a token through the hosted URL#
Every request to a private URL needs the __sbx_auth cookie as well as Keycloak's own credentials. Get an admin token from the master realm, then create a realm and a confidential client with a service account through the Admin REST API:
curl -s -H "$C" -d grant_type=password -d client_id=admin-cli -d username=admin \
--data-urlencode 'password=<password>' $B/realms/master/protocol/openid-connect/token | j "['access_token']" > admtok
A="Authorization: Bearer $(cat admtok)"
curl -s -o /dev/null -w '%{http_code}\n' -H "$C" -H "$A" -H 'Content-Type: application/json' \
-d '{"realm":"demo","enabled":true}' $B/admin/realms
curl -s -o /dev/null -w '%{http_code}\n' -H "$C" -H "$A" -H 'Content-Type: application/json' \
-d '{"clientId":"billing-worker","publicClient":false,"serviceAccountsEnabled":true,"standardFlowEnabled":false,"directAccessGrantsEnabled":false}' \
$B/admin/realms/demo/clients
cid=$(curl -s -H "$C" -H "$A" "$B/admin/realms/demo/clients?clientId=billing-worker" | j "[0]['id']")
curl -s -H "$C" -H "$A" $B/admin/realms/demo/clients/$cid/client-secret | j "['value']" > csecret201
201The admin token lives 60 seconds (expires_in: 60), so run these together. Now the OAuth 2.0 client credentials request a backend service would make:
curl -s -H "$C" -u "billing-worker:$(cat csecret)" -d grant_type=client_credentials \
$B/realms/demo/protocol/openid-connect/token | j "['access_token']" > cctokDecoded, the token's header and claims were:
header {'alg': 'RS256', 'typ': 'JWT'}
{'iss': 'https://prime-smart-elm-8080.box.sandboxer.com/realms/demo', 'azp': 'billing-worker', 'typ': 'Bearer', 'preferred_username': 'service-account-billing-worker'} exp-iat 300We fetched the realm's JWKS from $B/realms/demo/protocol/openid-connect/certs and checked the token with PyJWT, requiring that issuer:
import jwt, json
jwks = jwt.PyJWKSet.from_dict(json.load(open("jwks.json")))
tok = open("cctok").read()
key = [k for k in jwks.keys if k.key_id == jwt.get_unverified_header(tok)["kid"]][0]
c = jwt.decode(tok, key.key, algorithms=["RS256"],
issuer="https://prime-smart-elm-8080.box.sandboxer.com/realms/demo",
options={"verify_aud": False})
print("verified", c["iss"], c["azp"])verified https://prime-smart-elm-8080.box.sandboxer.com/realms/demo billing-workerTwenty token requests from our laptop took 0.192 s minimum, 0.194 s median and 0.203 s maximum. Token introspection at …/token/introspect returned active: true with the same issuer.
How each layer says no:
| Request | Answer |
|---|---|
Right client secret, no __sbx_auth cookie |
401, Sandboxer HTML page |
| Cookie, wrong client secret | 401, {"error":"unauthorized_client","error_description":"Invalid client or Invalid client credentials"} |
A JSON error means the request reached Keycloak. An HTML page means the proxy stopped it.
6. Apps inside the same sandbox#
An app running next to Keycloak should call localhost:8080, not the private URL, which it can't open without the cookie. Without --hostname-backchannel-dynamic, Keycloak's discovery document sent it out through the hosted URL anyway. From inside the sandbox:
sbx exec $id "curl -s localhost:8080/realms/demo/.well-known/openid-configuration | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"token_endpoint\"])'"https://prime-smart-elm-8080.box.sandboxer.com/realms/demo/protocol/openid-connect/tokenWith --hostname-backchannel-dynamic true, the same request returned:
issuer https://prime-smart-elm-8080.box.sandboxer.com/realms/demo
token_endpoint http://localhost:8080/realms/demo/protocol/openid-connect/token
jwks_uri http://localhost:8080/realms/demo/protocol/openid-connect/certs
authorization_endpoint https://prime-smart-elm-8080.box.sandboxer.com/realms/demo/protocol/openid-connect/authBack-channel endpoints follow the request, the browser-facing ones stay on the hosted URL, and the issuer doesn't change. A token requested inside the sandbox over localhost had iss: https://prime-smart-elm-8080.box.sandboxer.com/realms/demo, the same as one requested from the laptop, so both sides validate the same issuer. From the laptop, the issuer and token endpoint stayed on https://…box.sandboxer.com.
7. Stop, resume and restart#
sbx stop $id --wait
sbx resume $id
sbx exec $id "docker ps --format '{{.Names}} {{.Status}}'"kc Up 34 secondsStop took 2.2 seconds and resume 1.3 seconds. The resume reported memoryRestored: true, so the same Keycloak process kept running, the old cookie still worked and the next token request returned 200 in 0.20 s. The admin console signed in again with no console errors.
A resume boots from disk instead when no machine with the same CPU type is free. Then processes start fresh, and the restart policy and the kc-data volume bring Keycloak back with its realms. We didn't force that, but restarting Docker shows the same path:
sbx exec $id --timeout 180 "sudo systemctl restart docker
until curl -fs -o /dev/null localhost:8080/realms/demo; do sleep 0.5; done
docker ps --format '{{.Names}} {{.Status}}'"kc Up 8 secondsKeycloak answered again 8.6 seconds after the restart, the demo realm was still there, and the client secret from before still got a token.
8. Clean up#
sbx delete $id --yesAfter the delete, the hosted URL returned 404. A small sandbox costs $0.018/hour while it runs and nothing while it is stopped.
FAQ#
Why does the Keycloak admin console show "somethingWentWrong" behind the hosted URL?#
Keycloak doesn't know it's behind HTTPS, so the console's config points at http:// URLs and the browser blocks them as mixed content. Start Keycloak with --hostname https://<subdomain>-8080.box.sandboxer.com (or at least --proxy-headers xforwarded).
Is --proxy-headers xforwarded enough on its own?#
In our test it fixed the scheme: the issuer came out as https://…. Pin --hostname as well, so the issuer is fixed no matter which headers reach Keycloak. With it set, a forged X-Forwarded-Host didn't change the issuer.
Can an app on my laptop use Keycloak while the URL is private?#
Yes, if it sends the __sbx_auth cookie with every request, as the curl commands above do. Standard OIDC libraries don't add that cookie on their own. For outside clients that you can't configure, host the port with --public and rely on Keycloak's own authentication. Switching public and private mints a new token, so old cookies stop working.
Do I need the default size?#
Not for a test server. Keycloak ran on small and used 565 MiB. Moving to a smaller size isn't supported yet, so start with small and resize up on resume if you need to.
What happens if I rename the subdomain?#
The hosted URL changes, and the --hostname value no longer matches. Recreate the container with the new URL. Tokens issued before carry the old issuer.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.