# Docker examples The published image, `linq`, is built from `../dockerfiles/Dockerfile`. Everything in this folder is an example, and none of it is published: - `docker-compose/`: one Compose stack per deployment shape. - `dockerfiles/`: the other image shapes those stacks build from, and the Caddy config they mount. ## Compose files Every combination of linq's optional pieces — Postgres bundled vs. external, Redis on/off (resources/docs/adr/0009), Caddy on/off (resources/docs/adr/0012). Pick the file that matches your setup, configure the repo-root `.env`, then `docker compose -f up`. Each `linq` service loads that file with `env_file`, so it is its runtime configuration as well as Compose's source for port, build-argument, and bundled-Postgres substitutions. All `build:`, volume-mount, and `env_file` paths are resolved relative to the Compose file's directory (`../../..` reaches the repo root), not your shell's directory. Run from `docker-compose/`, or use `-f docker/examples/docker-compose/` from the repo root. If you copy a file elsewhere, change `../../../.env` to the `.env` beside the copied file and fix the other relative paths first. Set `LINQ_PORT` in the repo-root `.env` to change the published port from 3000. Run from the repo root, or pass `--env-file ../../../.env` when running from `docker-compose/` so Compose can interpolate that value. The container and Caddy upstream stay on port 3000. `LINQ_DATA_DIR` is always `/data` in the container, so the Compose volume remains the persistent log location. `LINQ_DEFAULT_DOMAIN` can also be set in that env file; when absent, it defaults to `localhost:`. For an existing database, change the domain through the UI as well: the default domain only seeds an empty database. `LINQ_DB_SCHEMA` selects the PostgreSQL schema linq owns and defaults to `public`; set it when sharing a database with another application. The server creates it during migration when it does not already exist. The examples without Redis also pass through `LINQ_CACHE_SWEEP_INTERVAL`, accepting 1 to 3600 seconds and defaulting to 60, for the in-process cache. | # | File | Postgres | Redis | Caddy | | --- | --- | --- | --- | --- | | 1 | `01-bundled-postgres.yml` | bundled | – | – | | 2 | `02-bundled-postgres-redis.yml` | bundled | yes | – | | 3 | `03-bundled-postgres-caddy.yml` | bundled | – | yes | | 4 | `04-bundled-postgres-full.yml` | bundled | yes | yes | | 5 | `05-external-postgres.yml` | external | – | – | | 6 | `06-external-postgres-redis.yml` | external | yes | – | | 7 | `07-external-postgres-caddy.yml` | external | – | yes | | 8 | `08-external-postgres-full.yml` | external | yes | yes | Three more cover other deployment shapes: `09-server-only.yml` and `10-client-only.yml` split the API and the Client UI into separate containers, and `11-app-host.yml` puts both on one host of their own. They're described below. All eight of the above build from `../dockerfiles/Dockerfile` — the combined image, API plus Client UI, which these files build at `/home`. The UI path is a build-time option. Their build argument is wired to `LINQ_CLIENT_BASE_PATH` in `.env`; change that value and run `docker compose ... up --build`. It is baked into the frontend, so restarting without rebuilding is not enough. `dockerfiles/` also has `Dockerfile.server` (API only) and `Dockerfile.client` (Client UI only, as a standalone export); the two files below (9 and 10) build from those instead, for a split deployment across separate containers and ports. `09-server-only.yml` is not a stack on its own — it's a small override on `01-bundled-postgres.yml` that swaps in `Dockerfile.server`, so there's no Client UI at all, not even at `/home`. Layer it on top with a second `-f`, and pair the result with `10-client-only.yml`, which runs the standalone Client UI at `/`, with no API or database of its own. The client's host port is `${LINQ_CLIENT_PORT:-3001}`; the server's is `${LINQ_PORT:-3000}` as usual; both containers keep listening on 3000 internally. From the repo root: ```sh docker compose --env-file .env -f docker/examples/docker-compose/01-bundled-postgres.yml -f docker/examples/docker-compose/09-server-only.yml -f docker/examples/docker-compose/10-client-only.yml up --build -d ``` For example, `LINQ_PORT=8080` and `LINQ_CLIENT_PORT=8081` publish the API on 8080 and the standalone UI on 8081. Open `http://localhost:8081/`, add a server pointing at `http://localhost:8080`, and the UI talks to it across origins — no `/home` involved on either side. Choose distinct, unused host ports. Pairing `10-client-only.yml` with one of the eight combined-image files above works too, but is redundant: the combined image already serves a UI at `/home` on `LINQ_PORT`, so the separate client only makes sense there if you specifically want the Client UI at `/` as well. ## The UI and API on their own host One process can also serve the Client UI at `/` and the API at `/api/*` on one host, with short links on the others (`resources/docs/adr/0019`). Set `LINQ_APP_HOST` to that host in `.env`; it must differ from `LINQ_DEFAULT_DOMAIN` and from every domain you register. linq tells the hosts apart by the `Host` header, so point both at the same container. `11-app-host.yml` does this with the published `linq` image (`../dockerfiles/Dockerfile` with its default base path, `/`) against an external Postgres. It builds nothing, and it refuses to start without `LINQ_APP_HOST` and `LINQ_DEFAULT_DOMAIN`. `LINQ_IMAGE_REPOSITORY` and `LINQ_IMAGE_VERSION` pick the image, defaulting to `ghcr.io/org-quicko/linq:latest`. Any of the eight build-from-source files above works the same way: set `LINQ_CLIENT_BASE_PATH=/` and `LINQ_APP_HOST` in `.env`, then `docker compose ... up --build`. With a Caddy example (3, 4, 7, 8), linq pushes a route for `LINQ_APP_HOST` at boot next to the domain routes, so the app host gets its certificate like any domain does. Its DNS still has to point at this host. Archive any existing domain whose host equals `LINQ_APP_HOST` first. The UI claims every path on that host, so its links would stop resolving. "External" Postgres means no `postgres` service in the file — set `DATABASE_URL` in `.env` to point at your own instance instead. Set `LINQ_DB_SCHEMA` there as well when its tables are not in `public`. Both are loaded directly into the `linq` container through `env_file`. For a local check from the repo root: ```sh docker compose -f docker/examples/docker-compose/01-bundled-postgres.yml up --build -d docker compose -f docker/examples/docker-compose/01-bundled-postgres.yml logs linq curl http://localhost:3000/api/health ``` Open `http://localhost:3000/home/` for the UI and use the admin key printed in the logs. Substitute any other example filename to try that stack. Stop one before starting another: they publish the same host ports and share a default Compose project name. Use `down` with the same `-f` argument to stop it; add `--volumes` only when you intend to delete its stored data. For external Postgres on the Docker Desktop host, use `host.docker.internal` in `.env`'s `DATABASE_URL`, not `localhost` (which refers to the linq container). Caddy examples publish only ports 80 and 443; linq stays reachable to Caddy at `linq:3000` over the Compose network. With the default localhost domain, `https://localhost/api/health` uses Caddy's local CA; trust that CA or use `curl -k` for this local smoke check. Public certificate issuance still requires a real domain, public DNS, and inbound ports 80/443, and cannot be validated by a localhost-only test. ## Dockerfiles | File | Contains | | --- | --- | | `../dockerfiles/Dockerfile` | The published `linq` image: API and Client UI in one process, the UI at `LINQ_CLIENT_BASE_PATH` (`/` by default, on `LINQ_APP_HOST`) | | `dockerfiles/Dockerfile.server` | API and redirects only, no Client UI | | `dockerfiles/Dockerfile.client` | Client UI only, as a standalone static export | All run on `oven/bun:1.4` and build from the **repo root**, not this directory — the Client UI imports `@linq/shared` as TypeScript over a workspace link and its tsconfig reaches the repo root, so `bun install` needs the whole workspace present even when only one app is being built: ```sh docker build -f docker/examples/dockerfiles/Dockerfile.client -t linq-client . docker build -f docker/examples/dockerfiles/Dockerfile.server -t linq-server . docker build -f docker/dockerfiles/Dockerfile -t linq . ``` `../dockerfiles/Dockerfile` accepts `LINQ_CLIENT_BASE_PATH` as a build argument. It feeds the same value to the static frontend build and runtime server. The default is `/`; changing it requires rebuilding the image: ```sh docker build --build-arg LINQ_CLIENT_BASE_PATH=/admin/example -f docker/dockerfiles/Dockerfile -t linq . ``` The Dockerfile does not read `.env`; pass the argument explicitly. With Compose, put the literal value under `services.linq.build.args` in the Compose file you deploy, then rebuild. With the default `/`, the server starts only with `LINQ_APP_HOST` set at runtime, so the UI answers on that one host and every other host keeps its short links (`resources/docs/adr/0019`). `Dockerfile.client` runs `serve-static.ts` under plain Bun rather than introducing nginx as a second base image. It mirrors `apps/server/src/http/admin-static.ts` — the handler the combined image already uses to serve this same kind of export at `/home` — just mounted at `/` instead: trailing-slash resolution, immutable caching for hashed `_next/static` assets, the export's own `404.html`, and a path-traversal guard. It needs no environment or Postgres — the UI is pointed at a linq server at runtime, in the browser. The server images need `DATABASE_URL`, `LINQ_DB_SCHEMA` (default `public`), `LINQ_DEFAULT_DOMAIN`, etc. at runtime, and default `LINQ_CACHE_SWEEP_INTERVAL` to 60 seconds (1 to 3600). `dockerfiles/caddy/caddy.json` is the empty skeleton Caddy boots from in the Caddy examples (3, 4, 7, 8); see `resources/docs/adr/0012`. See the root `README.md` (Deploying section) for what each piece does.