You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
test-server builds and publishes to ghcr.io/yschimke/okhttp-testbed/test-server on every push to main (#28). Nothing runs it. Until something does, the suites can only reach it through Testcontainers, and the two things a hosted instance is for — a real network path, and an endpoint the Android matrix can reach without Docker on the device — stay out of reach.
This is the deployment, plus the part that keeps a deployment from going stale: Watchtower watching the published tag and restarting the container when the digest moves.
The host
Anything that runs Docker and has a name. A 1 vCPU / 1 GB VM is generous — the server is a static Go binary with no state and no disk use beyond logs.
Requirements, in order of how much they constrain the choice:
A DNS name. Needed for a publicly trusted certificate, and for the suites to have something to configure other than an IP.
Inbound 443. See below.
Inbound on the remaining listener ports (or a decision to publish only some of them).
IPv6, ideally — Happy Eyeballs against real dual-stack and single-stack names #21 wants a dual-stack name, and GitHub-hosted runners having no IPv6 is already the limiting factor there. A v6-reachable instance doesn't fix the runner, but it means the v6 half is testable from anywhere else.
Ports
test-server/README.md covers this in full; the short version is that a non-standard port changes nothing about the results — every absolute URL the server emits is built from the Host the request arrived with, and the CI smoke test runs the image on non-default ports to keep that true.
The exception worth spending a real port on: 443 is the only port that tests the internet. Interception middleboxes, CDN behaviour and captive portals mostly act on 443, so an instance on 8443 is not being handled the way a user's traffic is. Put https there; the per-version and hostile ports can go wherever.
Listener
Container
Suggested published
Notes
https
8443
443
the one that matters
http
8080
80
plain HTTP, and the cleartext-policy cases
raw
8081
8081
request-head echo; not an HTTP server
tls10–tls13
8410–8413
8410–8413
badssl-style, non-standard by design
Certificates, and the one real conflict
The image has two modes and they are mutually exclusive today:
Generated CA (default) — mints its own CA at startup, serves it at /ca.pem. This is what lets a test assert a chain is accepted.
A hosted instance wants both, for different suites: a publicly trusted chain exercises the platform's own trust anchors (which is the whole point of a deployment over a container), while the fixture CA is what the positive-chain assertions need. Both currently apply to every TLS listener at once.
Three ways out, in preference order:
Per-listener certificates — https gets the real chain, the per-version ports keep the generated one. A small change to tlsServer, and the honest fix.
Two instances — one with TLS_CERT_FILE, one without, on different ports or names. No code change; two things to keep running.
Generated CA only, for now — the deployment adds a real network path but not real trust anchors. Fine as a first step, and worth being explicit that it is one.
Whichever, the certificate has to renew, so a deployment with a real chain needs an ACME client writing to a volume the container reads, and a restart or reload on renewal. Caddy is already in the compose stack and can do the ACME half.
Watchtower
Watchtower polls the registry, compares digests, and recreates a container when its tag moves. Since test-server:latest is republished on every push to main, that closes the loop: merge, and the deployment follows within the poll interval.
Use the maintained fork, nicholas-fedor/watchtower — published as nickfedor/watchtower on Docker Hub and ghcr.io/nicholas-fedor/watchtower, currently 1.20.3 and actively released. The original containrrr/watchtower has been quiet since 1.7.1. The fork keeps the same com.centurylinklabs.watchtower.* label namespace, so configuration and scoping carry over unchanged.
It must be scoped to test-server and nothing else.httpbin and caddy are pinned deliberately — a moved image is breakage the daily run should catch, not absorb, which is the same reason mockserver is pinned. Watchtower updating those would quietly defeat that. Scope it with WATCHTOWER_LABEL_ENABLE and label only the one service:
test-server:
image: ghcr.io/yschimke/okhttp-testbed/test-server:latestlabels:
com.centurylinklabs.watchtower.enable: "true"watchtower:
image: nickfedor/watchtower:1.20.3restart: unless-stoppedvolumes:
- /var/run/docker.sock:/var/run/docker.sockenvironment:
WATCHTOWER_LABEL_ENABLE: "true"# only labelled containersWATCHTOWER_CLEANUP: "true"# don't accumulate old layers on a small diskWATCHTOWER_POLL_INTERVAL: "300"
Things worth knowing before wiring it up:
It needs the Docker socket, which is root-equivalent on the host. That is the real cost of this convenience. Keep the host doing nothing else, and don't expose Watchtower's own HTTP API.
The image is public, so no registry credentials are involved. Don't add any.
Pin Watchtower's own tag. An auto-updater that auto-updates itself is the one container where a bad release takes the recovery mechanism with it.
Rollback is manual. If latest ships something broken, the fix is to pin the service to the previous commit tag (every build publishes :<sha> as well) and restart. Worth writing down next to the deployment rather than discovering under pressure.
The alternative to all of this is a webhook from the publish job. Watchtower is less to build and has no inbound attack surface; a webhook is faster and needs no Docker socket. Watchtower first.
Pointing the suites at it
Once it exists, the suites need to find it, and to cope with it being down:
A single configuration point — TESTBED_SERVER_URL or a Gradle property — defaulting to unset, so a developer with no deployment still runs the Testcontainers path.
An Endpoint and Probe entry (Endpoint reachability preflight, and endpoint availability on the status page #7) so a suite skips with a reason when the deployment is unreachable, instead of reporting as OkHttp failing. This is exactly the case that machinery exists for, and our own server is no more entitled to a false red than anyone else's.
The Android matrix is the strongest argument for the whole thing: a device or emulator can reach a hosted URL directly, where today it needs adb reverse and a fixture process on the host (see run-ech-test.sh).
Acceptance
Host provisioned, with a DNS name and 443 inbound.
Compose stack running, restart: unless-stopped, surviving a reboot.
GET /info over the deployed name reports the expected listeners and the base URL the client sees.
GET /tls reports the negotiated handshake and a populated offered block.
Each of tls10–tls13 negotiates its version and refuses the others.
Certificate story decided and documented — which of the three options above, and why.
Watchtower (the maintained fork, pinned) scoped by label, verified: push to main, confirm the container is recreated within the poll interval and /health recovers.
test-serverbuilds and publishes toghcr.io/yschimke/okhttp-testbed/test-serveron every push tomain(#28). Nothing runs it. Until something does, the suites can only reach it through Testcontainers, and the two things a hosted instance is for — a real network path, and an endpoint the Android matrix can reach without Docker on the device — stay out of reach.This is the deployment, plus the part that keeps a deployment from going stale: Watchtower watching the published tag and restarting the container when the digest moves.
The host
Anything that runs Docker and has a name. A 1 vCPU / 1 GB VM is generous — the server is a static Go binary with no state and no disk use beyond logs.
Requirements, in order of how much they constrain the choice:
Ports
test-server/README.mdcovers this in full; the short version is that a non-standard port changes nothing about the results — every absolute URL the server emits is built from theHostthe request arrived with, and the CI smoke test runs the image on non-default ports to keep that true.The exception worth spending a real port on: 443 is the only port that tests the internet. Interception middleboxes, CDN behaviour and captive portals mostly act on 443, so an instance on 8443 is not being handled the way a user's traffic is. Put
httpsthere; the per-version and hostile ports can go wherever.httpshttprawtls10–tls13Certificates, and the one real conflict
The image has two modes and they are mutually exclusive today:
/ca.pem. This is what lets a test assert a chain is accepted.TLS_CERT_FILE,TLS_KEY_FILE) — a real, publicly trusted chain./ca.pem404s.A hosted instance wants both, for different suites: a publicly trusted chain exercises the platform's own trust anchors (which is the whole point of a deployment over a container), while the fixture CA is what the positive-chain assertions need. Both currently apply to every TLS listener at once.
Three ways out, in preference order:
httpsgets the real chain, the per-version ports keep the generated one. A small change totlsServer, and the honest fix.TLS_CERT_FILE, one without, on different ports or names. No code change; two things to keep running.Whichever, the certificate has to renew, so a deployment with a real chain needs an ACME client writing to a volume the container reads, and a restart or reload on renewal. Caddy is already in the compose stack and can do the ACME half.
Watchtower
Watchtower polls the registry, compares digests, and recreates a container when its tag moves. Since
test-server:latestis republished on every push tomain, that closes the loop: merge, and the deployment follows within the poll interval.Use the maintained fork,
nicholas-fedor/watchtower— published asnickfedor/watchtoweron Docker Hub andghcr.io/nicholas-fedor/watchtower, currently 1.20.3 and actively released. The originalcontainrrr/watchtowerhas been quiet since 1.7.1. The fork keeps the samecom.centurylinklabs.watchtower.*label namespace, so configuration and scoping carry over unchanged.It must be scoped to
test-serverand nothing else.httpbinandcaddyare pinned deliberately — a moved image is breakage the daily run should catch, not absorb, which is the same reasonmockserveris pinned. Watchtower updating those would quietly defeat that. Scope it withWATCHTOWER_LABEL_ENABLEand label only the one service:Things worth knowing before wiring it up:
latestships something broken, the fix is to pin the service to the previous commit tag (every build publishes:<sha>as well) and restart. Worth writing down next to the deployment rather than discovering under pressure.Pointing the suites at it
Once it exists, the suites need to find it, and to cope with it being down:
TESTBED_SERVER_URLor a Gradle property — defaulting to unset, so a developer with no deployment still runs the Testcontainers path.EndpointandProbeentry (Endpoint reachability preflight, and endpoint availability on the status page #7) so a suite skips with a reason when the deployment is unreachable, instead of reporting as OkHttp failing. This is exactly the case that machinery exists for, and our own server is no more entitled to a false red than anyone else's.adb reverseand a fixture process on the host (seerun-ech-test.sh).Acceptance
restart: unless-stopped, surviving a reboot.GET /infoover the deployed name reports the expected listeners and the base URL the client sees.GET /tlsreports the negotiated handshake and a populatedofferedblock.tls10–tls13negotiates its version and refuses the others.main, confirm the container is recreated within the poll interval and/healthrecovers.:<sha>, restart.Follows #28. Related: #8 (self-hosted fixtures), #7 (preflight), #21 (IPv6).