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>
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
mktexists; subuid/subgid andenable-lingerare configured. All scripts run as usermkt. - 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-vin thepodmancommand (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 --nowthe 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).
- EspoCRM:
- 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/userespocrm/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_DIRat/var/www/html. - Reads its configuration from the
data/config.phpwritten byespocrm_ctrduring install, so start it after the web container.
Script 1: create_pod_espocrm.sh (create plus autostart)
Flow:
- 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. - Create directories:
mkdir -p "$DB_DATA_DIR" "$ESPO_DATA_DIR" "$USER_SYSTEMD_DIR". - 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 - Start
mariadb_ctr(podman rm -ffirst for idempotency), with env,-v "$DB_DATA_DIR":/var/lib/mysql, then the utf8mb4 image arguments. - 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 - Start
espocrm_ctr(podman rm -ffirst), with env,-v "$ESPO_DATA_DIR":/var/www/html. - Wait for EspoCRM install/response (HTTP 200/302 on
http://127.0.0.1:8093/, max ~3 min). - Start
espocrm_daemon_ctr(podman rm -ffirst), with--entrypoint docker-daemon.sh,-v "$ESPO_DATA_DIR":/var/www/html. - Generate systemd units:
cd "$USER_SYSTEMD_DIR"; podman generate systemd --files --new --name "$POD_NAME"(producespod-espocrm_pod.servicepluscontainer-mariadb_ctr.service,container-espocrm_ctr.service,container-espocrm_daemon_ctr.service). - Enforce start order (like the readiness injection in
create_pod_traefik.sh): insert anExecStartPrewait loop intocontainer-espocrm_ctr.serviceandcontainer-espocrm_daemon_ctr.servicethat waits for the dependency it needs (web waits for DB port127.0.0.1:8094; daemon waits for web port127.0.0.1:8093). Insert viaawk, as in the reference script. - Stop and remove the live pod:
podman pod stop --time 15 "$POD_NAME"; podman pod rm -f --ignore "$POD_NAME". - 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 - 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), withechostatus 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 --nowfor 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; finallyrm -rf "$BIND_DIR"(data gone). - Print a hint that
create_pod_espocrm.shmust be run again afterwards.
Script 4: backup_espocrm.sh (no timer/cron)
- Backup target
BAK_DIR="$HOME/bak"(mkdir -pat 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):Ifpodman 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"$MARIADB_ROOT_PASSWORDis 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.phpwith 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