Files
bin/plan.md
mkt cebe829dcd Add README, customization snapshot, and snapshot/restore tooling
Provisioning now restores all GUI customizations on reset+reprovision:

- create_pod_espocrm.sh: deploy the version-controlled espocrm-custom/ tree
  (CTag entity, layouts, i18n, clientDefs, custom views, custom CSS) into the
  pod, then chown www-data and rebuild. Replaces the earlier inline CSS-only
  step. Adds a live-phase cache rebuild so customizations and the client
  cacheTimestamp are refreshed on every run.
- espocrm-custom/: snapshot of custom/ and client/custom/ (source of truth).
- snapshot_espocrm_custom.sh: refresh the snapshot from a running pod.
- readme.md: usage, first-time host setup, image-update and reset workflows.
- Include the task/instruction notes and plan.md for reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:51:02 +02:00

159 lines
9.4 KiB
Markdown

# Install EspoCRM on DesTEngSsv006 (rootless Podman pod)
Goal: create four shell scripts that run EspoCRM as a rootless Podman pod under the local user `mkt`.
A reference script, `create_pod_traefik.sh`, is attached. Follow its structure and conventions (the "House style" section below).
## Target environment (given — do not provision in the scripts)
- Host: DesTEngSsv006, Debian GNU/Linux 12 (bookworm), Kernel 6.1.0-49-amd64, x86-64.
- Podman 4.3.1, rootless. User `mkt` exists; subuid/subgid and `enable-linger` are configured. All scripts run as user `mkt`.
- Scripts live in `/home/mkt/bin` (a private git repo, not public, classified as a company secret). Plaintext passwords in the scripts are therefore deliberately acceptable.
- **Autostart:** Podman 4.3.1 has **no Quadlet** (introduced in 4.4). Use `podman generate systemd --files --new` (systemd `--user`) for autostart. Do not use Quadlet.
## House style (mandatory — follow `create_pod_traefik.sh`)
- Shebang `#!/bin/bash`, short comment header (who runs it, what it does).
- A block of environment variables at the top (pod name, container names, images, ports, IPs, `BIND_DIR`, systemd paths).
- Network option exactly as in the reference: `NET_OPTS='slirp4netns:allow_host_loopback=true,port_handler=slirp4netns'`.
- Data stored as bind mounts under `BIND_DIR="$HOME/.local/share/$POD_NAME"`, passed via `-v` in the `podman` command (no named volumes).
- Create the pod guarded with `if ! podman pod exists`; publish ports on the pod via `-p`.
- Create containers with `podman run -d --name ... --pod "$POD_NAME" -v ... "$IMAGE"`.
- Generate systemd units with `cd "$USER_SYSTEMD_DIR"; podman generate systemd --files --new --name "$POD_NAME"`.
- Then stop and remove the live pod, and `systemctl --user enable --now` the generated services (so the systemd-managed instance is what runs in the end).
- Status messages via `echo "... (rc=$?)"` as in the reference.
## Fixed parameters
- Pod name: `espocrm_pod`.
- Containers: `mariadb_ctr` (DB), `espocrm_ctr` (web), `espocrm_daemon_ctr` (scheduler/cron). **No WebSocket container** (no live updates).
- Images (pinned, no auto-updates):
- EspoCRM: `docker.io/espocrm/espocrm:9.3.8` (Apache variant, amd64).
- MariaDB: `docker.io/library/mariadb:11.4.12` (LTS, pinned patch version; separate from the EspoCRM image).
- Ports (loopback only):
- `127.0.0.1:8093` → EspoCRM web (container port 80).
- `127.0.0.1:8094` → MariaDB (container port 3306), for host-side DB access.
- MariaDB charset: utf8mb4, collation `utf8mb4_unicode_ci` (from the start, to avoid a later collation migration).
- `siteUrl`: `http://127.0.0.1:8093`.
- Admin user `admin`. DB name/user `espocrm`/`espocrm`. Passwords in plaintext in the scripts (private repo). Generate strong random passwords (e.g. `openssl rand -base64 24`) and set them as fixed variable values.
## Data storage (bind mounts)
```
BIND_DIR="$HOME/.local/share/espocrm_pod"
DB_DATA_DIR="$BIND_DIR/mariadb" # -> /var/lib/mysql
ESPO_DATA_DIR="$BIND_DIR/espocrm" # -> /var/www/html (app, data/upload, custom, config.php)
```
Mount the bind mounts plain, **without `:U` and without `:Z`**. Both official images run their entrypoint as root and chown their own data directories to the in-container service user before dropping privileges: MariaDB chowns `/var/lib/mysql` to `mysql`, EspoCRM chowns its writable resources (`data`, `custom`, `client/custom`, `install/config.php`) to `www-data`. In rootless Podman the container root maps to host user `mkt` and holds `CAP_CHOWN` inside the user namespace, so those chowns succeed on the bind-mounted host directories. No SELinux on Debian, so no `:Z`. This matches the other rootless pods on this host.
`espocrm_ctr` and `espocrm_daemon_ctr` share the same `ESPO_DATA_DIR` (both mount `/var/www/html`).
## Container configuration
### mariadb_ctr (start first)
Env variables:
```
MARIADB_ROOT_PASSWORD=<random>
MARIADB_DATABASE=espocrm
MARIADB_USER=espocrm
MARIADB_PASSWORD=<random>
```
Image arguments (utf8mb4) after the image name:
```
--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
```
### espocrm_ctr (web, start after the DB)
Env variables:
```
ESPOCRM_DATABASE_PLATFORM=Mysql
ESPOCRM_DATABASE_HOST=127.0.0.1
ESPOCRM_DATABASE_PORT=3306
ESPOCRM_DATABASE_NAME=espocrm
ESPOCRM_DATABASE_USER=espocrm
ESPOCRM_DATABASE_PASSWORD=<random, identical to MARIADB_PASSWORD>
ESPOCRM_ADMIN_USERNAME=admin
ESPOCRM_ADMIN_PASSWORD=<random>
ESPOCRM_SITE_URL=http://127.0.0.1:8093
ESPOCRM_CONFIG_USE_WEB_SOCKET=false
```
Note: containers in a pod share the network namespace, so the DB is reachable at `127.0.0.1:3306` (not via the host port 8094). Use `127.0.0.1` instead of `localhost` to force TCP rather than a socket.
### espocrm_daemon_ctr (scheduler, start last)
- Same image `docker.io/espocrm/espocrm:9.3.8`.
- Start with `--entrypoint docker-daemon.sh` (cron/scheduler loop).
- Mounts the same `ESPO_DATA_DIR` at `/var/www/html`.
- Reads its configuration from the `data/config.php` written by `espocrm_ctr` during install, so start it after the web container.
## Script 1: create_pod_espocrm.sh (create plus autostart)
Flow:
1. Env-var block (see above) including the pinned images, ports, `NET_OPTS`, `BIND_DIR`, `USER_SYSTEMD_DIR="$HOME/.config/systemd/user"`, and the plaintext passwords.
2. Create directories: `mkdir -p "$DB_DATA_DIR" "$ESPO_DATA_DIR" "$USER_SYSTEMD_DIR"`.
3. Create the pod (guarded with `if ! podman pod exists`):
```
podman pod create -n "$POD_NAME" --network "$NET_OPTS" \
-p 127.0.0.1:8093:80 \
-p 127.0.0.1:8094:3306
```
4. Start `mariadb_ctr` (`podman rm -f` first for idempotency), with env, `-v "$DB_DATA_DIR":/var/lib/mysql`, then the utf8mb4 image arguments.
5. Wait for the DB to be ready:
```
for i in $(seq 1 60); do
podman exec "$DB_CTR" mariadb-admin ping --silent >/dev/null 2>&1 && break
sleep 2
done
```
6. Start `espocrm_ctr` (`podman rm -f` first), with env, `-v "$ESPO_DATA_DIR":/var/www/html`.
7. Wait for EspoCRM install/response (HTTP 200/302 on `http://127.0.0.1:8093/`, max ~3 min).
8. Start `espocrm_daemon_ctr` (`podman rm -f` first), with `--entrypoint docker-daemon.sh`, `-v "$ESPO_DATA_DIR":/var/www/html`.
9. Generate systemd units: `cd "$USER_SYSTEMD_DIR"; podman generate systemd --files --new --name "$POD_NAME"` (produces `pod-espocrm_pod.service` plus `container-mariadb_ctr.service`, `container-espocrm_ctr.service`, `container-espocrm_daemon_ctr.service`).
10. **Enforce start order** (like the readiness injection in `create_pod_traefik.sh`): insert an `ExecStartPre` wait loop into `container-espocrm_ctr.service` and `container-espocrm_daemon_ctr.service` that waits for the dependency it needs (web waits for DB port `127.0.0.1:8094`; daemon waits for web port `127.0.0.1:8093`). Insert via `awk`, as in the reference script.
11. Stop and remove the live pod: `podman pod stop --time 15 "$POD_NAME"; podman pod rm -f --ignore "$POD_NAME"`.
12. Enable the services:
```
systemctl --user daemon-reload
systemctl --user enable --now pod-${POD_NAME}.service
systemctl --user enable --now container-mariadb_ctr.service
systemctl --user enable --now container-espocrm_ctr.service
systemctl --user enable --now container-espocrm_daemon_ctr.service
```
13. Print closing hints (status and log commands, as in the reference script).
## Script 2: stop_pod_espocrm.sh
- Stops the systemd-managed instance: `systemctl --user stop container-espocrm_daemon_ctr.service container-espocrm_ctr.service container-mariadb_ctr.service pod-${POD_NAME}.service` (in this order), with `echo` status messages.
- Do not disable (autostart stays in place), only stop.
## Script 3: reset_pod_espocrm.sh (dangerous — deletes all data)
- **Extra safety prompt**: only proceed after the exact confirmation phrase is entered, e.g.:
```
read -r -p "WARNING: this deletes the pod, containers, and ALL data under $BIND_DIR. To confirm, type 'RESET espocrm_pod': " C
[ "$C" = "RESET espocrm_pod" ] || { echo "Aborted."; exit 1; }
```
- Then: `systemctl --user disable --now` for the pod and all three containers; `podman pod rm -f --ignore "$POD_NAME"`; delete the generated unit files in `$USER_SYSTEMD_DIR`; `systemctl --user daemon-reload`; finally `rm -rf "$BIND_DIR"` (data gone).
- Print a hint that `create_pod_espocrm.sh` must be run again afterwards.
## Script 4: backup_espocrm.sh (no timer/cron)
- Backup target `BAK_DIR="$HOME/bak"` (`mkdir -p` at the start).
- Timestamp `TS=$(date +%Y%m%d-%H%M%S)`.
- DB dump via `podman exec` (so the backup script needs no password; it uses the container's own env):
```
podman exec "$DB_CTR" sh -c 'exec mariadb-dump --single-transaction --routines --triggers -uroot -p"$MARIADB_ROOT_PASSWORD" espocrm' | gzip > "$BAK_DIR/espocrm-db-$TS.sql.gz"
```
If `$MARIADB_ROOT_PASSWORD` is not available in the exec session, alternatively go host-side via the port: `mariadb-dump -h127.0.0.1 -P8094 -uroot -p<pw> ...` (password as a plaintext variable here too, same repo model). That is what port 8094 is for.
- File backup of the EspoCRM data (contains `config.php` with the crypt/passwordSalt key, `data/upload`, `custom`):
```
tar -czf "$BAK_DIR/espocrm-files-$TS.tar.gz" -C "$ESPO_DATA_DIR" .
```
- Closing message with the created file paths.
- No timer, no cron job (manual invocation).
## Sources for verifying env variable names and entrypoints
- Official image and tags: https://hub.docker.com/r/espocrm/espocrm
- Official Docker build repo (env variables, `docker-daemon.sh`): https://github.com/espocrm/espocrm-docker