#!/usr/bin/env bash set -euo pipefail # Bootstraps the Dockerized dmtools stack (php, nginx, database) on a server. # - Builds and starts the containers # - Installs composer dependencies # - Ensures APP_SECRET is set (generated into .env.local if empty) # - Creates and migrates the database # - Installs + compiles AssetMapper assets # - Prints helpful info on success # # Usage: # ./docker/setup.sh # full setup # ./docker/setup.sh --no-build # skip image rebuild # ./docker/setup.sh --recreate # force-recreate containers # ./docker/setup.sh --down # stop and remove containers # # NGINX_PORT=8086 ./docker/setup.sh # if 8085 is already taken on this host # (or set NGINX_PORT / DB_HOST_PORT once in .env.local and every run picks it up) ROOT_DIR=$(cd "$(dirname "$0")"/.. && pwd) PROJECT=dmtools for var in NGINX_PORT DB_HOST_PORT; do if [ -z "${!var:-}" ] && grep -q "^${var}=" "$ROOT_DIR/.env.local" 2>/dev/null; then export "$var=$(grep "^${var}=" "$ROOT_DIR/.env.local" | tail -1 | cut -d= -f2-)" fi done if docker compose version >/dev/null 2>&1; then DOCKER_COMPOSE="docker compose" elif command -v docker-compose >/dev/null 2>&1; then DOCKER_COMPOSE="docker-compose" else echo "Error: Docker Compose not found." >&2 exit 1 fi dc() { (cd "$ROOT_DIR" && $DOCKER_COMPOSE -p "$PROJECT" -f docker-compose.yml "$@"); } REBUILD=1 RECREATE=0 DOWN_ONLY=0 for arg in "$@"; do case "$arg" in --no-build) REBUILD=0 ;; --recreate) RECREATE=1 ;; --down) DOWN_ONLY=1 ;; *) echo "Unknown option: $arg" >&2; exit 1 ;; esac done command -v docker >/dev/null 2>&1 || { echo "Error: docker is required." >&2; exit 1; } if [ ! -f "$ROOT_DIR/.env.local" ]; then echo "Error: .env.local is missing. Copy .env to .env.local and set real" >&2 echo " APP_SECRET, DB_PASSWORD and DB_ROOT_PASSWORD first." >&2 exit 1 fi if [ "$DOWN_ONLY" -eq 1 ]; then dc down exit 0 fi # docker-compose v1 can choke recreating a container in place on any config # change; removing them first sidesteps that. Safe: state lives in named volumes. dc rm -fs php nginx database 2>/dev/null || true BUILD_ARGS=() [ "$REBUILD" -eq 1 ] && BUILD_ARGS+=("--build") [ "$RECREATE" -eq 1 ] && BUILD_ARGS+=("--force-recreate") dc up -d "${BUILD_ARGS[@]}" # Setup one-offs run as root: the bind-mounted project dir is owned by the # deploying user, not the image's www-data, so www-data can't write vendor/ etc. pexec() { dc exec -T -u root php "$@"; } printf "Waiting for database to be healthy..." for i in {1..60}; do id=$(dc ps -q database 2>/dev/null || true) status=$([ -n "$id" ] && docker inspect -f '{{.State.Health.Status}}' "$id" 2>/dev/null || echo "") if [ "$status" = "healthy" ]; then echo " OK"; break; fi printf "."; sleep 2 [ "$i" -eq 60 ] && echo -e "\nWarning: database not healthy yet, continuing anyway." done pexec composer install --no-interaction # Prod compiles config into a cached container under var/cache/prod/ (persistent # php_var volume) and does NOT auto-detect config changes - clear it every run. echo "Clearing and warming the cache..." pexec php bin/console cache:clear --no-interaction if grep -q '^APP_SECRET=$' "$ROOT_DIR/.env" 2>/dev/null \ && ! grep -q '^APP_SECRET=' "$ROOT_DIR/.env.local" 2>/dev/null; then echo "Generating APP_SECRET in .env.local..." printf 'APP_SECRET=%s\n' "$(openssl rand -hex 16)" >> "$ROOT_DIR/.env.local" dc up -d php # pick up the new env value fi echo "Creating database if it doesn't exist..." pexec php bin/console doctrine:database:create --if-not-exists echo "Running migrations..." pexec php bin/console doctrine:migrations:migrate -n --allow-no-migration echo "Installing and compiling assets..." pexec php bin/console importmap:install pexec php bin/console asset-map:compile # LAST: the root-run commands above leave new files under var/ root-owned; # php-fpm runs as www-data and must be able to write there at runtime. pexec chown -R www-data:www-data var # php.ini sets opcache.validate_timestamps=0, so php-fpm keeps serving the # old compiled code/templates until it restarts. Bounce it so a --no-build # run (git pull + this script) actually picks up the new cache. dc restart php # Make sure Nginx Proxy Manager can reach this stack's nginx by name. # Harmless (and a no-op) if already connected; NPM keeps it across restarts. NPM_CONTAINER="${NPM_CONTAINER:-nginx_app_1}" if docker inspect "$NPM_CONTAINER" >/dev/null 2>&1; then docker network connect "${PROJECT}_default" "$NPM_CONTAINER" 2>/dev/null \ && echo "Connected $NPM_CONTAINER to ${PROJECT}_default." \ || true fi APP_URL="http://localhost:${NGINX_PORT:-8085}" cat <