#!/bin/bash # To be run by user mkt to create the EspoCRM pod (MariaDB + web + scheduler # daemon) as a rootless Podman pod and to create/enable the systemd --user # services for autostart. Podman 4.3.1 (no Quadlet): uses # `podman generate systemd --files --new`. # Environment variables POD_NAME='espocrm_pod' DB_CTR='mariadb_ctr' WEB_CTR='espocrm_ctr' DAEMON_CTR='espocrm_daemon_ctr' ESPO_IMAGE='docker.io/espocrm/espocrm:9.3.8' DB_IMAGE='docker.io/library/mariadb:11.4.12' HOST_LOCAL_IP='127.0.0.1' WEB_HOST_PORT='8093' # -> container port 80 (EspoCRM web) DB_HOST_PORT='8094' # -> container port 3306 (host-side DB access) NET_OPTS='slirp4netns:allow_host_loopback=true,port_handler=slirp4netns' # Data (bind mounts, no named volumes) BIND_DIR="$HOME/.local/share/$POD_NAME" DB_DATA_DIR="$BIND_DIR/mariadb" # -> /var/lib/mysql ESPO_DATA_DIR="$BIND_DIR/espocrm" # -> /var/www/html # GUI customizations snapshot, version-controlled in this repo next to the script. # Deployed into /var/www/html on every provisioning run so a reset+reprovision # restores all customizations (CTag entity, layouts, i18n, clientDefs, custom # views, custom CSS, ...). Refresh it after GUI changes with snapshot_espocrm_custom.sh. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CUSTOM_SRC="$SCRIPT_DIR/espocrm-custom" # contains custom/ and client/custom/ # systemd --user paths USER_SYSTEMD_DIR="$HOME/.config/systemd/user" WEB_UNIT_FILE="$USER_SYSTEMD_DIR/container-${WEB_CTR}.service" DAEMON_UNIT_FILE="$USER_SYSTEMD_DIR/container-${DAEMON_CTR}.service" # Credentials (plaintext deliberately accepted: private repo / company secret) MARIADB_ROOT_PASSWORD='KqHZUEih4FQY9bTX1VqjS2VbrScrYyi+' ESPOCRM_DB_PASSWORD='hsILtz+BLs/+8T8MEjO9tIt4fDPPRgB7' ESPOCRM_ADMIN_PASSWORD='Mb5/toETVFzdn+kX6iRxllzXNbcIQIUT' DB_NAME='espocrm' DB_USER='espocrm' ADMIN_USER='admin' SITE_URL="http://${HOST_LOCAL_IP}:${WEB_HOST_PORT}" # Prepare directories mkdir -p "$DB_DATA_DIR" "$ESPO_DATA_DIR" "$USER_SYSTEMD_DIR" # Create pod if not yet existing if ! podman pod exists "$POD_NAME"; then podman pod create \ -n "$POD_NAME" \ --network "$NET_OPTS" \ -p ${HOST_LOCAL_IP}:${WEB_HOST_PORT}:80 \ -p ${HOST_LOCAL_IP}:${DB_HOST_PORT}:3306 echo "Pod '$POD_NAME' created (rc=$?)" else echo "Pod '$POD_NAME' already exists." fi # MariaDB container (start first) podman rm -f "$DB_CTR" podman run -d --name "$DB_CTR" --pod "$POD_NAME" \ -e MARIADB_ROOT_PASSWORD="$MARIADB_ROOT_PASSWORD" \ -e MARIADB_DATABASE="$DB_NAME" \ -e MARIADB_USER="$DB_USER" \ -e MARIADB_PASSWORD="$ESPOCRM_DB_PASSWORD" \ -v "$DB_DATA_DIR":/var/lib/mysql \ "$DB_IMAGE" \ --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci echo "Started $DB_CTR container (rc=$?)" # Wait for the DB to be ready echo "Waiting for MariaDB 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 echo "MariaDB ping ready (rc=$?)" # EspoCRM web container (start after the DB) podman rm -f "$WEB_CTR" podman run -d --name "$WEB_CTR" --pod "$POD_NAME" \ -e ESPOCRM_DATABASE_PLATFORM=Mysql \ -e ESPOCRM_DATABASE_HOST="$HOST_LOCAL_IP" \ -e ESPOCRM_DATABASE_PORT=3306 \ -e ESPOCRM_DATABASE_NAME="$DB_NAME" \ -e ESPOCRM_DATABASE_USER="$DB_USER" \ -e ESPOCRM_DATABASE_PASSWORD="$ESPOCRM_DB_PASSWORD" \ -e ESPOCRM_ADMIN_USERNAME="$ADMIN_USER" \ -e ESPOCRM_ADMIN_PASSWORD="$ESPOCRM_ADMIN_PASSWORD" \ -e ESPOCRM_SITE_URL="$SITE_URL" \ -e ESPOCRM_CONFIG_USE_WEB_SOCKET=false \ -v "$ESPO_DATA_DIR":/var/www/html \ "$ESPO_IMAGE" echo "Started $WEB_CTR container (rc=$?)" # Wait for EspoCRM install/response (HTTP 200/302, max ~3 min) echo "Waiting for EspoCRM to install and respond..." for i in $(seq 1 90); do code=$(curl -s -o /dev/null -w '%{http_code}' "${SITE_URL}/" 2>/dev/null) case "$code" in 200|302) break ;; esac sleep 2 done echo "EspoCRM responded with HTTP $code (rc=$?)" # --- Deploy all GUI customizations from the repo snapshot --------------------- # Copies the version-controlled custom/ and client/custom/ trees into the pod so # a reset+reprovision restores everything: the CTag entity (controller, scopes, # recordDefs, i18n), layouts, clientDefs (Tags +button removed, row-actions = # Unlink only), the signature image-button view, and the table-var-col CSS. # Extract as container-root (--no-same-owner), then chown to the webserver user. # The rebuild step below refreshes the cached metadata (entityDefs, cssList, ...). if [ -d "$CUSTOM_SRC" ]; then tar -C "$CUSTOM_SRC" -cf - custom client/custom \ | podman exec -i "$WEB_CTR" tar -C /var/www/html -xf - --no-same-owner echo "Deployed GUI customizations from $CUSTOM_SRC (rc=$?)" podman exec "$WEB_CTR" chown -R www-data:www-data /var/www/html/custom /var/www/html/client/custom echo "Fixed ownership of customizations (rc=$?)" else echo "WARNING: customization snapshot not found at $CUSTOM_SRC; skipping deploy." fi # Rebuild EspoCRM cache so customizations on the persisted bind-mount take effect # and the client cacheTimestamp is bumped (forces browsers to reload custom views, # e.g. custom field views like the signature image button). The image entrypoint # does NOT rebuild on a normal same-version restart, so without this a recreate # keeps serving a stale client cache. Done here in the live phase where the DB is # already confirmed up (the systemd-managed phase has a start-up race on the DB). # The bumped cacheTimestamp and rebuilt cache persist on the bind-mount into the # systemd-managed instance. podman exec -u www-data "$WEB_CTR" php command.php rebuild echo "Rebuilt EspoCRM cache (rc=$?)" # EspoCRM scheduler/daemon container (start last; reads data/config.php) podman rm -f "$DAEMON_CTR" podman run -d --name "$DAEMON_CTR" --pod "$POD_NAME" \ --entrypoint docker-daemon.sh \ -v "$ESPO_DATA_DIR":/var/www/html \ "$ESPO_IMAGE" echo "Started $DAEMON_CTR container (rc=$?)" # Generate systemd service files cd "$USER_SYSTEMD_DIR" podman generate systemd --files --new --name "$POD_NAME" echo "Generated systemd service files (rc=$?)" # Enforce start order: web waits for DB port, daemon waits for web port. # Insert an ExecStartPre wait loop immediately BEFORE the container run line. awk -v ip="$HOST_LOCAL_IP" -v port="$DB_HOST_PORT" ' BEGIN { inserted=0 } /^ExecStart=\/usr\/bin\/podman run/ && inserted==0 { print "ExecStartPre=/bin/bash -ceu \047for i in $(seq 1 60); do (exec 3<>/dev/tcp/" ip "/" port ") 2>/dev/null && exit 0; sleep 2; done; echo \"dep " ip ":" port " not up\" >&2; exit 1\047" inserted=1 } { print } ' "$WEB_UNIT_FILE" > "${WEB_UNIT_FILE}.tmp" mv "${WEB_UNIT_FILE}.tmp" "$WEB_UNIT_FILE" echo "Injected DB readiness check (${HOST_LOCAL_IP}:${DB_HOST_PORT}) into ${WEB_CTR} unit" awk -v ip="$HOST_LOCAL_IP" -v port="$WEB_HOST_PORT" ' BEGIN { inserted=0 } /^ExecStart=\/usr\/bin\/podman run/ && inserted==0 { print "ExecStartPre=/bin/bash -ceu \047for i in $(seq 1 90); do (exec 3<>/dev/tcp/" ip "/" port ") 2>/dev/null && exit 0; sleep 2; done; echo \"dep " ip ":" port " not up\" >&2; exit 1\047" inserted=1 } { print } ' "$DAEMON_UNIT_FILE" > "${DAEMON_UNIT_FILE}.tmp" mv "${DAEMON_UNIT_FILE}.tmp" "$DAEMON_UNIT_FILE" echo "Injected web readiness check (${HOST_LOCAL_IP}:${WEB_HOST_PORT}) into ${DAEMON_CTR} unit" # Stop & remove the live pod and containers podman pod stop --time 15 "$POD_NAME" podman pod rm -f --ignore "$POD_NAME" if podman pod exists "$POD_NAME"; then echo "ERROR: Pod $POD_NAME still exists." exit 1 else echo "Stopped & removed live pod $POD_NAME and containers" fi # Enable systemd services (the systemd-managed instance is what runs in the end) systemctl --user daemon-reload systemctl --user enable --now "pod-${POD_NAME}.service" echo "Enabled systemd service pod-${POD_NAME}.service (rc=$?)" systemctl --user enable --now "container-${DB_CTR}.service" echo "Enabled systemd service container-${DB_CTR}.service (rc=$?)" systemctl --user enable --now "container-${WEB_CTR}.service" echo "Enabled systemd service container-${WEB_CTR}.service (rc=$?)" systemctl --user enable --now "container-${DAEMON_CTR}.service" echo "Enabled systemd service container-${DAEMON_CTR}.service (rc=$?)" # Closing hints echo "" echo "EspoCRM is reachable at ${SITE_URL} (admin / see ESPOCRM_ADMIN_PASSWORD)" echo "Status: systemctl --user status pod-${POD_NAME}.service" echo "Status: systemctl --user status container-${DB_CTR}.service container-${WEB_CTR}.service container-${DAEMON_CTR}.service" echo "View logs: journalctl --user -u container-${WEB_CTR}.service -f"