Files
kzbkowskiandClaude Sonnet 5 b313c126ce
CI / PHP 8.2 (push) Failing after 1m40s
CI / PHP 8.3 (push) Failing after 55s
CI / PHP 8.4 (push) Failing after 1m11s
Release image / image (push) Successful in 2m33s
ADconsole v1.1.0
Config backup, a Docker dev/prod split, and a Gitea-built release image.

Added
- /settings -> General -> "Configuration backup": JSON export/import of the
  panel's general settings + attribute definitions
  (App\Services\ConfigBackupService); audited as config.export / config.import.
  Export can leave out the attribute-encryption key. No AD data included.
- docker/compose.dev.yaml: live-reload dev stack that bind-mounts the working
  tree. docker/php/Dockerfile is now multi-stage (base -> dev / prod).
- .github/workflows/release.yml: on every vX.Y.Z tag, builds the prod image and
  pushes gitea.kzbikowski.pl/kzbkowski/adconsole:{X.Y.Z,X.Y,latest}.

Changed
- docker/compose.yaml: self-contained prod image (app code + composer install
  --no-dev baked in via COPY); persistent state in docker/data/{app_storage,db};
  defaults to the released image (ADCONSOLE_VERSION pins a tag, ADCONSOLE_IMAGE
  replaces the ref). Apache now honours public/.htaccess (AllowOverride All).
  Run from the repo root with --env-file .env.
- Repo root now holds only README.md; CHANGELOG / CONTRIBUTING / SECURITY /
  CODE_OF_CONDUCT moved under docs/.

Upgrade notes: docs/upgrading.md#v110--docker-layout. Full log: docs/CHANGELOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JKT9p3bfP8RZesExdYCS52
2026-09-10 09:13:04 +02:00

159 lines
6.9 KiB
Markdown

# 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 <command>
```
(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.