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
159 lines
6.9 KiB
Markdown
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.
|