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
6.9 KiB
Deployment (Docker)
Everything Docker lives in docker/:
docker/compose.yaml— production. A self-contained image: the app code andcomposer install --no-devare baked in at build time (docker/php/Dockerfile, targetprod). Nothing from the working tree is mounted except.env. Persistent state lives indocker/data/. Wired for an external Traefik proxy on thetraefik_publicnetwork.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: installvendor/if missing, generateAPP_KEY, create +chownthestorage/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
chowna bind-mounted datadir. Either run the daemon rootful, or switchdocker/compose.yaml'smariadbvolume to a named volume (asdocker/compose.dev.yamlalready 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(orLDAP_TLS=true);LDAP_TLS_VERIFY=trueonce the CA is trusted by the app host- Service account delegated only the rights you use (active-directory.md)
AD_LOGIN_GROUPis 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 .envfile permissions600, owned by the deploy userdocker/data/on a backed-up filesystem, permissions restricted to the deploy user- Log shipping / retention configured; review
LDAP_LOGGINGvolume - Backups of the database tested
Scaling notes
- Multiple app containers: move
SESSION_DRIVERand the app caches off local disk (database/redis) — note the OU-tree/dashboard/org-chart caches are hard-coded to thefilestore, so with >1 node each node keeps its own (they expire in minutes; acceptable, or add a shared store and changeCache::store('file')call sites). - One
mariadbis plenty; the workload is tiny.