Add a lightweight schema migration runner, fix missing PHPMailer in prod image
docker-entrypoint-initdb.d only applies db/init.sql, and only on a brand new volume - existing installs never got db/migrations/*.sql applied unless someone ran them by hand. This bit twice this session (qr-oss's own init.sql was missing a migration, and qr-vip's live DB was two migrations behind after a code deploy). scripts/migrate.php now runs automatically on every container start via a new entrypoint wrapper, applying any not-yet-recorded migration - safe to run repeatedly since every migration file already checks information_schema before altering. Also caught in the process: Dockerfile.fpm (the production image) never installed phpmailer/phpmailer, unlike the dev Dockerfile - meaning self-registration's password email has been silently fatal-erroring in production since the nginx+php-fpm switch.
This commit is contained in:
@@ -97,11 +97,20 @@ WORKDIR /var/www/html
|
||||
RUN composer update
|
||||
RUN composer require phpmailer/phpmailer:^6.9
|
||||
COPY ./src ./
|
||||
COPY ./db/migrations ./db/migrations
|
||||
RUN chmod 755 *;
|
||||
|
||||
# Qr code storage lives outside the document root so files can only be reached through
|
||||
# the authenticated qrcode_image.php / qrcode_zip_download.php endpoints.
|
||||
RUN mkdir -p /var/www/qrcode-storage/zip && chmod -R 777 /var/www/qrcode-storage
|
||||
|
||||
# Applies any not-yet-applied db/migrations/*.sql on every container start (see
|
||||
# src/scripts/migrate.php) - docker-entrypoint-initdb.d only runs db/init.sql, and only
|
||||
# on a brand new volume, so without this an existing install's schema silently falls
|
||||
# behind the code on every `git pull` + restart.
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["php", "-S", "0.0.0.0:80"]
|
||||
|
||||
@@ -51,7 +51,9 @@ RUN cp -R ./php-qrcode/src /var/www/html/
|
||||
|
||||
WORKDIR /var/www/html
|
||||
RUN composer update
|
||||
RUN composer require phpmailer/phpmailer:^6.9
|
||||
COPY ./src ./
|
||||
COPY ./db/migrations ./db/migrations
|
||||
RUN chown -R www-data:www-data /var/www/html \
|
||||
&& find /var/www/html -type f -exec chmod 644 {} \; \
|
||||
&& find /var/www/html -type d -exec chmod 755 {} \;
|
||||
@@ -62,5 +64,13 @@ RUN mkdir -p /var/www/qrcode-storage/zip \
|
||||
&& chown -R www-data:www-data /var/www/qrcode-storage \
|
||||
&& chmod -R 775 /var/www/qrcode-storage
|
||||
|
||||
# Applies any not-yet-applied db/migrations/*.sql on every container start (see
|
||||
# src/scripts/migrate.php) - docker-entrypoint-initdb.d only runs db/init.sql, and only
|
||||
# on a brand new volume, so without this an existing install's schema silently falls
|
||||
# behind the code on every `git pull` + restart.
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
EXPOSE 9000
|
||||
CMD ["php-fpm"]
|
||||
|
||||
@@ -43,7 +43,8 @@ project by Giandonato Inverso, built on [AdminLTE](https://adminlte.io/).
|
||||
# What is included
|
||||
|
||||
- PHP 8.4 application source
|
||||
- Database schema + migrations
|
||||
- Database schema + migrations (applied automatically on every container start,
|
||||
so `git pull` + restart is enough to bring an existing install up to date)
|
||||
- Docker Compose files (dev and production)
|
||||
- CSS/JS assets
|
||||
|
||||
|
||||
+3
-5
@@ -16,11 +16,9 @@ services:
|
||||
DATABASE_PREFIX: "${DATABASE_PREFIX:-}"
|
||||
DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}"
|
||||
ALLOW_SELF_REGISTRATION: "${ALLOW_SELF_REGISTRATION:-false}"
|
||||
# This file is also what vm420 runs in production (not docker-compose.prod.yml -
|
||||
# see the "prod" naming is misleading, this is the actually-deployed one). Defaults
|
||||
# here are dev-convenience only (mailhog, no auth) - a real deployment's .env must
|
||||
# override MAIL_HOST/MAIL_SMTP_AUTH/MAIL_USERNAME/MAIL_PASSWORD explicitly, same as
|
||||
# DATABASE_PASSWORD above already requires.
|
||||
# Defaults here are dev-convenience only (mailhog, no auth) - a real deployment's
|
||||
# .env must override MAIL_HOST/MAIL_SMTP_AUTH/MAIL_USERNAME/MAIL_PASSWORD
|
||||
# explicitly, same as DATABASE_PASSWORD above already requires.
|
||||
MAIL_HOST: "${MAIL_HOST:-mailhog}"
|
||||
MAIL_PORT: "${MAIL_PORT:-1025}"
|
||||
MAIL_ENCRYPTION: "${MAIL_ENCRYPTION:-}"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
php /var/www/html/scripts/migrate.php
|
||||
|
||||
exec docker-php-entrypoint "$@"
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
// Lightweight migration runner: applies any db/migrations/*.sql file not yet recorded
|
||||
// in schema_migrations, in filename order. Every migration file is itself idempotent
|
||||
// (checks information_schema before altering), so re-running an already-applied file
|
||||
// is a safe no-op - this script leans on that instead of needing transactional rollback.
|
||||
// Run automatically by the container entrypoint on every start (see docker/entrypoint.sh),
|
||||
// so a `git pull` + restart is enough to bring an existing install's schema up to date -
|
||||
// docker-entrypoint-initdb.d only ever runs db/init.sql, and only on a brand new volume.
|
||||
|
||||
require_once __DIR__ . '/../config/environment.php';
|
||||
|
||||
mysqli_report(MYSQLI_REPORT_OFF);
|
||||
|
||||
$mysqli = new mysqli(DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD, DATABASE_NAME, DATABASE_PORT);
|
||||
if ($mysqli->connect_errno) {
|
||||
fwrite(STDERR, "migrate.php: could not connect to database: {$mysqli->connect_error}\n");
|
||||
exit(1);
|
||||
}
|
||||
$mysqli->set_charset(DATABASE_CHARSET);
|
||||
|
||||
$mysqli->query(
|
||||
'CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
filename VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
applied_at DATETIME NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
|
||||
);
|
||||
|
||||
$files = glob(__DIR__ . '/../db/migrations/*.sql');
|
||||
sort($files, SORT_STRING);
|
||||
|
||||
$applied = [];
|
||||
$result = $mysqli->query('SELECT filename FROM schema_migrations');
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$applied[$row['filename']] = true;
|
||||
}
|
||||
|
||||
$ran = 0;
|
||||
foreach ($files as $file) {
|
||||
$filename = basename($file);
|
||||
if (isset($applied[$filename])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "Applying migration: $filename\n";
|
||||
|
||||
if (!$mysqli->multi_query(file_get_contents($file))) {
|
||||
fwrite(STDERR, "migrate.php: failed to apply $filename: {$mysqli->error}\n");
|
||||
exit(1);
|
||||
}
|
||||
// multi_query queues result sets asynchronously - drain them all before the next
|
||||
// query, and check for a mid-batch error on each one.
|
||||
do {
|
||||
if ($res = $mysqli->store_result()) {
|
||||
$res->free();
|
||||
}
|
||||
if ($mysqli->errno) {
|
||||
fwrite(STDERR, "migrate.php: error while applying $filename: {$mysqli->error}\n");
|
||||
exit(1);
|
||||
}
|
||||
} while ($mysqli->more_results() && $mysqli->next_result());
|
||||
|
||||
$stmt = $mysqli->prepare('INSERT INTO schema_migrations (filename, applied_at) VALUES (?, NOW())');
|
||||
$stmt->bind_param('s', $filename);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
$ran++;
|
||||
}
|
||||
|
||||
echo $ran === 0 ? "No pending migrations.\n" : "Applied $ran migration(s).\n";
|
||||
|
||||
$mysqli->close();
|
||||
Reference in New Issue
Block a user