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 update
|
||||||
RUN composer require phpmailer/phpmailer:^6.9
|
RUN composer require phpmailer/phpmailer:^6.9
|
||||||
COPY ./src ./
|
COPY ./src ./
|
||||||
|
COPY ./db/migrations ./db/migrations
|
||||||
RUN chmod 755 *;
|
RUN chmod 755 *;
|
||||||
|
|
||||||
# Qr code storage lives outside the document root so files can only be reached through
|
# 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.
|
# 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
|
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
|
EXPOSE 80
|
||||||
CMD ["php", "-S", "0.0.0.0: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
|
WORKDIR /var/www/html
|
||||||
RUN composer update
|
RUN composer update
|
||||||
|
RUN composer require phpmailer/phpmailer:^6.9
|
||||||
COPY ./src ./
|
COPY ./src ./
|
||||||
|
COPY ./db/migrations ./db/migrations
|
||||||
RUN chown -R www-data:www-data /var/www/html \
|
RUN chown -R www-data:www-data /var/www/html \
|
||||||
&& find /var/www/html -type f -exec chmod 644 {} \; \
|
&& find /var/www/html -type f -exec chmod 644 {} \; \
|
||||||
&& find /var/www/html -type d -exec chmod 755 {} \;
|
&& 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 \
|
&& chown -R www-data:www-data /var/www/qrcode-storage \
|
||||||
&& chmod -R 775 /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
|
EXPOSE 9000
|
||||||
CMD ["php-fpm"]
|
CMD ["php-fpm"]
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ project by Giandonato Inverso, built on [AdminLTE](https://adminlte.io/).
|
|||||||
# What is included
|
# What is included
|
||||||
|
|
||||||
- PHP 8.4 application source
|
- 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)
|
- Docker Compose files (dev and production)
|
||||||
- CSS/JS assets
|
- CSS/JS assets
|
||||||
|
|
||||||
|
|||||||
+3
-5
@@ -16,11 +16,9 @@ services:
|
|||||||
DATABASE_PREFIX: "${DATABASE_PREFIX:-}"
|
DATABASE_PREFIX: "${DATABASE_PREFIX:-}"
|
||||||
DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}"
|
DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}"
|
||||||
ALLOW_SELF_REGISTRATION: "${ALLOW_SELF_REGISTRATION:-false}"
|
ALLOW_SELF_REGISTRATION: "${ALLOW_SELF_REGISTRATION:-false}"
|
||||||
# This file is also what vm420 runs in production (not docker-compose.prod.yml -
|
# Defaults here are dev-convenience only (mailhog, no auth) - a real deployment's
|
||||||
# see the "prod" naming is misleading, this is the actually-deployed one). Defaults
|
# .env must override MAIL_HOST/MAIL_SMTP_AUTH/MAIL_USERNAME/MAIL_PASSWORD
|
||||||
# here are dev-convenience only (mailhog, no auth) - a real deployment's .env must
|
# explicitly, same as DATABASE_PASSWORD above already requires.
|
||||||
# override MAIL_HOST/MAIL_SMTP_AUTH/MAIL_USERNAME/MAIL_PASSWORD explicitly, same as
|
|
||||||
# DATABASE_PASSWORD above already requires.
|
|
||||||
MAIL_HOST: "${MAIL_HOST:-mailhog}"
|
MAIL_HOST: "${MAIL_HOST:-mailhog}"
|
||||||
MAIL_PORT: "${MAIL_PORT:-1025}"
|
MAIL_PORT: "${MAIL_PORT:-1025}"
|
||||||
MAIL_ENCRYPTION: "${MAIL_ENCRYPTION:-}"
|
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