Deployment (container)
TinyGuard ships as a single container image: the provider and the admin console are one binary, served from the same origin. This page covers running it directly; see Kubernetes for the Helm chart.
The image
Section titled “The image”registry.tinyfactory.ai/tinyblox/guard:<tag>- Base: distroless, non-root (
uid:gid 65532:65532), no shell. - Ports:
8080HTTP,8443HTTPS,9090metrics. - Entrypoint:
idp-server, which defaults to theservesubcommand. - Tags:
sha-<commit>on every build, the released version, andlateston the release line. Deploy by digest (@sha256:…) — a tag can move, a digest cannot.
The image is private; authenticate before pulling:
docker login registry.tinyfactory.aiBring it up, in order
Section titled “Bring it up, in order”Nothing starts without its inputs, so prepare them first:
- Generate the secrets file: the encryption key. Keep a copy.
- Write the configuration, or set the settings as
GUARD_<SECTION>_<KEY>environment variables.config checkvalidates it without starting anything. - Choose the first administrator: an address and a password, or let the first start generate one.
- Run it.
Kubernetes follows the same sequence with Secrets instead of files; see Kubernetes.
Secrets
Section titled “Secrets”The secrets file holds the deployment’s encryption key and any value the
configuration refers to by a $SECRETS.<name> marker. Generate one once, with
nothing but the image (--user lets the container write into your directory):
IMAGE=registry.tinyfactory.ai/tinyblox/guard:<tag>docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work \ "$IMAGE" generate-secrets --output secrets.tomlKeep it out of version control and back it up: the encryption key in this file is what makes stored client secrets, provider credentials, and tenant values readable. Losing it loses them.
Any configuration string can pull a value from the file instead of holding it inline:
[storage]password = "$SECRETS.pg_password"For Vault, AWS/GCP/Azure secret managers, mounted files, and rotating the encryption key, see Secret sources and Envelope key rotation.
Configuration
Section titled “Configuration”Every key is documented in the configuration reference. A minimal production configuration:
[server]public_url = "idp.example.com"listen_scheme = "http"listen_host = "0.0.0.0"http_port = 8080console_static_dir = "/app/console"
[server.metrics]enabled = truelisten_host = "0.0.0.0"port = 9090
[storage]mode = "embedded"data_directory = "/app/data"Validate a configuration before you deploy it. config check loads it exactly
as serve would, environment included, runs the same checks, touches no
network, and exits non-zero with the reason. Give it the environment you will
run with:
docker run --rm \ -e GUARD_SERVER_PUBLIC_URL=id.example.com -e GUARD_STORAGE_MODE=postgres \ -v /etc/tinyguard/config.toml:/app/config/config.toml:ro \ -v /etc/tinyguard/secrets.toml:/app/secrets/secrets.toml:ro \ registry.tinyfactory.ai/tinyblox/guard:latest \ config check --config /app/config/config.toml --secrets /app/secrets/secrets.tomlAdd --print to see the effective configuration (secrets redacted), and an
unknown GUARD_* variable is an error — GUARD_SERVER_HTTP_PROT is reported
with the name you meant. validate-config checks the file alone and ignores the
environment. Every name is in the environment reference.
generate-config writes a commented template of every section when you would
rather start from the full surface.
TLS in front of the container
Section titled “TLS in front of the container”Set listen_scheme = "http" and terminate TLS at your proxy or ingress — the
provider then issues https://<public_url> URLs while serving plain HTTP
inside the network. Tell it which proxies to trust so client addresses and
forwarded headers are read from the right hop:
[server]proxy_enabled = truetrusted_proxies = ["10.0.0.0/8"]List the addresses your proxy connects from (a single address or a network).
The client address is then the last address in X-Forwarded-For that is not
one of your proxies; entries a client adds itself are ignored. Sign-in, the
admin console and the API all use this one address, and a session only works
from the address it signed in from. Requests from any other peer keep their
own address.
To serve TLS from the container instead, set listen_scheme = "https" and
mount a certificate:
[server.tls]cert_path = "/app/tls/tls.crt"key_path = "/app/tls/tls.key"Self-signed material (self_signed = true) is for development only.
First administrator
Section titled “First administrator”On the first start against an empty store, the provider seeds one administrator and grants it the tenant and platform administration roles:
GUARD_BOOTSTRAP_ADMIN_EMAIL— the address (defaultadmin@idp.local).GUARD_BOOTSTRAP_ADMIN_PASSWORD_PLAIN— a password, hashed at boot; orGUARD_BOOTSTRAP_ADMIN_PASSWORD_ARGON2ID— a pre-computed Argon2id hash.
To make a hash with nothing but the image (it reads the password from stdin, so it stays out of your shell history):
printf '%s' 'a-long-passphrase' | docker run --rm -i "$IMAGE" hash-passwordWithout either variable, a random password is generated and printed once to the
server log. Sign in at https://<public_url>/auth/v1/, change the password,
then remove the variables.
To read back a generated one-time value later:
idp-server bootstrap get --config … --secrets … --format jsonRun it
Section titled “Run it”Do the three steps above first: you need secrets.toml, a config.toml (or the
settings as GUARD_* variables), and the first administrator’s details. The
image’s default command is a demo configuration (loopback-oriented,
self-signed TLS, embedded storage). Real deployments mount their own
configuration and secrets and name them explicitly:
docker run -d --name tinyguard \ -p 8080:8080 \ --read-only --tmpfs /tmp \ -v "$PWD/config.toml:/app/config/config.toml:ro" \ -v "$PWD/secrets.toml:/app/secrets/secrets.toml:ro" \ -v tinyguard-data:/app/data \ -e GUARD_BOOTSTRAP_ADMIN_EMAIL=admin@example.com \ -e GUARD_BOOTSTRAP_ADMIN_PASSWORD_PLAIN='<a strong password>' \ registry.tinyfactory.ai/tinyblox/guard:latest \ serve --config /app/config/config.toml --secrets /app/secrets/secrets.tomlThree things to know about the filesystem:
- The root filesystem can be read-only. The process writes only under the
data directory and
/tmp; mount/tmpas a tmpfs (--tmpfs /tmp). console_static_dirmust point at the bundle baked into the image (/app/console). Do not mount over it.- Never bake secrets into an image. They are files you mount, or values a secret manager resolves at boot.
Storage
Section titled “Storage”| Mode | Use | Notes |
|---|---|---|
embedded |
Single node, or a first deployment | The database lives in data_directory; give it a volume. One instance only. |
postgres |
Multiple replicas, managed backups, a shared database server | Any PostgreSQL 14 or newer. See below. |
[storage]mode = "postgres"host = "db.internal"user = "tinyguard"password = "$SECRETS.pg_password"db_name = "tinyguard"schema = "tinyguard"tls_mode = "verify-full"ca_file = "/etc/ssl/pg-ca.pem" # omit to use the system trust storepool_size = 10PostgreSQL
Section titled “PostgreSQL”TinyGuard works with any PostgreSQL 14 or newer, however it is run. It asks for four things and does a few more on its own account.
A database and a schema. It keeps everything in one named schema
(tinyguard unless you set schema) and never uses public or an
unqualified table name, so it can share a database with other components
without colliding with them. The login needs USAGE and CREATE on that
schema. If the schema does not exist and the login may not create it, the
error says what to run: CREATE SCHEMA tinyguard AUTHORIZATION <login>.
TLS that means what it says.
tls_mode |
Encrypted | Server’s certificate checked | Host name checked |
|---|---|---|---|
disable |
no | – | – |
prefer |
if offered | no | no |
require |
yes | no | no |
verify-ca |
yes | yes | no |
verify-full |
yes | yes | yes |
Only verify-full tells you that you reached the server you meant to. prefer
and require stop a passive observer but not someone in the path, so use
verify-full anywhere but a development machine. It is the default, so a
database without TLS has to be named explicitly with tls_mode = "disable". Point ca_file at the
authority that signed the server’s certificate, or leave it out to use the
system trust store. An unrecognised mode is a configuration error: earlier
builds read anything they did not recognise as prefer, and connected without
TLS at all.
A connection budget. Each replica holds up to pool_size connections
(default 10), opened as needed. The server must allow the sum:
max_connections >= replicas x pool_size + a few for migrations and toolsSize pool_size from your concurrency, not your replica count: a pool of 4
carried 400 simultaneous operations in testing. Give the login a
CONNECTION LIMIT to match, so one component cannot use up the server.
Migrations that you control. By default every replica applies pending schema steps at boot, taking turns on a database lock so concurrent starts do not race. To keep the runtime login to data access only, run the steps yourself with a login that may change the schema:
GUARD_STORAGE_USER=tinyguard_owner GUARD_STORAGE_PASSWORD=… \ idp-server migrate --config config.toml --secrets secrets.tomland set auto_migrate = false. Boot then only checks that the schema is
current and refuses to start, naming migrate, if it is not. The Helm chart
can run this as a hook Job before each install and upgrade; see
Kubernetes.
One leader. Some duties must run once, not once per replica: signing-key rotation, session and event cleanup, the email and event pipelines. Replicas agree on which of them does it through a lease in the database, renewed every few seconds and expiring after 30 seconds without renewal, so a crashed leader is replaced within that time. A replica that cannot reach the database assumes it is not the leader: a duty skipped for a moment is harmless, a duty run twice is not.
What it does not need. No extension, no superuser, no replication setup. Tenants are a column in the data (derived from the key, indexed), so a tenant’s size is a query and its data is a range rather than a scan.
Behind a connection pooler
Section titled “Behind a connection pooler”TinyGuard uses prepared statements and sends statement_timeout as a startup
parameter, so a pooler in transaction mode needs two things: PgBouncer 1.21 or
newer with max_prepared_statements set, and either
ignore_startup_parameters = options or statement_timeout_seconds = 0. It
uses no session-level locks and no LISTEN/NOTIFY. A pooler is optional:
the pool is already bounded.
Sharing a server
Section titled “Sharing a server”Give each component its own database or its own schema, its own login, and a
CONNECTION LIMIT; set a statement_timeout for the login; and count every
component’s connection budget against max_connections. A starting point for
the runtime login, to adjust to your workload:
ALTER ROLE tinyguard CONNECTION LIMIT 24; -- replicas x pool_size, plus headroomALTER ROLE tinyguard SET statement_timeout = '30s'; -- the service also sends its own (storage.statement_timeout_seconds)ALTER ROLE tinyguard SET idle_in_transaction_session_timeout = '60s';The service sets statement_timeout on its own sessions, so the role setting
is a backstop for anything else that connects as that login. It never holds a
transaction open across a request, so a short idle_in_transaction_session_timeout
costs nothing and frees a leaked session. In production, keep
the identity provider’s database out of the failure domain of the services
that depend on signing in to it — an outage should not take down both.
Health and metrics
Section titled “Health and metrics”| Endpoint | Purpose |
|---|---|
/livez |
Liveness — the process is up. Never touches storage. |
/readyz |
Readiness — 200 while storage and the caches answer, 503 when either does not, so an instance without a database stops receiving traffic. |
/auth/v1/health |
Detailed subsystem health (which of storage and the caches is failing). |
/auth/v1/version |
Build version and whether an update is available. |
:9090/metrics |
Metrics ([server.metrics]). Keep it in-cluster. |
/livez and /readyz are unversioned and unauthenticated, on the main port,
and answer directly (no redirect) with an empty body: the status is the answer.
They are the platform’s standard probe paths, so one probe definition fits
every component. The versioned spellings /auth/v1/ping and /auth/v1/ready
answer identically and remain.
Point liveness at /livez and readiness at /readyz, and give the first start
a startup probe on /readyz: a restarting pod should never be sent traffic
before /readyz answers, and liveness must not depend on the database —
restarting a healthy process cannot repair a database that is down.
Shutdown
Section titled “Shutdown”On SIGTERM the server stops accepting connections, lets in-flight requests
finish for up to server.graceful_shutdown_seconds (default 10), and then
hands leadership of the singleton duties to another replica so a rolling
deploy does not leave them unowned. Give the container a
terminationGracePeriodSeconds comfortably above the drain bound (the chart
sets 30).
Upgrades
Section titled “Upgrades”- Read the CHANGELOG for the version you are moving to.
- Deploy the new digest.
- The server applies its own storage migrations on boot; a failed migration refuses to start rather than serving half-migrated data.
Embedded storage pins you to one pod, so an upgrade is a restart with a brief outage; on Postgres, roll the deployment normally.