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

9.4 KiB

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