mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
Six defects found by running the thing rather than reading it. The previous change was verified by bundling, by tests, and by the container images — none of which start a Worker, so every one of these was invisible. **`bun dev` did not start.** It ran one multi-config process, which does not connect a service binding between the workers it loads; the API reported `AUTH [not connected]` and could not verify a token. It is two processes now, which is what the dev registry connects, and the second is backgrounded with the first killed on exit so stopping the pair stops both. **Neither server could bind.** Wrangler resolves `localhost` and takes `::1` first; a host with no IPv6 address on its loopback dies with a bind error from inside the runtime that names neither the app nor the port. `dev.ip` is pinned to `127.0.0.1`, and `inspector_port` is now distinct per app — it is not derived from the port above, so the second server to start died on an address already in use. **The API worker failed to evaluate.** A specifier ending in `.sql` is claimed by the bundler as a module of its own, so the schema file was emitted verbatim beside the bundle and the runtime threw on an export it could not find. The route was reaching past the domain module into the schema to spell a status; it now asks the domain module, which is the rule everywhere else here and happens to also avoid the hazard. **Signing in failed on the second request that touched the database.** A pool is cached per connection string, and on a Worker an I/O object created while handling one request may not be touched while handling another. The first request always succeeded, which is why it went unnoticed — a sign-in is several. The cache is now kept only where a process outlives its requests, which is the case it was added for. **The images named a base that podman will not resolve.** A short name needs a registry; the database service alongside them already spelled one. **Compose pinned container names.** The name is not scoped to the project, so a second checkout got the same three, and `down` in one stopped the other's containers. This is not hypothetical — it stopped a running development database while this was being tested. Verified by signing in end to end against both dev servers: a code requested over HTTP, read from the issuer's log, redeemed, exchanged for tokens, and presented to the API, which resolved it to the account the sign-in had just created.
110 lines
4.9 KiB
YAML
110 lines
4.9 KiB
YAML
# The whole control plane on one machine.
|
|
#
|
|
# Two uses, deliberately the same file. It is what a self-hoster runs, and it
|
|
# is the shape this deployment takes when it stops being a set of Workers: two
|
|
# stateless processes and a database, with a reverse proxy in front of them
|
|
# terminating TLS. Nothing here knows about a hosting provider.
|
|
#
|
|
# cp .env.example .env # then fill it in
|
|
# docker compose up --build everything, built from source
|
|
# docker compose up postgres just the database, for `bun dev`
|
|
#
|
|
# No service sets `container_name`. It reads like a nicety and behaves like a
|
|
# global: the name is not scoped to the project, so a second checkout of this
|
|
# repo — a worktree, a colleague's clone — gets the same three names, and
|
|
# `docker compose down` in one of them stops the containers belonging to the
|
|
# other. Compose names them per project on its own, which is what makes two
|
|
# checkouts independent.
|
|
#
|
|
# **There are no credentials in this file, and none of them have defaults.**
|
|
# Every one is read from `.env`, and compose refuses to start naming the
|
|
# variable it wanted rather than falling back to something. A default is worth
|
|
# less than it looks: the deployment that never set the variable is exactly the
|
|
# one where the default is a publicly known value, and `ADMIN_SHARED_SECRET`
|
|
# below bypasses authentication entirely.
|
|
#
|
|
# Migrations are not run for you — `bun run db:migrate` against DATABASE_URL,
|
|
# because a container that migrates on boot races with the second copy of
|
|
# itself and there is eventually a second copy.
|
|
|
|
x-postgres-url: &postgres-url
|
|
DATABASE_URL: postgres://${POSTGRES_USER:?set POSTGRES_USER in .env}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/${POSTGRES_DB:?set POSTGRES_DB in .env}
|
|
|
|
services:
|
|
postgres:
|
|
image: docker.io/postgres:18-alpine
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
|
POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env}
|
|
# Loopback, not every interface. `5432:5432` would publish the database to
|
|
# anything that can reach this host. The three services below talk to each
|
|
# other over the compose network and do not use this mapping at all; it is
|
|
# here only so `bun dev` and `bun run db:migrate` can reach the database
|
|
# from outside a container.
|
|
ports:
|
|
- '127.0.0.1:5432:5432'
|
|
volumes:
|
|
- nestri_data:/var/lib/postgresql
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"']
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
|
|
auth:
|
|
build:
|
|
# The repository root, because the lockfile and the shared packages are
|
|
# there. Same reason for the API below.
|
|
context: .
|
|
dockerfile: apps/auth/Dockerfile
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
environment:
|
|
<<: *postgres-url
|
|
# Passed through rather than fixed here, so that setting them in `.env`
|
|
# is enough to make this deployment deliver mail. All three together or
|
|
# none of them: the issuer refuses to send when they are half configured.
|
|
EMAIL_SEND_URL: ${EMAIL_SEND_URL:-}
|
|
EMAIL_API_KEY: ${EMAIL_API_KEY:-}
|
|
EMAIL_FROM: ${EMAIL_FROM:-}
|
|
# Printing a live sign-in code to the log is a thing you ask for by name,
|
|
# and it is asked for in `.env` — not defaulted to here. With mail
|
|
# unconfigured and this unset, the issuer refuses to send rather than
|
|
# logging codes, which is the failure a self-hoster should get.
|
|
EMAIL_DEV_LOG: ${EMAIL_DEV_LOG:-}
|
|
# Loopback. This listener speaks plain HTTP and sets no `Secure` on the
|
|
# cookies it issues, because it expects to be behind something that
|
|
# terminates TLS. Published on every interface it would be a way to reach
|
|
# the issuer *around* that proxy, with codes and tokens in clear text.
|
|
ports:
|
|
- '127.0.0.1:1337:1337'
|
|
|
|
api:
|
|
build:
|
|
context: .
|
|
dockerfile: apps/api/Dockerfile
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
auth:
|
|
condition: service_started
|
|
environment:
|
|
<<: *postgres-url
|
|
# The issuer's public URL, and not `http://auth:1337`. A token carries
|
|
# the address it was minted through, and verification compares the two
|
|
# literally — so the name a browser used is the only one that can appear
|
|
# here. `AUTH_INTERNAL_URL` is how this container actually gets there.
|
|
AUTH_ISSUER_URL: ${AUTH_ISSUER_URL:?set AUTH_ISSUER_URL in .env}
|
|
AUTH_INTERNAL_URL: http://auth:1337
|
|
# A shared secret that turns any request carrying it into an operator.
|
|
# Required, with no default, for that reason.
|
|
ADMIN_SHARED_SECRET: ${ADMIN_SHARED_SECRET:?set ADMIN_SHARED_SECRET in .env to a value you generated}
|
|
# Loopback, for the same reason as the issuer above.
|
|
ports:
|
|
- '127.0.0.1:3000:3000'
|
|
|
|
volumes:
|
|
nestri_data:
|