# Deployment (Docker) Everything Docker lives in `docker/`: - **`docker/compose.yaml`** — production. A **self-contained image**: the app code and `composer install --no-dev` are baked in at build time (`docker/php/Dockerfile`, target `prod`). Nothing from the working tree is mounted except `.env`. Persistent state lives in `docker/data/`. Wired for an **external Traefik** proxy on the `traefik_public` network. - **`docker/compose.dev.yaml`** — development / live-reload. Bind-mounts the working tree over `/var/www/html`; edits to PHP / Blade / assets apply on the next request. State is in throwaway named volumes. See [development.md](development.md). - `docker/compose.override.yaml.template` / `docker/compose.dev.override.yaml.template` — copy each to the matching `*.override.yaml` (git-ignored) to run **without** Traefik by publishing a host port. - `docker/php/Dockerfile` — PHP 8.4 + Apache, `ldap`/`pdo_mysql`/`opcache`, Composer. Stages: `base` → `dev` (no code baked in) / `prod` (code + deps baked in). - `docker/php/entrypoint.sh` — on every boot: install `vendor/` if missing, generate `APP_KEY`, create + `chown` the `storage/` tree, `migrate --force`, warm caches (prod) / just clear them (dev). Idempotent. ## Invocation Because the Compose files sit in `docker/`, always run **from the repo root** and pass `--env-file .env` so `${DB_*}` / `${APP_DOMAIN}` interpolation reads the repo-root `.env`: ```sh docker compose --env-file .env -f docker/compose.yaml ``` (Tip: `export COMPOSE_FILE=docker/compose.yaml COMPOSE_ENV_FILES=.env` once per shell, then plain `docker compose …` works.) ## With Traefik ```sh cp .env.example .env # APP_ENV=production, APP_DEBUG=false # APP_URL=https://adconsole.example.com # APP_DOMAIN=adconsole.example.com <- Traefik Host() rule # DB_PASSWORD / DB_ROOT_PASSWORD <- set real values # LDAP_* / AD_* <- your domain docker compose --env-file .env -f docker/compose.yaml up -d # pulls the release image docker compose --env-file .env -f docker/compose.yaml logs -f app # watch bootstrap ``` `docker/compose.yaml` defaults to the image the Gitea release workflow publishes (`gitea.kzbikowski.pl/kzbkowski/adconsole:1.1.0`), so `up -d` **pulls** it — no local build. Add `--build` to build from your working tree instead; the result is tagged with the same name. Enable a certificate resolver by uncommenting the `certresolver` label in `docker/compose.yaml` and setting its name to match your Traefik config. The `.env` file is **bind-mounted**, not baked into the image: edit it and `docker compose … restart app` — no rebuild needed. Everything else is immutable. ### The image `.github/workflows/release.yml` builds `docker/php/Dockerfile` target `prod` on every `vX.Y.Z` tag and pushes `…/adconsole:{X.Y.Z, X.Y, latest}` to the Gitea container registry (needs Actions enabled and `packages: write`; set a `REGISTRY_TOKEN` secret if the built-in token is not allowed to push). Deploy a specific release: ```sh ADCONSOLE_VERSION=1.1.0 \ docker compose --env-file .env -f docker/compose.yaml pull ADCONSOLE_VERSION=1.1.0 \ docker compose --env-file .env -f docker/compose.yaml up -d ``` Set `ADCONSOLE_VERSION` (or the full `ADCONSOLE_IMAGE`) in `.env` to make it stick. A different registry / private mirror: `ADCONSOLE_IMAGE=host/adconsole:tag`. To publish from a workstation instead of CI: `docker compose … build && docker compose … push`. ## Without Traefik ```sh cp docker/compose.override.yaml.template docker/compose.override.yaml # APP_URL=http://localhost:8080 in .env docker compose --env-file .env \ -f docker/compose.yaml -f docker/compose.override.yaml up -d --build ``` App on `http://localhost:8080`, MariaDB on `localhost:3306`. Put a TLS-terminating proxy (nginx, Caddy, your load balancer) in front for anything but a lab. ## Data & backups Production state is under **`docker/data/`** (git-ignored), created on first run: | What | Path | Back up? | |---|---|---| | Database (`panel_settings` + `panel_attributes` config, `panel_audit` change log, sessions, cache) | `docker/data/db/` | **Yes** — `panel_settings` / `panel_attributes` are your config and `panel_audit` is your only record of who changed what. `docker compose … exec mariadb mariadb-dump -uroot -p adconsole`. | | `storage/` (logs, framework cache, sessions if file-driver) | `docker/data/app_storage/` | Optional (logs only really). | | `.env` | bind-mounted from the repo root | **Yes**, securely — it holds the service-account password. | A file-level copy of `docker/data/db/` is only consistent with the container stopped; prefer `mariadb-dump` on a running stack. The panel's own config also has a JSON export at `/settings` → *General* → *Configuration backup*. > On **rootless Docker** the MariaDB image can fail to `chown` a bind-mounted > datadir. Either run the daemon rootful, or switch `docker/compose.yaml`'s > `mariadb` volume to a named volume (as `docker/compose.dev.yaml` already does). **Audit-log retention.** `panel_audit` grows with panel activity and nothing prunes it automatically. If you need a retention window, trim it with a scheduled job of your own, e.g. `DELETE FROM panel_audit WHERE created_at < NOW() - INTERVAL 365 DAY;`, and back it up (or ship it to a SIEM) before trimming if the entries have compliance value. ## Upgrades Bump `ADCONSOLE_VERSION` in `.env` (or `git pull` if you track `main` and rely on the `latest` tag), then: ```sh docker compose --env-file .env -f docker/compose.yaml pull docker compose --env-file .env -f docker/compose.yaml up -d ``` The entrypoint runs `migrate --force` and `optimize` on the new container. See [upgrading.md](upgrading.md) for version-specific notes. ## Hardening checklist - [ ] `APP_DEBUG=false`, `APP_ENV=production` - [ ] HTTPS only; HSTS at the proxy - [ ] `LDAP_SSL=true` (or `LDAP_TLS=true`); `LDAP_TLS_VERIFY=true` once the CA is trusted by the app host - [ ] Service account delegated **only** the rights you use ([active-directory.md](active-directory.md)) - [ ] `AD_LOGIN_GROUP` is a small, audited group - [ ] Panel reachable only from admin networks / VPN (proxy ACL or firewall) - [ ] DB not exposed to the internet; strong `DB_PASSWORD` / `DB_ROOT_PASSWORD` - [ ] `.env` file permissions `600`, owned by the deploy user - [ ] `docker/data/` on a backed-up filesystem, permissions restricted to the deploy user - [ ] Log shipping / retention configured; review `LDAP_LOGGING` volume - [ ] Backups of the database tested ## Scaling notes - Multiple app containers: move `SESSION_DRIVER` and the app caches off local disk (`database`/`redis`) — note the OU-tree/dashboard/org-chart caches are hard-coded to the `file` store, so with >1 node each node keeps its own (they expire in minutes; acceptable, or add a shared store and change `Cache::store('file')` call sites). - One `mariadb` is plenty; the workload is tiny.