# 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= MARIADB_DATABASE=espocrm MARIADB_USER=espocrm MARIADB_PASSWORD= ``` 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= ESPOCRM_ADMIN_USERNAME=admin ESPOCRM_ADMIN_PASSWORD= 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 ...` (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