> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit-dev.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker

> Running Orbit's containers day to day

Orbit ships with a Docker Compose setup, driven through a `Makefile` so you don't need to remember raw `docker compose` invocations. This is the same setup used in the [Quickstart](/quickstart) — this page covers the rest of the day-to-day commands.

## Services

* **`app`** — the Laravel application, serving on port `8000`. The image includes PCOV, so PHP test coverage works without extra setup.
* **`vite`** — the frontend dev server, serving assets and HMR on port `5173`.
* **`queue`** — runs `php artisan queue:work --tries=3` against the same image as `app`. Required for [email notifications](/concepts/notifications#channels) to actually be delivered — nothing else in the stack processes queued jobs.
* **`nsfwjs`** — the image-moderation service used to screen [avatar uploads](/concepts/account#profile), on port `3333`. `app` depends on it; `queue` depends on `app`.
* **`uptime-kuma`** — an optional monitoring dashboard on port `3001`, gated behind the Compose `monitoring` profile so it doesn't start with the rest of the stack by default (see below).

There's no separate `.env.docker` file — `docker-compose.yml` declares each service's environment as plain `KEY: ${KEY}` interpolation from whatever environment the `docker compose` process itself runs in. See [Configuration](/deployment/configuration) for what each variable does, and below for where those values actually come from.

## Where configuration comes from: Doppler or `.env`

Every `make` target that needs environment variables runs an `ensure-env` step first, and the `Makefile` picks one of two modes automatically by checking whether this directory has ever been linked to a [Doppler](https://www.doppler.com/) project (`doppler setup`):

* **Linked to Doppler** (the core team) — the `Makefile` wraps every command in `doppler run --`, injecting your Doppler config's secrets straight into the `docker compose` process. Doppler is the sole source of truth; no `.env` file is read or written, and `ensure-env` is a no-op.
* **Not linked to Doppler** (contributors, forks, anyone without Doppler access) — `ensure-env` falls back to a plain `.env` file at the repo root: it copies `.env.example` to `.env` on first run and generates a fresh `APP_KEY` if one isn't set yet, both idempotently. `docker compose` then reads that `.env` automatically for `${KEY}` interpolation.

You never choose between the two modes explicitly — detection is automatic, and either way the containers end up with the exact same set of variables, just sourced differently. Doppler isn't required to run Orbit locally; it's purely an internal convenience for the team that already has access to it.

<Note>
  If you're on the Doppler-linked path, generate `APP_KEY` yourself before your first `make setup`/`make up` — nothing inside the container writes back to Doppler:

  ```bash theme={null}
  php artisan key:generate --show      # prints a fresh base64:... key
  doppler secrets set APP_KEY="<paste the key here>"
  ```

  Seeing `MissingAppKeyException` or another config-looking error? In Doppler mode, diff `doppler secrets --only-names` against the `environment:` blocks in `docker-compose.yml`. In fallback mode, confirm `.env` exists with a non-empty `APP_KEY=base64:...` line — delete it and re-run any `make` target to regenerate it from scratch.
</Note>

<Note>
  `php artisan serve` normally treats the mere *presence* of a `.env` file as a signal to filter which environment variables reach the request-handling process. That's harmless in the fallback mode (the filtered set is the same `.env` it read from), but it would break the Doppler-linked mode, where the real config lives only in the process environment injected by `doppler run --`, not in a file. That's why the `app` container's `CMD` runs `serve` with `--no-reload` — it disables that filtering so the full environment always passes through, regardless of which mode is active.
</Note>

## Common commands

| Command                 | Does                                                                                                                              |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `make setup`            | Builds every image without cache and starts the default stack, bootstrapping `.env`, the SQLite file, the app key, and migrations |
| `make up`               | Starts the stack in the foreground, streaming logs — identical to `make dev`                                                      |
| `make dev`              | Same as `make up`                                                                                                                 |
| `make up-d`             | Starts the stack detached                                                                                                         |
| `make down`             | Stops the stack, keeping volumes                                                                                                  |
| `make clean`            | Stops the stack and drops volumes — use this if the database or dependencies need a truly clean slate                             |
| `make shell`            | Opens a shell in the `app` container                                                                                              |
| `make tinker`           | Opens a Laravel Tinker session inside the `app` container                                                                         |
| `make migrate`          | Runs `php artisan migrate` inside the container                                                                                   |
| `make fresh`            | Runs `migrate:fresh --seed` — drops all tables, re-migrates, and reseeds demo data                                                |
| `make composer-install` | Rebuilds and restarts `app` after adding, updating, or removing a Composer dependency                                             |
| `make npm-install`      | Rebuilds and restarts `vite` after adding, updating, or removing an npm dependency                                                |
| `make logs`             | Tails container logs                                                                                                              |

## Monitoring stack

`uptime-kuma` doesn't start with `make up` or `make dev` — it lives behind Compose's `monitoring` profile so it's opt-in:

| Command                | Does                                     |
| ---------------------- | ---------------------------------------- |
| `make up-monitoring`   | Starts `uptime-kuma` on `localhost:3001` |
| `make down-monitoring` | Stops it                                 |
| `make logs-monitoring` | Tails its logs                           |

## Running tests in Docker

| Command                 | Runs                                    |
| ----------------------- | --------------------------------------- |
| `make test`             | The PHP (Pest) suite                    |
| `make test-coverage`    | The PHP suite with the coverage gate    |
| `make test-js`          | The Vitest suite once                   |
| `make test-js-coverage` | The Vitest suite with the coverage gate |
| `make lint`             | ESLint over the frontend                |
| `make type-check`       | TypeScript's `tsc`                      |

## Troubleshooting: stale Vite HMR

If hot reload stops working and the browser console shows WebSocket errors, the file watcher inside the `vite` container has likely desynced from the filesystem — this is more common when the repo lives on non-native storage (for example, an external or removable drive), since Vite falls back to polling (`server.watch.usePolling` in `vite.config.js`) rather than native filesystem events across Docker bind mounts.

<Steps>
  <Step title="Restart the containers">
    ```bash theme={null}
    make down
    make up-d
    ```
  </Step>

  <Step title="If that doesn't fix it, rebuild from scratch">
    ```bash theme={null}
    make clean
    make setup
    ```
  </Step>
</Steps>

<Note>
  This isn't fixed by raising the `fs.inotify.max_user_watches` kernel limit — that sysctl is global, not per-container, so Docker won't start a container that tries to set it per-service. Polling is the mechanism actually in effect here, and a restart forces a fresh full re-scan.
</Note>


## Related topics

- [Quickstart](/quickstart.md)
- [Testing](/architecture/testing.md)
- [FAQ and troubleshooting](/reference/faq.md)
