Scaffold Symfony 8 app with Bootstrap/DataTables and Docker stack
- Symfony 8.1 webapp skeleton on PHP 8.5 (AssetMapper/importmap, no Node)
- Form-login gate: User entity, security.yaml, SecurityController,
app:user:create command, initial migration (user + messenger_messages)
- Bootstrap 5 + DataTables layout (base/login/home templates); any
table.datatable is auto-initialised
- trusted_proxies/headers wired for the shared Nginx Proxy Manager
- Docker (servers only): docker-compose.yml (php:8.5-fpm-alpine,
nginx:1.30-alpine, mariadb:12.3; host ports 8085/3310; external
docker_default network) + docker/{Dockerfile,php.ini,nginx,setup.sh,restart.sh}
- .env with CHANGEME placeholders (no secrets), .gitattributes enforces
LF so the deploy scripts stay runnable on Linux
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
server {
|
||||
listen 80;
|
||||
# Only ever reached via the server's shared Nginx Proxy Manager, which
|
||||
# terminates TLS and forwards traffic for this domain here - this container
|
||||
# itself doesn't need to know about HTTPS or other domains.
|
||||
server_name dmtools.fvandenberg.nl;
|
||||
root /var/www/html/public;
|
||||
|
||||
location / {
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ ^/index\.php(/|$) {
|
||||
# A plain "fastcgi_pass php:9000" resolves once at worker start and
|
||||
# caches that IP forever - if the php container is recreated without
|
||||
# also restarting nginx, every request 502s. Routing through a variable
|
||||
# forces re-resolution per the resolver TTL. 127.0.0.11 is Compose's
|
||||
# embedded DNS. Use the globally-unique container name, not the bare
|
||||
# "php" alias: this nginx is also on the shared proxy network where
|
||||
# another project may also have a service called "php".
|
||||
resolver 127.0.0.11 valid=10s;
|
||||
set $php_upstream dmtools-php:9000;
|
||||
fastcgi_pass $php_upstream;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||
|
||||
# fastcgi_params never forwards Host - forward the real one explicitly.
|
||||
fastcgi_param HTTP_HOST $http_host;
|
||||
|
||||
# fastcgi_pass does NOT auto-forward incoming headers. Without these,
|
||||
# Symfony can't tell the original request was HTTPS and redirects to
|
||||
# itself forever. if_not_empty: NPM omits some of these entirely, and an
|
||||
# empty-but-present header can behave differently from an absent one.
|
||||
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto if_not_empty;
|
||||
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for if_not_empty;
|
||||
fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host if_not_empty;
|
||||
fastcgi_param HTTP_X_FORWARDED_PORT $http_x_forwarded_port if_not_empty;
|
||||
|
||||
fastcgi_read_timeout 120;
|
||||
internal;
|
||||
}
|
||||
|
||||
# Everything else under public/ (compiled assets, favicon, robots) is served
|
||||
# straight off disk; only index.php is ever executed.
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
error_log /var/log/nginx/dmtools_error.log;
|
||||
access_log /var/log/nginx/dmtools_access.log;
|
||||
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
FROM php:8.5-fpm-alpine
|
||||
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
git \
|
||||
icu-dev \
|
||||
libzip-dev \
|
||||
zip \
|
||||
unzip \
|
||||
$PHPIZE_DEPS \
|
||||
&& docker-php-ext-configure intl \
|
||||
&& docker-php-ext-install -j"$(nproc)" \
|
||||
intl \
|
||||
pdo_mysql \
|
||||
opcache \
|
||||
zip \
|
||||
&& apk del $PHPIZE_DEPS
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
COPY docker/php/php.ini /usr/local/etc/php/conf.d/app.ini
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
COPY composer.json composer.lock symfony.lock ./
|
||||
RUN composer install --no-interaction --no-scripts --no-autoloader --prefer-dist
|
||||
|
||||
COPY . .
|
||||
RUN composer dump-autoload --optimize --classmap-authoritative
|
||||
|
||||
# var/ is a .dockerignore'd host concern and gets a named volume mounted over it
|
||||
# at runtime; create + own it so php-fpm (www-data) can write cache/logs/sessions.
|
||||
RUN mkdir -p var && chown -R www-data:www-data var
|
||||
|
||||
# /var/www/html is bind-mounted from the host at runtime, owned by whoever
|
||||
# deployed it - keep git from refusing to touch it.
|
||||
RUN git config --system --add safe.directory /var/www/html
|
||||
|
||||
USER www-data
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
CMD ["php-fpm"]
|
||||
@@ -0,0 +1,20 @@
|
||||
; dmtools - PHP-FPM runtime tuning (servers).
|
||||
|
||||
expose_php = Off
|
||||
memory_limit = 256M
|
||||
max_execution_time = 60
|
||||
date.timezone = Europe/Amsterdam
|
||||
|
||||
upload_max_filesize = 20M
|
||||
post_max_size = 21M
|
||||
|
||||
realpath_cache_size = 4096K
|
||||
realpath_cache_ttl = 600
|
||||
|
||||
opcache.enable = 1
|
||||
opcache.enable_cli = 0
|
||||
opcache.memory_consumption = 128
|
||||
opcache.max_accelerated_files = 20000
|
||||
opcache.validate_timestamps = 0
|
||||
opcache.preload_user = www-data
|
||||
opcache.preload = /var/www/html/config/preload.php
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Fully restart the dmtools stack for this checkout (testing or live). Scoped to
|
||||
# the compose project name, so running it from the test checkout never touches
|
||||
# the prod stack.
|
||||
#
|
||||
# ./docker/restart.sh # down + rebuildless bring-up via setup.sh
|
||||
# ./docker/restart.sh --prune-all # also run host-wide docker prune
|
||||
|
||||
DOCKER_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT_DIR=$(cd "$DOCKER_DIR/.." && pwd)
|
||||
PROJECT=dmtools
|
||||
|
||||
PRUNE_ALL=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--prune-all) PRUNE_ALL=1 ;;
|
||||
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Restarting stack: $PROJECT"
|
||||
(cd "$ROOT_DIR" && docker compose -p "$PROJECT" -f docker-compose.yml down --remove-orphans) || true
|
||||
|
||||
docker rm -f dmtools-php dmtools-nginx dmtools-db 2>/dev/null || true
|
||||
for net in $(docker network ls -q --filter "name=^${PROJECT}_" 2>/dev/null); do
|
||||
docker network rm "$net" || true
|
||||
done
|
||||
|
||||
if [ "$PRUNE_ALL" -eq 1 ]; then
|
||||
echo "Host-wide prune..."
|
||||
docker system prune -f || true
|
||||
docker builder prune -af || true
|
||||
else
|
||||
echo "Skipping host-wide prune (pass --prune-all to force it)."
|
||||
fi
|
||||
|
||||
echo "Running setup script..."
|
||||
"$DOCKER_DIR/setup.sh" --no-build
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/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_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_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
|
||||
|
||||
APP_URL="http://localhost:${NGINX_PORT:-8085}"
|
||||
cat <<EOT
|
||||
|
||||
Setup complete!
|
||||
|
||||
Open the app: $APP_URL (real access is via https://dmtools.fvandenberg.nl through NPM)
|
||||
|
||||
Create the first user:
|
||||
$DOCKER_COMPOSE -p $PROJECT exec php php bin/console app:user:create you@example.com --admin
|
||||
|
||||
Common commands (from the project root):
|
||||
$DOCKER_COMPOSE -p $PROJECT logs -f nginx
|
||||
$DOCKER_COMPOSE -p $PROJECT logs -f php
|
||||
$DOCKER_COMPOSE -p $PROJECT exec php bash
|
||||
$DOCKER_COMPOSE -p $PROJECT down
|
||||
|
||||
Re-run this script any time. Use --no-build to skip rebuilding images.
|
||||
EOT
|
||||
Reference in New Issue
Block a user