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

6.9 KiB

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.
  • 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:

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

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:

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

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:

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 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)
  • 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.