docker-library

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars57
mentions170
03774111148Oct 4Oct 18Nov 2
UTC days · repositories docker-library owns2 repositories

Package downloads

Cumulative recorded downloads

crates.io—
01234Oct 4Oct 18Nov 2No observations in this range
Oct 4crates.io: 0/1 packages observed

Across 1 known packages in repositories docker-library owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

docker-library’s repositories ranked by pull request authors, as on trending.

Loading

Contributes to

Repositories outside docker-library’s account where docker-library opened or reviewed pull requests in the past 365 days.

Who noticed docker-library

Everyone who starred or forked docker-library’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

171posts
Nov 31 post
  1. PostgreSQL 14 End of Life on 12 November 2026: How to Upgrade Postgres in Docker PostgreSQL 14 gets its last update on 12 November 2026. The PostgreSQL versioning policy lists that date as the final release for version 14, and the rule behind it is short: each major version is supported for five years, then it gets one final minor release and becomes unsupported. Any security bug found in 14 after that day stays unfixed. The calendar around it is tight. The PostgreSQL 19 RC 1 announcement of 15 October says the 12 November update release "will include a large number of fixes", and it puts general availability of PostgreSQL 19 on 19 November 2026, one week after 14 drops out of support. If you are still on 14, you have to choose a target now, before 19 has any track record. Managed databases get a little more room. The Amazon RDS for PostgreSQL release calendar keeps 14 in standard support until 28 February 2027, with paid Extended Support starting on 1 March 2027. A self-hosted container gets no grace period at all. The postgres:14 image you pulled in 2022 keeps running exactly the code it shipped with. Why so many stacks are still on 14 Postgres is the line in a compose file people are most nervous about touching, and with reason. Changing the image tag across a major version does not upgrade the data, because the on-disk format changes between majors. The versioning page says it plainly: a dump/reload or pg_upgrade is required. So a stack set up in 2021 or 2022 tends to get its app images rebuilt every month while the database tag sits still. The official Docker image moves on regardless. The versions.json file in the docker-library/postgres repository, which lists the versions the image maintainers build, currently covers 14 through 19. PostgreSQL 13, which reached end of life in November 2025, is no longer in it. Step 1: find every Postgres 14 you run Image names are not reliable. A container may run PostGIS, TimescaleDB, a vendor image or something built in-house, all with Postgres inside. Ask the binary instead: {% raw %} for c in $(docker ps --format '{{.Names}}'); do v=$(docker exec "$c" postgres --version 2>/dev/null) && echo "$c: $v" done {% endraw %} That only sees running containers, so also search compose files on disk for stacks that are stopped or live on other hosts: grep -rnE 'image: *"?postgres' --include='*compose*.y*ml' /srv /opt 2>/dev/null For every 14 instance, write down three facts before planning anything. Each one decides a step later on (examples below use a container called pg14): {% raw %} # data checksums: "version: 0" means off docker exec -u postgres pg14 pg_controldata | grep -i checksum # bootstrap superuser: empty output means "postgres" docker exec pg14 printenv POSTGRES_USER # where the data volume is mounted docker inspect -f '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' pg14 {% endraw %} Step 2: pick the target version Our recommendation is PostgreSQL 18, not 19. Version 18 is the current stable line (18.6 on the versioning page today) and is supported until 14 November 2030. 19.0 will be a week old when 14 expires, and a migration forced by an end-of-life date is a poor moment to also be an early adopter. Move to 18 now and plan 19 for next year, once a few minor releases are out. You do not need to step through 15, 16 and 17. The pg_upgrade documentation says it supports upgrades "from 9.2.X and later", and the versioning page confirms you can skip versions, while recommending that you read the release notes of every version in between. For 14 to 18 that means the notes for 15, 16, 17 and 18, starting with the incompatibilities list at the top of each. Step 3: take a backup you have restored once docker exec pg14 pg_dumpall -U app | gzip > pg14-$(date +%F).sql.gz Restore it into a scratch container and run a few queries against it. Until you have done that, it is a file, not a backup. Option A: dump and restore For databases up to a few gigabytes this is the simplest path, and it clears out table bloat as a side effect. Start a postgres:18 container next to the old one with its own empty volume, then stream the data across: docker exec pg14 pg_dumpall -U app | docker exec -i pg18 psql -U app -q When we ran this, psql printed two errors: ERROR: role "app" already exists ERROR: database "app" already exists Both are harmless. The new container created that role and database on first start, and the rest of the dump loads into them. The cost of this method is downtime: writes have to stop for the whole dump and restore, so it grows with the size of the data. Option B: pg_upgrade with hard links For larger databases, pg_upgrade --link recreates the schema in the new cluster and hard-links the data files instead of copying them. The pg_upgrade documentation says link mode is "much faster (no file copying)" and uses less disk space. It needs the binaries of both versions in one place, which the official image does not provide. The tianon/docker-postgres-upgrade project publishes images tagged OLD-to-NEW, including 14-to-18. Its README calls it a proof of concept to "learn from" and adapt, not a finished tool, and that matches what we saw. We upgraded a throwaway 14.24 cluster created with POSTGRES_USER=app, which is how most compose files set it up, to 18.6. It failed three times before it worked, and none of the three is specific to our setup. Error 1: data checksums old cluster does not use data checksums but the new one does Failure, exiting The PostgreSQL 18 release notes list this under incompatibilities: initdb now enables data checksums by default, and "pg_upgrade requires matching cluster checksum settings". A cluster created by the postgres:14 image has checksums off unless you asked for them. The fix is to create the new cluster with --no-data-checksums. If you want checksums, turn them on afterwards with pg_checksums on the stopped cluster. Its documentation warns that on a large cluster this "can potentially take a long time", and nothing else may write to the data directory while it runs. Error 2: the superuser name FATAL: role "postgres" does not exist could not connect to source postmaster started with the command: ... The helper image runs initdb as the default postgres superuser and connects to the old cluster with that name. If your compose file set POSTGRES_USER=app, the old cluster has no role called postgres. Pass the real name to both initdb and pg_upgrade. With both fixes in place, run the check mode first. It changes no data: # old cluster stopped, its files moved to /srv/pg/14/docker docker run --rm \ --mount type=bind,src=/srv/pg,dst=/var/lib/postgresql \ -e PGDATAOLD=/var/lib/postgresql/14/docker \ -e PGDATANEW=/var/lib/postgresql/18/docker \ -e POSTGRES_INITDB_ARGS='--no-data-checksums --username=app' \ tianon/postgres-upgrade:14-to-18 --check --username=app When it prints *Clusters are compatible*, run the same command with --link --username=app in place of --check --username=app. Error 3: the app can no longer connect The upgrade finished and postgres:18 started on the new data. Then a client in another container got this: FATAL: no pg_hba.conf entry for host "172.26.0.3", user "app", database "app", no encryption The official image adds a line to pg_hba.conf that allows password logins from other hosts, but only when it initializes an empty data directory. Here initdb ran inside the helper image, so the new pg_hba.conf only trusts local connections. The pg_upgrade documentation warns about this in its "Restore pg_hba.conf" step and adds that postgresql.conf and postgresql.auto.conf may need the same treatment. Copy your old files over if you changed them. If you only had the image defaults, add the line back: docker exec pg18 sh -c 'echo "host all all all scram-sha-256" >> "$PGDATA/pg_hba.conf"' docker exec pg18 psql -U app -c 'SELECT pg_reload_conf()' The volume path changed in the 18 image Starting with 18, the official image keeps data in a version-specific directory, /var/lib/postgresql/18/docker, and declares its volume at /var/lib/postgresql. The image documentation says mounts should target the new location, and pull request #1259 explains why: with one mount for both versions, pg_upgrade --link works without crossing filesystems. Update the service definition to match: db: image: postgres:18 volumes: - /srv/pg:/var/lib/postgresql # not /var/lib/postgresql/data If you just change the tag and leave the old mount, the 18 entrypoint script refuses to start and prints: Error: in 18+, these Docker images are configured to store database data in a format which is compatible with "pg_ctlcluster" ... That is a safe failure. It is still better to see it in a test run than at 2 a.m. After the upgrade PostgreSQL 18's pg_upgrade keeps optimizer statistics, according to its release notes, but our run still ended with "Some statistics are not transferred by pg_upgrade" and suggested two vacuumdb commands. Run them, then look for passwords stored as MD5: docker exec pg18 vacuumdb -U app --all --analyze-in-stages --missing-stats-only docker exec pg18 vacuumdb -U app --all --analyze-only docker exec pg18 psql -U app -Atc "SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%'" The PostgreSQL 14 release notes switched the default password hashing to SCRAM-SHA-256, but roles carried over from 13 or older can still hold MD5 hashes. PostgreSQL 18 deprecates MD5 authentication and says support will be removed in a future major release, so reset those passwords while you are already in a maintenance window. Two more things. In link mode the old cluster is unsafe to start once the new one has run, as the pg_upgrade docs warn, so your rollback is the backup from step 3, not the old directory. And keep that old directory until the application has run a full business cycle on 18, then delete it with the delete_old_cluster.sh script pg_upgrade leaves behind. The same habit that keeps Postgres current applies to every base image you run. We covered it for PHP in PHP2Shell: patch the PHP underneath WordPress, and the planning side of a hard end-of-support date in our Exchange 2016/2019 exit plan. If you are cleaning up a compose stack anyway, sending container logs to stdout is another small fix worth doing in the same pass. Frequently asked questions Will my PostgreSQL 14 database stop working on 12 November 2026? No. It keeps running. End of life means the project stops releasing fixes, so security and data corruption bugs found after the final 14 release will not be patched. Can I upgrade from 14 straight to 18? Yes. pg_upgrade supports any source from 9.2 onward, and dump/restore works across any versions. Read the release notes of 15, 16, 17 and 18 for incompatibilities first. Should I wait for PostgreSQL 19? We would not. 19 is planned for 19 November 2026, after 14 loses support. Upgrading to 18 now and to 19 later is two planned jobs instead of one rushed one. Is it enough to change the image tag from 14 to 18? No. The data directory has to be upgraded with pg_upgrade or dump/restore. With an old-style mount, the 18 image refuses to start and points you to pg_upgrade. Sources: PostgreSQL versioning policy, PostgreSQL 19 RC 1 announcement, PostgreSQL 18 release notes, PostgreSQL 14 release notes, pg_upgrade documentation, Amazon RDS for PostgreSQL release calendar, postgres image documentation and tianon/docker-postgres-upgrade. Commands tested on Docker with postgres:14 (14.24) and postgres:18 (18.6) on 1 November 2026. Originally published on fanpino.com.

  2. How to upgrade Postgres in Docker to 18, and be ready for 19: dump and restore or pg_upgrade --link I moved a Postgres database on Dokploy from 16 to 18 with pg_upgrade. It went fine, with the database down for under a minute, but it stopped twice on the way. First, pg_upgrade stopped at its checks: old cluster does not use data checksums but the new one does. Then 18 started, and my app couldn't connect. Neither stop was bad luck. Both follow from how the 18 tooling works, and both wait in a typical pg_upgrade of a 14 to 17 database made with the image's defaults. You'll meet them soon: 14 gets its final release on November 12, 2026, and 19 is planned for October 29 (as of October 2026). This guide is for you if you run Postgres 14 to 18 in a Docker volume, Dokploy and Coolify included. It gives you one timed rehearsal that picks your path: dump and restore, or pg_upgrade --link. Then both paths step by step, the exact errors with their fixes, the rollback for each, and five checks that prove it worked. > Key takeaways > > - The 18 image moved PGDATA to /var/lib/postgresql/18/docker and the volume to /var/lib/postgresql, so pg_upgrade --link can hard-link inside one mount. A volume mounted at PGDATA itself, as Dokploy does for 18, blocks it. > - Rehearse a dump and restore on a copy and time it. If it fits your downtime window, take that path. If not, --link took 12.9 to 14.4 seconds from 1.4 to 8.5 GB in End Point's 2015 test. > - A 17 cluster made with the image's defaults has data checksums off, and 18's initdb turns them on. pg_upgrade refuses the mismatch, and the fix you pick follows you to 19. > - An upgraded cluster doesn't get the image's pg_hba.conf line for other containers, so your app is refused until you copy your old file back. > - Once the new server starts, a restored backup is --link's only way back. > How this guide was checked: I didn't run any of these commands for this post. They're assembled from the PostgreSQL 18 docs, the official image's source (18.6, as of October 2026), and the upgrade image's scripts, and each block says "Untested." Error messages come from that source and from GitHub issues. The examples use a Compose project myapp, a service db, a volume pgdata, and the superuser postgres. Table of contents - [Why the Postgres 18 image moved your data](#why-the-postgres-18-image-moved-your-data) - [Dump and restore or pg_upgrade --link: let one timed rehearsal decide](#dump-and-restore-or-pg_upgrade---link-let-one-timed-rehearsal-decide) - [Before either path: rehearse on a copy and time it](#before-either-path-rehearse-on-a-copy-and-time-it) - [Path A: dump and restore into a fresh 18 volume](#path-a-dump-and-restore-into-a-fresh-18-volume) - [Path B: pg_upgrade --link in four steps](#path-b-pg_upgrade---link-in-four-steps) - [How to check the upgrade worked](#how-to-check-the-upgrade-worked) - [Errors you might see, and what fixes them](#errors-you-might-see-and-what-fixes-them) - [On Dokploy: the mount that blocks an in-place upgrade to 19](#on-dokploy-the-mount-that-blocks-an-in-place-upgrade-to-19) - [Upgrading to 19: what's ready and what isn't](#upgrading-to-19-whats-ready-and-what-isnt) - [When not to use pg_upgrade --link](#when-not-to-use-pg_upgrade---link) - [Check two lines before 19 ships](#check-two-lines-before-19-ships) - [Sources](#sources) Why the Postgres 18 image moved your data Three answers from a developer would worry me before a database upgrade, because each one skips a step that decides whether the data survives. The first is "just change the version." In Docker, current 18 images refuse it outright. The 18 image moved the data down one level so that two clusters can share one mount. That's what pg_upgrade --link needs, because a hard link can't cross a mount point. Here's the mechanism. --link makes one link(2) call per data file instead of copying it. The link(2) manual says the call fails with EXDEV across different mounts, "even if the same filesystem is mounted on both." Docker makes every volume its own mount. On 17 and earlier, the volume is the data directory (/var/lib/postgresql/data). There's no room inside it for a second cluster, so the new one has to live on a second mount, and linking fails. A maintainer said volumes were "stuck using the slow version" (PR #1259). From 18, PGDATA is /var/lib/postgresql/18/docker and the VOLUME is /var/lib/postgresql (image docs). Mount the parent, and 17/docker, 18/docker, and later 19/docker sit side by side in one volume. What the old mount does on 18 depends on when the image was built: - Current Debian images (since October 22, 2025): the entrypoint finds old data, or an unused mount at /var/lib/postgresql/data, prints Error: in 18+, these Docker images are configured to store database data in a format which is compatible with "pg_ctlcluster", and exits 1 without touching your data (entrypoint source, PR #1372). Under restart: always, that's a restart loop. - 18.0-era images: a deliberate data symlink made Docker fail on many setups with error mounting ... to rootfs at "/var/lib/postgresql/data" ... no such file or directory. Alpine images kept the symlink until January 29, 2026. - Alpine images built before the April 21, 2026 fix: their check missed bind mounts at the old path, so some setups started an empty cluster in an anonymous volume and ran that way for weeks. A maintainer predicted that last symptom, "a running empty database," for anyone who starts a new major on old data without upgrading (PR #1259). The refusal in the first bullet exists to stop it. Verify it: check which directory your server writes to. # Untested. docker inspect <container> --format '{{ json .Mounts }}' docker compose exec db psql -U postgres -c 'SHOW data_directory;' You want one mount whose Destination is /var/lib/postgresql, and a data directory inside it. Next question: do you need any of this, or is a dump simpler? Dump and restore or pg_upgrade --link: let one timed rehearsal decide Rehearse a dump and restore on a copy and time it. If it fits your downtime window, use dump and restore. If it doesn't, use pg_upgrade --link. One published rule of thumb puts a number on it, CYBERTEC's from 2019: dump-and-restore trouble starts "around 100GB." Your own timing beats any general rule. The PostgreSQL docs call dump and restore the method that "can be slow" and say pg_upgrade finishes "in minutes, particularly with --link mode." Here's what each path does to your data, per the pg_upgrade docs: | Path | What happens to the data | Extra disk during the upgrade | Needs | Way back | |---|---|---|---|---| | Dump and restore | Every row re-inserted, indexes rebuilt | The dump plus a full new cluster | The new image's client tools | The old volume, untouched | | pg_upgrade --copy (default) | Every file copied | About the cluster's size again | Old and new binaries | The old cluster, untouched | | pg_upgrade --link | Hard links; nothing copied | Almost none | One mount for both clusters | Rename pg_control.old if the new server never started; after that, your backup | | pg_upgrade --clone | Reflinks | Almost none at first | Btrfs, XFS with reflink, or APFS (not ext4) | The old cluster, untouched | | pg_upgrade --swap (new in 18) | Directories moved | Almost none | One filesystem | Your backup, once transfer starts | The only controlled comparison with published numbers is from 2015, but its shape is the point. In Greg Sabino Mullane's test (9.3 to 9.4, pgbench data, fsync off on the target, hardware not stated), dump and restore took 210 seconds at 1.4 GB and 1,927 seconds at 8.5 GB. The whole --link run took 12.9 to 14.4 seconds at every size, and --copy landed in between, from 74.7 to 607 seconds. --link time follows the number of tables and indexes, not bytes. Daniel Westermann measured 13.8 seconds for a --link upgrade of 10,000 tables and 10,000 indexes, from 17.2 to an 18 development build (March 2025). Two more options sit between them. With --copy, you need no layout move and keep the old cluster as your way back, at the cost of the disk and minutes of copying. With --clone, you keep it too, if your filesystem can do reflinks. And know what you're betting with --link: after first start, your rollback is the restore half of that timing, so check it against the window too. Dump and restore buys real things, though. The new cluster gets the 18 layout and checksums by default, every index is rebuilt on the new image's C library, and the old volume stays as your rollback. Windmill, BloodHound, authentik, Discourse, Coolify, and Immich's community guide all document dump and restore for the Postgres their apps run on. My pick: for a production database, I'd use pg_upgrade, because it's faster on big databases. The rehearsal tells you whether yours counts as big. Either way, the rehearsal comes first. Before either path: rehearse on a copy and time it "We have backups" is the second answer that would worry me, when nobody has restored them. Dump your database with the 18 tools, restore it into a scratch volume, and time both halves. The total is your Path A downtime. The restore half is your rollback time for Path B, because once the new server has started, the docs' answer to any failure is "restore from backup." What you need before you start: - postgres:18.6, the newest 18 minor as of October 2026, pinned. - For Path B, tianon/postgres-upgrade (tags 14-to-18 to 17-to-18). - Free disk for a full dump, plus room for a second cluster on Path A. - A scratch volume, and a quiet hour on the calendar for the real run. Take the dump with the new version's tools: the docs recommend pg_dump and pg_dumpall "from the newer version of PostgreSQL." Path A shows the commands; for the rehearsal, point them at a scratch volume and leave the app running. War story: with three weeks of production data on Dokploy, I ran a migration late at night and for about 90 seconds believed I'd wiped the users table. I hadn't. But when I went looking for backups, there were none. I set up Dokploy's scheduled pg_dump to Cloudflare R2, and the week after, I restored one into a throwaway Postgres service on purpose. It took 10 minutes; the database was a couple hundred MB at the time. A backup you've never restored isn't a backup. It's a file you feel good about. GitLab learned the same lesson at scale in its January 2017 outage. Its backup job had been failing on a pg_dump version mismatch, the S3 bucket was empty, and "nobody was responsible for testing this procedure." For Path B, rehearse on a 17 copy, not the 18 restore. Dump with your current image's own tools and restore into a scratch postgres:17 volume created with production's POSTGRES_USER and POSTGRES_INITDB_ARGS, so the install user and checksum setting match. Then move it into the new layout, run --check --link and the real upgrade against it, and time the whole thing. (The docs' own test advice is lighter: "create a schema-only copy of the old cluster, insert dummy data, and upgrade that.") A logical copy won't carry production's recorded collation version unless the scratch 17 runs production's image, and it never carries damage inside the data files. And run --check against a stopped cluster, production or the copy: it reaches the old server through its Unix socket, which lives inside the other container. The backup is usually your biggest disk cost. --link needs almost nothing extra; a full dump of your data does. With a timed rehearsal in hand, you know your path. Path A: dump and restore into a fresh 18 volume Dump with the 18 client, restore into a new volume mounted the 18 way, and keep the old volume until you've checked. The new cluster gets the new layout and data checksums for free. # Untested. 1) Stop the app so nothing writes during the dump. docker compose stop app # 2) Dump everything with the 18 tools, against the running 17 server. # Export PGPASSWORD in your shell first; -e passes it into the container. docker run --rm --network myapp_default -e PGPASSWORD postgres:18.6 \ pg_dumpall -h db -U postgres > all.sql Then point the db service at a new volume and the new image. Keep POSTGRES_PASSWORD: an empty volume won't initialize without it. # Untested. docker-compose.yml services: db: image: postgres:18.6 environment: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - pgdata18:/var/lib/postgresql # new volume, mounted at the parent volumes: pgdata18: # Untested. 3) Start 18 and wait for its first-run setup to finish. docker compose up -d db docker compose logs -f db # wait for "PostgreSQL init process complete; ready for start up." # 4) Restore, keep the errors, refresh statistics, start the app. docker compose exec -T db psql -U postgres -f - < all.sql 2> restore-errors.log docker compose exec db vacuumdb -U postgres --all --analyze-in-stages docker compose start app Wait for that log line: during first init, a temporary server listens on the Unix socket only, and a restore that reaches it gets cut off when the entrypoint stops it. In restore-errors.log, expect role "postgres" already exists (and database ... already exists if you set POSTGRES_DB); anything else needs a look. Rollback is one edit: put postgres:17 and the old volume back in the Compose file. If your rehearsal didn't fit the window, Path B is for you. Path B: pg_upgrade --link in four steps Path B keeps your data where it is and links it into an 18 cluster in the same volume. It takes four steps: move the layout on your current major, check two settings, run the upgrade, then give 18 what it didn't inherit. My own 16 to 18 pg_upgrade hit two of the problems below: the checksum mismatch (step 2) and the pg_hba.conf lockout (step 4). Step 1: Move your volume into the one-mount layout Move the existing cluster into <your major>/docker inside the same volume, set PGDATA, and mount the parent. Do it on your current major, so the layout change and the version change happen on different days. The image docs describe this opt-in: set PGDATA=/var/lib/postgresql/17/docker, mount at /var/lib/postgresql, and first move "all database files into a PG_MAJOR/docker subdirectory." Inside one volume the move is a rename, so it's near-instant at any size. Stop the database so nothing restarts it. docker compose stop keeps it stopped; for a Swarm service, docker service scale <service>=0 stops all replicas while keeping the service. Compose names the volume <project>_pgdata, so check the real name with docker volume ls; a bare pgdata would create a new, empty volume. # Untested. Runs as the postgres user so ownership stays the same. docker run --rm --user postgres --entrypoint sh \ -v myapp_pgdata:/var/lib/postgresql/data postgres:17 -c ' cd /var/lib/postgresql/data && mkdir -p 17/docker && chmod 700 17/docker && for f in *; do [ "$f" = 17 ] || mv "$f" 17/docker/; done' Then change the service: # Untested. services: db: image: postgres:17 environment: PGDATA: /var/lib/postgresql/17/docker volumes: - pgdata:/var/lib/postgresql # was /var/lib/postgresql/data stop_grace_period: 2m # default is 10s Gotcha: both lines matter. Mount the parent on 17 without setting PGDATA, and the image's own VOLUME /var/lib/postgresql/data gives you an anonymous volume that won't survive the container being removed. Don't test it with docker compose up --force-recreate: Compose keeps anonymous volumes across a recreate (docker compose up), so a test row survives anyway. Run SHOW data_directory; instead; it should print /var/lib/postgresql/17/docker. The longer grace period matters because pg_upgrade refuses a cluster that wasn't shut down cleanly, and the image's Dockerfile asks for a stop timeout longer than Docker's 10-second default. The docs don't describe an undo; moving the files back, dropping PGDATA, and mounting /var/lib/postgresql/data again should work, but that's an inference. Step 2: Check data checksums and the superuser This is where my 16 to 18 upgrade stopped first. Two settings decide whether the upgrade runs at all, so check both while 17 is still up. First, checksums: docker compose exec db psql -U postgres -c 'SHOW data_checksums;'. 18's initdb turns them on by default (18.0 release notes), and pg_upgrade refuses a mismatch: - off (the 17 image's default): pg_upgrade stops with old cluster does not use data checksums but the new one does. Create the new cluster with --no-data-checksums, as a docker-library maintainer suggests in tianon issue #121. That's the flag that got mine through. - on (Immich's Compose file sets it, for example): leave the flag out, or you'll get the mirror error, old cluster uses data checksums but the new one does not. The upgrade image's README example never hits this, because it creates its 17 test cluster with checksums on. Real 17 clusters made with the defaults don't have them. Second, the superuser. If your Compose file sets POSTGRES_USER=app, the cluster's install user is app, and the upgrade image's plain initdb creates postgres, so the run fails. The workaround reported in tianon's issues is -e PGUSER=app plus --username=app in POSTGRES_INITDB_ARGS. Step 3: Run pg_upgrade --check --link, then the real run There's no official way to get two Postgres versions into one container. tianon/postgres-upgrade is postgres:18 plus 17's binaries from the PostgreSQL apt repository. Its own README calls it a proof of concept to learn from and adapt. Stop the database again (docker compose stop db) and keep it stopped. Nikolay Samokhvalov of Postgres.ai reported on the pgsql-hackers list a production case where an upgrade's target was started mid-run and corruption surfaced weeks later. The cause isn't proven, but nothing in pg_upgrade prevents it. # Untested. One volume, mounted at the parent, for both clusters. # Keep POSTGRES_INITDB_ARGS only if SHOW data_checksums said off. docker run --rm \ -v myapp_pgdata:/var/lib/postgresql \ -e PGDATAOLD=/var/lib/postgresql/17/docker \ -e PGDATANEW=/var/lib/postgresql/18/docker \ -e POSTGRES_INITDB_ARGS="--no-data-checksums" \ tianon/postgres-upgrade:17-to-18 \ --check --link Set PGDATAOLD yourself: the image defaults it to /var/lib/postgresql/17/data. Arguments that start with - go straight to pg_upgrade. If the check passes, run the same command without --check. Pro tip: always pass the mode flag with --check. --check --link makes a test hard link between the two directories and fails with could not create hard link between old and new data directories: Invalid cross-device link if they're on different mounts. A plain --check doesn't test linking at all. The check writes one thing: the image runs initdb into 18/docker whenever that directory has no PG_VERSION, and skips it on every later run. So after a failed check or run, delete 18/docker (only that one) before you try again. Otherwise a rerun with the checksum fix still meets the 18 cluster the failed attempt created. Two more things the upgrade container doesn't know about your setup: - Extensions with C code (PostGIS, pgvector, pg_cron) must be installed for both majors. pg_upgrade loads every library the old cluster uses into the new one. The PostgreSQL apt repository has postgresql-18-pgvector and postgresql-18-postgis-3, so build your own image on top of tianon's when you need them. - Settings from command: postgres -c ... don't exist in the upgrade container. Pass them with -o '<options>' for the old server and -O '<options>' for the new one; a missing library in shared_preload_libraries stops the old server from starting. Where's the point of no return? The moment pg_upgrade renames the old global/pg_control to pg_control.old. Every check, the schema dump, and the schema restore happen before that, so a failure there leaves the old cluster untouched. Between the rename and the new server's first start, you can remove the .old suffix and start 17 again. After that first start, the docs say: restore from backup. Step 4: Start 18 and give it what it didn't inherit This is the step that locked my app out. Before 18's first start, copy your access rules. Then switch the Compose file to postgres:18.6, drop the PGDATA line (18's default is /var/lib/postgresql/18/docker), start it, and finish four jobs the upgrade leaves to you. 1. Restore client access. The official image adds a host all all all line to pg_hba.conf only when it creates a cluster. An upgraded cluster skips that, so other containers get no pg_hba.conf entry for host. That's what my app got, and blogger Rezha Julio hit it on all four instances he upgraded. The pg_upgrade docs say the new cluster's configuration files may need adjusting to match the old one's (postgresql.conf, postgresql.auto.conf). In Docker that includes pg_hba.conf: # Untested. Before 18's first start. docker run --rm --user postgres --entrypoint sh \ -v myapp_pgdata:/var/lib/postgresql postgres:18.6 -c ' cp /var/lib/postgresql/17/docker/pg_hba.conf /var/lib/postgresql/18/docker/' Your ALTER SYSTEM settings are in 17/docker/postgresql.auto.conf; re-apply the ones you still want. If you copy pg_hba.conf after 18 has started, run SELECT pg_reload_conf();. 2. Refresh statistics the right way. 18 carries most planner statistics across, but not extended statistics or the counters that trigger autovacuum. Run the two commands pg_upgrade prints: # Untested. docker compose exec db vacuumdb -U postgres --all --analyze-in-stages --missing-stats-only docker compose exec db vacuumdb -U postgres --all --analyze-only Keep --missing-stats-only: without it, --analyze-in-stages replaces the statistics 18 just carried over with coarse ones for a while (vacuumdb docs). 3. Update extensions and find MD5 passwords. If an extension needs it, pg_upgrade writes update_extensions.sql to the volume root in tianon's image; run it with psql. Role passwords carry over exactly as stored, and the advisory for CVE-2026-6478 notes MD5 hashes that came along from upgrades of 13 or earlier. List them with SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%'; and have those users set new passwords. 4. Check collations. The images moved from Debian bookworm (glibc 2.36) to trixie (glibc 2.41), and floating tags moved on August 11, 2025. pg_upgrade keeps the old index files and the old recorded collation version. So you may see The database was created using collation version 2.36, but the operating system provides version 2.41. Whether that changed how ordinary text sorts is unproven. The docs' rule is rebuild, then refresh (ALTER COLLATION notes). Run it before the app starts, because a plain REINDEX blocks most queries on each table it rebuilds: # Untested. Rebuild every database's indexes, then refresh each one's recorded version. docker compose exec db reindexdb -U postgres --all docker compose exec db psql -U postgres -c 'ALTER DATABASE <db> REFRESH COLLATION VERSION;' Repeat the refresh for each database except template0, which records no version (template1 and postgres included). The refresh alone only silences the warning. REINDEX rebuilds every index, the same work a restore spends building them, so if your 17 ran on bookworm, budget it into your window or take Path A. Now prove it worked. How to check the upgrade worked Five checks tell you the upgrade landed where you think it did. They work after either path. # Untested. On the upgraded server. docker compose exec db psql -U postgres -c 'SHOW server_version;' docker compose exec db psql -U postgres -c 'SHOW data_directory;' docker compose exec db psql -U postgres -c 'SHOW data_checksums;' docker compose exec db psql -U postgres -c \ 'SELECT datname, datcollversion, pg_database_collation_actual_version(oid) FROM pg_database WHERE datcollversion IS NOT NULL;' # Over the network, the way your app connects (export PGPASSWORD first): docker run --rm --network myapp_default -e PGPASSWORD postgres:18.6 \ psql -h db -U postgres -c 'SELECT 1;' What you should see: - server_version starts with 18.6, or whichever 18 minor you pinned. - data_directory is /var/lib/postgresql/18/docker, inside the one mount. - data_checksums matches your choice: off if you passed --no-data-checksums, on after Path A or for a cluster that had them. - The two collation columns: if they differ, step 4's REINDEX and refresh are still due, which is expected after Path B from a bookworm cluster. Matching columns mean the recorded version equals the library's; they never prove a REINDEX ran. - The network query prints one row. no pg_hba.conf entry for host means other containers are still refused, which is the stop I hit. The exec checks above can't catch it: they use the local socket, not the network. Then read every table once, because --link never reads your data pages. CYBERTEC's Kaarel Moppel checks that a dump to /dev/null "runs through clean": docker compose exec db pg_dumpall -U postgres > /dev/null. It reads tables, not indexes, and takes as long as a dump. Last, cleanup. After Path B, run the delete_old_cluster.sh script pg_upgrade left in the volume root, and expect it to free almost no space: after --link, the old and new names point at the same files. After Path A, delete the old volume only once the app has run cleanly on 18. If a step stopped with a message instead, find it below. Errors you might see, and what fixes them Each of the first five rows points back to where this guide fixes it; the last six are rarer. | Message | Cause | Fix | |---|---|---| | Error: in 18+, these Docker images are configured to store database data in a format... | Old data or a mount at /var/lib/postgresql/data | Path A, or step 1 first | | old cluster does not use data checksums but the new one does (or the reverse) | Checksum settings differ | Step 2, then empty 18/docker | | could not create hard link ... Invalid cross-device link | The clusters are on different mounts | One volume at the parent (step 1) | | no pg_hba.conf entry for host | No rule for other containers | Step 4 | | database "<db>" has a collation version mismatch | Old C library's indexes and recorded version | Step 4: REINDEX, then refresh | | error mounting ... to rootfs at "/var/lib/postgresql/data" | An 18.0-era image with the data symlink | Mount the parent; use a current 18 tag | | You must identify the directory where the old cluster binaries reside | pg_upgrade ran inside the plain 18 image (issue #1377) | Use an image with both versions | | The source cluster was not shut down cleanly | The old container was killed, often by a short stop timeout (tianon #27) | Start 17 on the volume, stop it cleanly, rerun | | FATAL: role "postgres" does not exist | The cluster's superuser isn't postgres (tianon #88) | PGUSER plus --username (step 2) | | Your installation references loadable libraries that are missing from the new installation. | An extension's library isn't in the upgrade image for 18 | Install it; the list is in loadable_libraries.txt | | Only the install user can be defined in the new cluster. | The new cluster already has extra roles, for example from /docker-entrypoint-initdb.d scripts when postgres:18 was started on it first | Empty 18/docker and let the upgrade image create it | On Dokploy, one more problem waits for the upgrade to 19. On Dokploy: the mount that blocks an in-place upgrade to 19 Dokploy mounts a Postgres volume directly at PGDATA for 18 and later, so a --link upgrade to 19 can't happen inside that volume as it is. My 16 to 18 pg_upgrade ran on Dokploy, and my database there runs 18 today, but the routes below are the general ones, not a replay of that run. Check your own service's mount with docker inspect before you plan 19. Dokploy's source (v0.30.8, the latest release as of October 2026) defaults new Postgres services to postgres:18. For an 18+ image, it mounts the named volume at /var/lib/postgresql/<major>/docker, so 19/docker would sit outside it. The mount is set once, at creation. A maintainer's PR #5005 says changing the image never moves it, and that postgres:16 to postgres:18 "starts with an empty cluster"; the fix was a warning in the UI. Current Debian 18 images should refuse to start instead, as in the first section. | Platform | Where the volume mounts for 18+ | --link to 19 in place | |---|---|---| | Plain Compose (as in step 1) | /var/lib/postgresql | Works | | Coolify, services created on 18+ | /var/lib/postgresql (Coolify docs) | Works | | Dokploy | /var/lib/postgresql/18/docker | Blocked by the mount | You have two routes on Dokploy: - Dump and restore into a new 19 service. Dokploy's backups run pg_dump -Fc --no-acl --no-owner for one database, so they don't carry roles or grants. Take a pg_dumpall with the 19 tools for a full copy. - Remount, then link. Scale the service to 0, move the volume's contents into an 18/docker subdirectory (step 1's command, with 18 in place of 17 and Dokploy's volume name, <appName>-data), then change the mount to /var/lib/postgresql. Dokploy's Volumes settings can edit a mount, but its docs don't cover this route, so rehearse it on a copy. Either way, 19 itself isn't ready yet. Upgrading to 19: what's ready and what isn't As of October 9, 2026, 19 is at Beta 4, with release candidate 1 planned for October 15 and general availability for October 29 (19 Open Items). There's no postgres:19 tag and no 18-to-19 upgrade image yet. That's where the 18 layout pays off: 18/docker and 19/docker in one volume, linked in seconds. Three things to know first: - Build the upgrade image yourself until tianon publishes one: copy his 17-to-18 Dockerfile and docker-upgrade script and change the versions, including the pinned old-version package. The PostgreSQL apt repository already carries postgresql-18 next to the 19 beta packages. - The checksum choice follows you. An 18 cluster created with --no-data-checksums fails against 19's default initdb the same way. Pass the flag again, or turn checksums on in the stopped 18 cluster with 18's pg_checksums --enable, an offline rewrite of the data files. Online enabling was added to 19 and then reverted in Beta 4. - 19's pg_upgrade refuses two things a 14 to 18 cluster can have: btree_gist indexes on inet or cidr columns, and carriage returns or line feeds in database, role, or tablespace names (19 release notes, beta). Should you go on release day? 18.3 shipped out of cycle with a catalog fix that 18.0 to 18.2 users had to apply by hand, while clusters upgraded straight into 18.3 or later got it for free. So upgrade into the newest minor and treat 19.0 as a test target. When clients ask me whether to upgrade, I say yes, before support ends, only after a tested backup, and in a quiet hour we've scheduled. On 14 or 15, don't wait for 19 at all: go to the latest 18 minor before 14's final release on November 12, 2026 (versioning policy). When not to use pg_upgrade --link Even when your rehearsal doesn't fit the window, think twice about --link in five cases: - You're coming from Alpine. Alpine and Debian use different C libraries and different postgres user IDs, and tianon's image is Debian-based. Take Path A. - You're also moving from bookworm to trixie. pg_upgrade keeps the old text indexes, and the docs say to rebuild them when the library changes. Budget the REINDEX from step 4, or take Path A, which rebuilds them anyway. - You haven't restored your backup. After the new server starts, it's your only rollback. - You can only afford seconds of downtime. Logical replication gets the switchover to "several seconds," per the docs, but it's a project for a small team: schema copied by hand, sequences synced by hand, no DDL during the copy. - You're moving between x86 and ARM. Upgrade on the original machine first, then move. If the data already moved, the docs' --set-char-signedness option exists for this. The third answer that would worry me is "it's managed, not our problem." Amazon RDS and Supabase run pg_upgrade for you, and Neon has you dump and restore, or replicate, into a new project. Moving to managed Postgres doesn't remove the upgrade; it moves it to someone else's runbook, with the same "restore from backup" as the way back. Check two lines before 19 ships Mine went fine, under a minute down, after two stops. I've also seen the other ending, with a client: an upgrade that came up empty, and an app that couldn't connect for a few hours. My two stops are two lines you can check today, on your current server: # Untested. docker exec <container> psql -U postgres -c 'SHOW data_checksums;' docker exec <container> psql -U postgres -c \ "SELECT type, database, user_name, address, auth_method FROM pg_hba_file_rules WHERE type LIKE 'host%';" The second (pg_hba_file_rules, superuser only) shows the rule your other containers connect through, the one an upgraded cluster won't have until you copy it. Then make the decision: - [ ] Check the mount with docker inspect (first section): one mount at /var/lib/postgresql, or, for Path B, a layout move first. - [ ] Dump and restore into a scratch volume, and time it. - [ ] It fits your downtime window: Path A, into a fresh 18 volume. - [ ] It doesn't: move to the one-mount layout on your current major, then --check --link, then the real run. - [ ] Either way: copy pg_hba.conf before 18's first start, and run the five checks after. - [ ] On Dokploy: plan the remount or a new service before 19. Run the first command now. What it prints decides whether you pass --no-data-checksums, at 18 and again at 19. Sources 1. postgres official image documentation, docker-library, accessed 2026-09-27. 2. Pull request #1259, version-specific PGDATA, docker-library/postgres, accessed 2026-09-27. 3. Pull request #1372, data symlink removed, better detection, docker-library/postgres, accessed 2026-09-27. 4. docker-entrypoint.sh, 18/trixie at commit e00e1bd, docker-library/postgres, accessed 2026-10-08. 5. pg_upgrade, PostgreSQL 18 documentation, accessed 2026-10-08. 6. Upgrading a PostgreSQL Cluster, PostgreSQL 18 documentation, accessed 2026-09-27. 7. PostgreSQL 18.0 release notes, PostgreSQL, accessed 2026-09-27. 8. PostgreSQL 18.3 release notes, PostgreSQL, accessed 2026-10-08. 9. vacuumdb, PostgreSQL 18 documentation, accessed 2026-10-08. 10. pg_checksums, PostgreSQL 18 documentation, accessed 2026-10-08. 11. ALTER COLLATION, PostgreSQL 18 documentation, accessed 2026-10-08. 12. CVE-2026-6478, PostgreSQL security, accessed 2026-09-27. 13. Versioning policy, PostgreSQL, accessed 2026-10-08. 14. PostgreSQL 19 Open Items, PostgreSQL wiki, accessed 2026-10-09. 15. PostgreSQL 19 Beta 4 Released!, PostgreSQL, accessed 2026-10-08. 16. PostgreSQL 19 release notes (beta), PostgreSQL, accessed 2026-10-08. 17. pg_hba_file_rules, PostgreSQL 18 documentation, accessed 2026-10-09. 18. link(2) manual page, man7.org, accessed 2026-10-08. 19. docker compose up, Docker documentation, accessed 2026-10-08. 20. docker service scale, Docker documentation, accessed 2026-10-08. 21. docker-postgres-upgrade, tianon, accessed 2026-10-08. 22. tianon issue #121, checksum mismatch, accessed 2026-10-08. 23. Dokploy pull request #5005, Dokploy, accessed 2026-10-08. 24. Configure persistent storage, Coolify documentation, accessed 2026-10-08. 25. How fast is pg_upgrade anyway?, Greg Sabino Mullane, End Point, 2015, accessed 2026-10-08. 26. PostgreSQL 18: swap mode for pg_upgrade, Daniel Westermann, dbi services, 2025, accessed 2026-10-08. 27. Postmortem of database outage of January 31, GitLab, 2017, accessed 2026-10-08. 28. Upgrade to a new PostgreSQL major version, Kaarel Moppel, CYBERTEC, 2019, accessed 2026-09-27. 29. Upgrading four PostgreSQL instances to 18.2 in Docker, Rezha Julio, 2026, accessed 2026-10-08. 30. Prevent premature startup of pg_upgrade targets, Nikolay Samokhvalov, pgsql-hackers, 2026, accessed 2026-10-08. --- > Tested with: nothing run for this post. Commands are assembled from the PostgreSQL 18 docs and the official image source for postgres:18.6 and postgres:17.11 (Debian trixie), plus tianon/postgres-upgrade:17-to-18, as of October 2026. > > Changelog: First drafted October 2026, rewritten October 9, 2026, and published October 24, 2026.

  3. PHP2Shell で、対応をオープンにしている会社 はじめに 2026 年 10 月 21 日に修正版が出た PHP の深刻な脆弱性 PHP2Shell では、修正版が出る前からサービスを止める会社も出るなど、緊急の対応が相次ぎました。 前回は、レンタルサーバー各社で、脆弱性のある状態で利用者のサイトに外部からアクセスできた時間を並べ、自前でサービスを動かしている所も 5 つ図に入れました。 前回の終わりに書いたとおり、自社でサービスとインフラの両方を持っていれば、止めるか、配布元を待たずに自分でビルドするか、いつ再開するかを、すべて自社で決められます。 今回は、自前で動かしている会社のうち、対応の経緯をオープンにしていた 3 社、GREE・pixiv・コドモンを紹介します。 自分たちで判断して手を動かし、その時刻や経緯を記録しているから、ここまでオープンにできるのだと考えています。 3 社がオープンにしていること 3 社とも、公式のお知らせやブログに次の 3 つを書いています。 - サービスを止めた時刻 - サービスを戻した時刻(一部だけ戻したのか、全部戻したのかも分けて) - 止めた理由が PHP の脆弱性であること 攻撃を避けるためにソフトウェアの名前を伏せる所や、始めた時刻だけをお知らせする所も多い中で、3 社はここまで公開していました。 3 社の止めた時間 gantt title 3 社がサービスを止めていた時間(日本時間) dateFormat YYYY-MM-DD HH:mm axisFormat %m/%d %H時 section 予告と公開 予告(oss-security) :milestone, 2026-10-21 05:22, 0m 修正版の公開 :milestone, 2026-10-21 22:34, 0m section GREE サービス全体を停止 :active, 2026-10-21 23:30, 2026-10-22 06:55 SNS と決済は復旧、一部のゲームは復旧中 : 2026-10-22 06:55, 2026-10-22 15:00 section pixiv メンテナンスモード :active, 2026-10-21 19:00, 2026-10-21 23:16 section コドモン 主要サービスを停止 :active, 2026-10-21 14:49, 2026-10-22 03:22 一部の機能は停止を続ける : 2026-10-22 03:22, 2026-10-23 09:00 PHP 公式は日本時間の 10/21 早朝、修正版を 23:00 に出すと予告しました(oss-security への投稿は 5:22)。実際に出たのは 30 分ほど早い 22:34 です(GitHub の security advisory の公開時刻)。 GREE GREE はサービス全体を止めました。 - 10/21 23:30 ごろ: サービス全体を止めて緊急メンテナンスを開始 - 10/22 6:55 ごろ: メンテナンスを終了し、SNS や決済などを復旧(一部のゲームは復旧作業を続ける) - 10/22 15:30: 全体復旧の見込み(15 時ごろ)のあと、タイトルに「【復旧】」と付けてお知らせを更新 修正版が出てから 1 時間ほどでサービス全体を止め、7 時間半ほどで SNS や決済などのプラットフォームの機能から先に戻しています。 お知らせでは、PHP の脆弱性が公表されたことを受けてシステムの安全性を確保するための対応だと、理由を説明しています。使っているソフトウェアの名前を伏せずに示しています。 復旧作業が続いていた一部のゲームについては、各ゲームのお知らせを見るよう案内しています。全体の状況はこのお知らせ、個別のゲームは各ゲームのお知らせ、と案内先を分けています。 全体が復旧する見込みは 10/22 15 時ごろと示し、15:30 にはお知らせのタイトルに「【復旧】」と付けて更新しました。被害やデータの流出は確認されていないことも明記し、問い合わせフォームも案内しています。停止から一部の復旧、全体の復旧まで、段階ごとに時刻を追える書き方になっています。 pixiv pixiv は修正版が出る前にサービスを止めて待機し、予定(23:00)より早く 22:34 に出た修正版を 22:41 に確認して、そこから 35 分で再開しました。 利用者向けのお知らせとは別に、公式ブログ(pixiv inside)に作業の時刻が分単位で残っています。 - 10/21 19:00: 影響を受けうるサービスをメンテナンスモードにする - 22:41: 予定(23:00)より早く出た修正版(22:34 公開)を確認 - 22:42〜22:56: コンテナイメージのビルド - 22:56〜23:16: Kubernetes へのデプロイ(ステージングでの確認を含む) - 23:16: メンテナンスモードを解除 35 分で戻せたのは、修正版が出る前に準備を済ませていたからです。 Docker 公式イメージのビルドスクリプトを使って、PHP のバージョンだけを差し替えられるようにしていました。公式イメージの公開を待たずに、ソースから自前でビルドする経路も用意していました。 Docker 公式イメージの更新がマージされたのは 10/22 0:16 で、pixiv が再開した 23:16 の 1 時間後です。配布元を待たずに自前でビルドしたことで、この 1 時間の待ち時間を減らせています。 またブログでは、メンテナンスモードが入っていないエンドポイントがあったことも課題として挙げています。今回は条件がそろっていただけで、毎回この手順で済むとは限らない、とも振り返っています。 コドモン コドモンは、保育施設と保護者が使うサービスです。 修正版が出る 8 時間ほど前の 14:49 までに、対象となる全サービスを止めました。 - 10/21 9:01: 社内で把握 - 11:37: 23:00 の公開に合わせて、夜に止める方針で合意 - 12:37: 情報が広まってきたのを受けて、前倒しで止めると決定 - 13:00 ごろ〜14:49: 順に止め、全対象サービスの停止を完了 - 22:38: 修正版の公開を検知 - 10/22 3:22: 職員用の管理画面、保護者向けの連絡帳アプリなどを再開 - 10/23 9:00: 最後まで止めていた所在確認アラートを再開 止めると決めた理由も書いています。お預かりしているお子さまの情報を守ることを最優先にした、という判断です。 一部のサービスだけ止める案、お迎えの時間まで動かす案、監視を強めて検知したら止める案も検討し、採らなかった理由を一つずつ書いています。 反省点も並べています。修正版が届く時刻が PHP の配布元ごとに違うことを事前に把握していなかったこと、作業と判断が深夜に集中したこと、予防のための停止だと利用者に伝わりにくかったこと、などです。 何を重視するかで、止め方は変わる 3 社とも自社で決められる立場でしたが、止めた時刻と戻し方はそれぞれ違っていました。コドモンは修正版が出る 8 時間ほど前の 14:49 までに、pixiv は出る前の 19:00 に、GREE は出てから 1 時間ほどあとの 23:30 ごろに止めています。 自分のサービスで同じ判断をするなら、何を重視するかで、次のように選び方が変わります。 | 何を重視するか | 止め方 | 近い対応をした会社 | |---|---|---| | 利用者の情報を守ることを最優先 | 修正版が出る前に、日中でも前倒しで止める | コドモン | | 止める時間を短くしたい | 準備を済ませてから止めて待ち、出たらすぐ当てて戻す | pixiv | | 公開まではふだんどおり動かし、戻すときは確実に | 出てからサービス全体を止め、段階的に戻す | GREE | - コドモン は、お迎えの時間まで動かす案も比べたうえで、お子さまの情報を守ることを最優先にして日中に止めました。主要なサービスを 10/22 3:22 に戻し、残りの機能も翌日に再開しています - pixiv は、修正版をすぐ当てられるように準備してから 19:00 に止めました。修正版が出たあと自前でビルドして当て、23:16 に再開しています - GREE は、修正版が出たあとの 23:30 ごろにサービス全体を止めました。翌朝 6:55 ごろに SNS や決済などのプラットフォームを先に戻し、各ゲームの復旧はそれぞれのお知らせで案内しています 修正版が出る前から止めて安全を優先するやり方も、準備を整えて止める時間を短くするやり方も、公開後に全体を止めて段階的に戻すやり方もあります。唯一の正解はなく、サービスの性質と何を守りたいかで、選び方は変わります。 自分たちで決めて記録しているから、オープンにできる 3 社に共通するのは、サービスもインフラも自社で持っていることです。止めるかどうか、どこまで止めるか、いつ戻すかを、配布元やほかの会社の判断を待たずに自分たちで決められます。 自分たちで決めて自分たちで作業しているので、いつ何をしたか、なぜそうしたかを、自社の記録だけで正確に書けます。 - GREE は、全体を止めるタイミングも、SNS や決済から先に戻す順番も自社で決めています。そのため、停止から一部の復旧、全体の復旧まで、段階ごとの時刻をそのままお知らせに出せています - pixiv は、Docker 公式イメージの更新を待たずに、コンテナイメージのビルドから Kubernetes へのデプロイまでを自分たちで動かしています。作業を自社で完結させているから、分単位の記録をブログに残せています - コドモン は、社内で把握した 9:01 から方針を変えた 12:37 まで、判断した時刻と理由を記録していました。自分たちで比較して決めているから、採らなかった案とその理由まで公開できています 判断から作業までが自社の中で完結していて、その記録も残っているから、ここまで具体的な時刻や経緯をオープンにできます。 前回のレンタルサーバーは、この点で条件が違います。インフラは業者のものでも、その上で動くサイトは利用者のものです。業者が利用者のサイトを止めれば、利用者の業務も止まるため、業者だけの判断では止めにくい事情があります。また、自前でビルドするのではなく配布元のパッケージを待つ構成なら、対応を始める時刻も配布元しだいになります。利用者との関係や作業工程の制約があるので、すべてを自社だけで決めて動ける 3 社と同じようにはいかない所があるのは仕方ありません。 おわりに 今回紹介した 3 社は、対応を自社で決めるだけでなく、サービスを止めた時刻と再開した時刻、そして PHP の脆弱性への対応であることを明記していました。さらに pixiv はビルドからデプロイまでの分単位の記録を、コドモンは方針変更の時刻や採らなかった案の理由まで公開しています。 自分たちで判断して対応し、その記録を残しているから、いつ止めていつ戻したかを正確に出せるだけでなく、判断や作業の経緯までオープンにできる。そこがこの 3 社の良さです。 本記事の情報は 10/29 時点のものです。 参考文献 - GREE「【復旧】緊急メンテナンスについて」 games.gree.net - pixiv お知らせ pixiv.net - pixiv inside「PHP2Shell」対応の記録 inside.pixiv.blog - コドモン Product Team Blog「PHP2Shellへの対応を振り返って」 tech.codmon.com - oss-security(予告) openwall.com - Docker official-images #22541 official-images#22541 - GitHub の security advisory(修正版の公開の時刻 13:34 UTC)github.com/php/php-src/security/advisories/GHSA-w… ・ github.com/php/php-src/security/advisories/GHSA-6…

    1 like
  4. PHP2Shell で、サイトが無防備だった時間 はじめに PHP2Shell について、Qiita にもいろいろな記事が出ています。 みなさんの記事を読んでいてふと思いました。 利用者のサイトが脆弱性のある状態で外部からアクセスできるままになっていた時間を、各社のお知らせから測ってみようか、と。 ちなみに、私が昔いた現場はパッチの適用が数日遅れていました。さもありなん、です。 PHP の脆弱性の話になると、PHP を使う開発者だけでなく、レンタルサーバーの業者までいい加減なのでは、と言われがちです。私も最初はそう思っていました。 脆弱性のある状態で利用者のサイトを外部にさらしていたこと自体は、インフラを預かる業者の責任です。ただ、それが「いい加減だったから」なのか、作業の工程上しかたない部分があったのかを、公式のお知らせの時刻で確かめます。 予告から公開まで PHP 公式は日本時間の 10/21 早朝、深刻度 9.2 の脆弱性 2 件の修正版を 23:00 に出すと予告しました(oss-security への投稿は 5:22)。実際に出たのは 22:34 です(GitHub の security advisory の公開時刻)。 gantt title 予告から修正版の公開まで(日本時間) dateFormat YYYY-MM-DD HH:mm axisFormat %m/%d %H時 section 予告と公開 予告(oss-security) :milestone, 2026-10-21 05:22, 0m 修正版がない時間(17 時間あまり) : 2026-10-21 05:22, 2026-10-21 22:34 修正版の公開 :milestone, 2026-10-21 22:34, 0m 一方で、悪用の試みは公表前の 10/17 から見つかっていたことが判明しています。 予告より早く出た理由は公表されていません。 各社の対応時刻と停止期間 安全になるための条件は、パッチを当て終えることです。 ただ、当て終えた時刻だけでは、危険だった時間は分かりません。先にサイトを止めていれば、そのあいだは外部からアクセスできないからです。 そこで、止めた時間と当て終えた時刻の両方を図にしました。 図の最上段は予告と公開の時刻です。その下がレンタルサーバー、いちばん下が自前でサービスを動かしている所で、バーの色は次のとおりです。 - 赤いバー(露出): 予告のあと、サイトの停止や緩和策、パッチの適用がまだで、脆弱性のある状態で外部からアクセスできた時間 - 薄い青のバー(停止): サイトやサービスを止めていた時間 - 青紫のバー: 緩和策を入れていた時間、またはパッチを順番に当てていた時間 - 灰色のバー: パッチを当て終えた後、または表示を再開した後 「自前」は、レンタルサーバーを借りずに、自社のサービスを自社で用意したサーバー(クラウドを含む)で動かしている所のことです。外部から見ても PHP を使っているかどうか分からないので、止めた時間だけを描いています。 gantt title 予告から公開後までの状態(日本時間) dateFormat YYYY-MM-DD HH:mm axisFormat %m/%d %H時 section 予告と公開 予告(oss-security) :milestone, 2026-10-21 05:22, 0m 修正版の公開 :milestone, 2026-10-21 22:34, 0m section レンタルサーバー エックスサーバー 露出 :crit, 2026-10-21 05:22, 2026-10-21 12:00 エックスサーバー 緩和策のみ・順次適用(全部の完了は 10/26) : 2026-10-21 12:00, 2026-10-23 12:30 コアサーバー V1 ほか 露出 :crit, 2026-10-21 05:22, 2026-10-22 00:50 コアサーバー V1 ほか 適用済み :done, 2026-10-22 00:50, 2026-10-23 12:30 mixhost 露出 :crit, 2026-10-21 05:22, 2026-10-22 10:18 mixhost 適用済み :done, 2026-10-22 10:18, 2026-10-23 12:30 ヘテムル 露出 :crit, 2026-10-21 05:22, 2026-10-22 03:05 ヘテムル サイトを停止 :active, 2026-10-22 03:05, 2026-10-22 11:24 ヘテムル 表示を再開 :done, 2026-10-22 11:24, 2026-10-23 12:30 お名前.com 露出 :crit, 2026-10-21 05:22, 2026-10-22 20:45 お名前.com 適用済み :done, 2026-10-22 20:45, 2026-10-23 12:30 KAGOYA 露出 :crit, 2026-10-21 05:22, 2026-10-22 14:40 KAGOYA 順次適用 : 2026-10-22 14:40, 2026-10-23 00:00 KAGOYA 適用済み :done, 2026-10-23 00:00, 2026-10-23 12:30 section 自前で動かすサービス BASE 停止 :active, 2026-10-21 15:50, 2026-10-22 08:00 ジンジャー 停止(終わりは予定の時刻) :active, 2026-10-21 17:00, 2026-10-22 05:00 freee の 4 サービス 停止 :active, 2026-10-21 19:04, 2026-10-22 12:56 Garoon クラウド版 停止 :active, 2026-10-21 21:00, 2026-10-22 03:01 LoGo フォームの PDF 印刷 停止 :active, 2026-10-21 21:00, 2026-10-22 02:09 図に入れた自前の 5 つは songchong さんの記事から取ったもので、どれも修正版が出る前に止めています(同じ記事にあるほかの 6 つは、公開のあとに止めています)。 レンタルサーバー各社の動きは次のとおりです。 - ヘテムル・ロリポップ!: パッチが当たるまで利用者のサイトごと止めた 2 社です(ロリポップ!は止めた時刻の記載がないので図にはありません) - エックスサーバー: サイトは止めませんでしたが、公開前の 12:00 までに緩和策を入れ、公開後は順次パッチを当てています - コアサーバー V1 など: 止めずに、公開から 2 時間ほどで当て終えています 図に入れていない所は次のとおりです。 | 事業者 | 止めた・緩和策 | パッチの適用 | |---|---|---| | ロリポップ! | 10/21 からサイトの表示を停止 | 10/22 15:33 の更新で適用済み | | さくらのレンタルサーバ | 10/21 22:30 から会員関連の機能を緊急メンテナンス | 10/23 に適用済み | | WebARENA | 10/21 に緩和策と、海外からのアクセスの停止 | 10/22 17:30 から緊急メンテナンス | | ConoHa WING | ファイルマネージャーと Web メールを停止 | 10/22 時点で順次適用中 | | CPI | — | 8.2〜8.4 は 10/22、7.4・8.0 は 10/27〜10/29 | レンタルサーバーの時刻は、各社の公式のお知らせから拾いました(10/27 に確認)。並べたのはお知らせに書かれた時刻(またはお知らせの更新時刻)で、必ずしも全部のサーバーに当て終えた時刻ではありません。 ただ、こうして並べても「各社が何時間、脆弱性のある状態で外部にさらしていたか」は一つの数字に決まりません。 10/17(悪用の試みが見つかった日)と 10/21(公式の予告)のどちらから数えるかで 4 日ずれますし、サイトを止めたのか、お知らせに書いていないだけなのか、区別のつかない所があるからです。 !どこから数えるか(10/17〜10/29) 当て終えるまでに時間がかかった理由(推定) - 公式の修正版は 8.2〜8.5 だけで、古いバージョンはバックポートか独自の修正待ち - 一度に当てず順番に当てたので、全体が終わるまで時間がかかった(KAGOYA は 14:40〜24:00 の 9 時間あまり。順番にした理由はお知らせに書かれていません) - パッケージで PHP を入れている場合は、配布元(CloudLinux や remi など)のビルドを待つ必要があった(下の表) - 公開直後は配布元にアクセスが集中し、つながりにくくなった所があった(Ubuntu 向けパッケージの配布に使われる Launchpad が 503 エラー、php.net が 504 エラー) !修正版がサーバーに当たるまでの 3 つの経路 配布元が修正版のパッケージを出した時刻は次のとおりです(比較のため、先頭に PHP 公式の公開時刻を入れています)。 | 配布元 | 出た日時(日本時間) | 修正版の公開から | |---|---|---| | PHP 公式(ソースコード) | 10/21 22:34 | — | | Debian(DSA 6574-1) | 10/21 22:45 | 11 分 | | Ubuntu(USN-8948-1) | 10/21 22:56 | 22 分 | | Debian LTS(DLA 4848-1) | 10/21 23:11 | 37 分 | | Ondřej Surý の PPA(Launchpad) | 10/21 23:45 | 1 時間 11 分 | | Docker 公式イメージ(official-images へのマージ) | 10/22 0:16 | 1 時間 42 分 | | Red Hat Enterprise Linux 8〜10(AlmaLinux 9 も同じ日) | 10/21(UTC・時刻なし) | — | | remi(RHEL 系で広く使われるリポジトリ) | 10/22 21:58 | 23 時間 24 分 | | Red Hat の延長サポート向け(EUS など) | 10/23(UTC・時刻なし) | — | 多くの配布元は、公開から 2 時間以内にパッケージを出しています。待ちが長かったのは remi と、Red Hat の延長サポート向けです。 レンタルサーバー各社が修正をどこから得たかは、お知らせに次のように書かれています。 | 事業者 | 修正の出どころ(お知らせの記載) | |---|---| | エックスサーバー | 8.2〜8.5 は修正版に更新、5.1〜8.1 はバージョンを変えずに修正パッチを適用。出どころの記載なし | | コアサーバー V1 | 記載なし | | mixhost | 採用している CloudLinux の修正パッケージ。サポートが終わったバージョンも CloudLinux が独自に修正 | | ヘテムル | 公式の修正版がない古いバージョンは、自社で修正を作成 | | ロリポップ! | 公式の修正版が出たバージョンはそれを適用し、ほかは自社で独自に修正 | | お名前.com | バージョンを変えずにパッチを適用。出どころの記載なし | | KAGOYA | 修正版に更新。出どころの記載なし | | さくらのレンタルサーバ | 記載なし | | WebARENA | 記載なし | | ConoHa WING | バージョンを変えずにパッチを適用。出どころの記載なし | | CPI | 8.2〜8.4 は修正パッチを適用、7.4・8.0 はパッチ適用版を準備。出どころの記載なし | 配布元のパッケージを待っていたと書いている所はありませんでした。当て終えるまでにかかった時間のうち、どこまでが配布元の待ちによるものかは、お知らせからは分かりません。 主な配布元の時刻を、さきほどの図の最上段に足すと次のとおりです(自前の段は省きました)。レンタルサーバー各社のバーと見比べられます。 gantt title 配布元の公開時刻とレンタルサーバーの対応(日本時間) dateFormat YYYY-MM-DD HH:mm axisFormat %m/%d %H時 section 予告・公開・配布元 予告(oss-security) :milestone, 2026-10-21 05:22, 0m 修正版の公開 :milestone, 2026-10-21 22:34, 0m Debian(22時45分)・Ubuntu(22時56分) :milestone, 2026-10-21 22:45, 0m Ondřej Surý の PPA :milestone, 2026-10-21 23:45, 0m Docker 公式イメージ :milestone, 2026-10-22 00:16, 0m remi :milestone, 2026-10-22 21:58, 0m section レンタルサーバー エックスサーバー 露出 :crit, 2026-10-21 05:22, 2026-10-21 12:00 エックスサーバー 緩和策のみ・順次適用(全部の完了は 10/26) : 2026-10-21 12:00, 2026-10-23 12:30 コアサーバー V1 ほか 露出 :crit, 2026-10-21 05:22, 2026-10-22 00:50 コアサーバー V1 ほか 適用済み :done, 2026-10-22 00:50, 2026-10-23 12:30 mixhost 露出 :crit, 2026-10-21 05:22, 2026-10-22 10:18 mixhost 適用済み :done, 2026-10-22 10:18, 2026-10-23 12:30 ヘテムル 露出 :crit, 2026-10-21 05:22, 2026-10-22 03:05 ヘテムル サイトを停止 :active, 2026-10-22 03:05, 2026-10-22 11:24 ヘテムル 表示を再開 :done, 2026-10-22 11:24, 2026-10-23 12:30 お名前.com 露出 :crit, 2026-10-21 05:22, 2026-10-22 20:45 お名前.com 適用済み :done, 2026-10-22 20:45, 2026-10-23 12:30 KAGOYA 露出 :crit, 2026-10-21 05:22, 2026-10-22 14:40 KAGOYA 順次適用 : 2026-10-22 14:40, 2026-10-23 00:00 KAGOYA 適用済み :done, 2026-10-23 00:00, 2026-10-23 12:30 危険な時間を減らすには ここからは私の見方です。 いい加減だったのか 測ってみた結果、業者がいい加減だったとは言えません。時間がかかった所も、作業の工程の限界によるものだと私は見ています。 - 主な配布元は修正版の公開から 2 時間以内にパッケージを出し、レンタルサーバーでも公開から 2 時間ほどで当て終えた所(コアサーバー V1 など)があります - 予告から公開までのあいだに、手を打った所があります。エックスサーバーは 12:00 までに緩和策を入れ、ヘテムルは 13:30 ごろから phpMyAdmin などの機能を止め始め(サイトの表示を止めたのは 10/22 3:05)、さくらは 22:30 から会員関連の機能の緊急メンテナンスに入りました - 公式の修正版がない古いバージョンにも手当てをしています。ヘテムルとロリポップ!は自社で修正を作り、mixhost は CloudLinux の修正を使い、エックスサーバーは 5.1 まで対応を広げました もちろん、パッチが当たるまでのあいだ、脆弱性のある状態で利用者のサイトを外部にさらしていた責任は業者にあります。 ただ、完了が翌日以降になった所も、手を抜いていたとは言えません。古いバージョンを抱えていたり、配布元のパッケージを待つ構成だったり、順番に当てる必要があったりすれば、作業の工程上、すぐには当て終えられないからです。 CERT-EU・英国の NCSC・米国の NSA・日本の IPA は、すぐ当てられないなら止めるよう呼びかけていました。サイトを止めてからパッチを当てるやり方は、再開が遅くなっても安全です。呼びかけどおりサイトごと止めたヘテムルとロリポップ!がこれにあたります。 とはいえ、レンタルサーバーが利用者のサイトを止めるのは簡単ではありません。 レンタルサーバーの業者はインフラを持っていますが、その上で動くサイト(サービス)は利用者のものです。止めれば利用者の業務が止まり、動かし続ければ脆弱性にさらされるので、業者だけでは決めにくい立場にあります。 これに対して、自社でサービスとインフラの両方を持っている会社は、サービスを止めるか、配布元を待たずに自分でビルドするか、いつ再開するかを、すべて自社で決められます。さきほどの図で、自前の 5 つが修正版の公開前に止めていたのも、自社で決められる立場にあったから取れた手です。 時刻を分単位で出している所は、測られてもいいと分かって出している所です。書いていない所が悪い、という話ではありません。 どこで露出していたか ここまでの図と表から、露出していた時間は 2 つに分かれます。 !露出していた時間は 2 つに分かれる(レンタルサーバー) 1 つ目の「予告から公開まで」は、この図のレンタルサーバーではエックスサーバーが 12:00 までに緩和策を入れ、さきほどのガントチャートにあった自前の 5 つが公開前にサービスを止めていました。 2 つ目の「公開から当て終える・止めるまで」は、サポート切れの古いバージョンほど長引いています。公式の修正版がないため自社で作るか CloudLinux などから得る必要があり、図に入れていない CPI の PHP 7.4・8.0 は適用が 10/27〜10/29 でした。 減らすためにできること 1 で「予告から公開まで」、2・3 で「公開から当て終えるまで」の 2 つの露出時間を減らし、4・5 で利用者ごとの影響と確認の手間を減らせます。 !2 つの時間を、どこで減らすか 1. 予告の段階(止めるか遮断するか): 止めるか動かし続けるかを利用者が前もって選べる設定(Google Compute Engine のメンテナンス設定のようなもの)を用意し、動かし続ける利用者は WAF で攻撃のリクエストを遮断します。Cloudflare は今回、修正版の公開の 10 時間半ほど前に、PHP2Shell を遮断するルールを既定でオンにしています 2. 配布元を待たない経路: ソースからビルドできるようにしておけば、パッケージを待たずに済みます。pixiv は公式イメージの公開を待たずにビルドする経路を用意していて、修正版に気づいてから 35 分で再開しました。Docker 公式イメージの更新がマージされたのは、その 1 時間後の 10/22 0:16 です 3. サポート切れのバージョンへの備え: 古いバージョンほど修正に時間がかかりました。移行の期限を決めておくか、TuxCare のようなバックポートを使えるようにしておきます 4. アカウントごとの実行環境の分離: 順番に当てると全体が終わるまで時間がかかります。mixhost が採用している CloudLinux には、アカウントごとに PHP を選べる PHP Selector があります 5. 露出していた時間の利用者ごとの通知: 順番に当てた場合、全体の完了時刻だけでは自分のサイトがいつまで露出していたか分かりません。その利用者のサイトに外部からアクセスできた時間を知らせます 処理の流れにすると、次のとおりです。 flowchart TD A[予告が出る] --> B{利用者ごとの設定} B -->|止めて待つ・既定| C[サイトを止める] B -->|動かしたまま| D[WAF で攻撃のリクエストを遮断する] E[上流を待たずにソースからビルド] --> F[修正版を当てる] G[サポート切れのバージョン] -->|期限を決めて移行か、バックポート| F C --> F D --> F F --> H[再開して、外部からアクセスできた時間を利用者ごとに通知] subgraph アカウントごとに分けた PHP の実行環境 B C D F end !危険な時間を減らす処理の流れ 必要な仕組みは、どれも既存の製品や機能にあります。これらをあらかじめ組み合わせておけば、作業の工程でどうしてもかかる時間があっても、外部にさらす時間は減らせます。 もっとも、いちばん強いのは、外部からアクセスできる公開側に PHP の実行環境(ランタイム)を置かないことです。前回の話のとおり、置かなければ、さきほどの対応の図に並ぶこともありません。 おわりに 脆弱性のある状態でサイトを外部にさらしていた責任はレンタルサーバー業者にあります。ただ、時間がかかったのは業者がいい加減だったからではなく、作業の工程や、利用者のサイトを業者だけでは止めにくい立場によるものです。そのうえで、前もって仕組みを組み合わせておけば、危険な時間はまだ減らせます。 一方で、自社でサービスとインフラの両方を持っていれば、止める判断もビルドの手順も自分で決められます。次の記事では、自前でサービスを動かしている会社のお知らせから、止めた時刻と戻した時刻、そこに至る経緯まで詳しく公開していた 3 社(GREE・pixiv・コドモン)を取り上げています。 10/27 時点の情報です。サイトの側から見た Tivo0921 さんの記事もあわせてどうぞ。 興味のある方の参考になれば幸いです。ご指摘は歓迎します。 参考文献 - PHP News Archive 2026 php.net - oss-security(予告) openwall.com - 配布元: Debian DSA 6574-1・Debian DLA 4848-1・Ubuntu USN-8948-1・Ondřej Surý の PPA・Docker official-images #22541・Red Hat・remi - GitHub の security advisory(修正版の公開の時刻 13:34 UTC)github.com/php/php-src/security/advisories/GHSA-w… ・ github.com/php/php-src/security/advisories/GHSA-6… - Tenable(10/17 の悪用の試み) tenable.com - 各社のお知らせ: エックスサーバー・同 停止・コアサーバー V1・mixhost・ヘテムル・ロリポップ!・お名前.com・KAGOYA・さくら・WebARENA・ConoHa WING・CPI - UptimeRobot(配布元の混雑) uptimerobot.com - IPA 注意喚起 ipa.go.jp - Cloudflare の WAF(ppc.land) ppc.land - CloudLinux PHP Selector support.cpanel.net - TuxCare(Plesk) plesk.com - Google Compute Engine cloud.google.com

    1 like
  5. The Compose Race Your Coding Agent Inherits From You !Hero artwork: a robotic arm stamping green checkmarks on a conveyor belt of identical generated compose files, the finished stack toppling into a wall labeled connection refused Ask a coding agent for a Docker Compose file with Postgres, then cold-boot the result on a machine that has never run it. There is a good chance the app container starts, immediately asks for a database that does not exist yet, and fails its first connections. Wait a few seconds, try again, and everything works. The agent will tell you it tested the file. Both things are true, and the bug is older than the agent: it inherited the race from the tutorials it was trained on, and it also inherited the half-fix that looks like the fix. Docker Compose is a tool for defining and running multi-container applications from one YAML file. I tested the generated shape, the half-fixed shape, and the corrected shape on my own host this week: three cold starts on fresh volumes (Docker Engine 29.1.3, Compose v5.1.4), the inherited race, the inherited half-fix that still races, and the three-line correction that actually holds. Everything below is from that run, with the run methodology and the timestamps as captured. > Key Takeaways > - A bare depends_on: list only orders container startup. Compose explicitly does not wait for "ready", only "running" (Docker docs). > - The most-copied Postgres healthcheck, pg_isready -U postgres, probes the Unix socket, and during first-boot initialization a temporary Postgres server answers on that socket before the real one is up (docker-library/postgres issue #146). > - In my run, that socket-probe healthcheck reported the database healthy 4 seconds before Postgres actually accepted a TCP connection. The gate was wired correctly. The probe lied. > - The fix is three edits: condition: service_healthy, restart: true, and -h 127.0.0.1 on the probe. My corrected file connected on attempt one, on a cold volume. In this post: - [What does the agent actually write?](#what-does-the-agent-actually-write) - [Why does the race survive a working demo?](#why-does-the-race-survive-a-working-demo) - [What did I actually run?](#what-did-i-actually-run) - [Why doesn't the copied healthcheck fix it?](#why-doesnt-the-copied-healthcheck-fix-it) - [What does the fixed file look like?](#what-does-the-fixed-file-look-like) - [How do you review agent YAML in 60 seconds?](#how-do-you-review-agent-yaml-in-60-seconds) - [FAQ](#faq) What does the agent actually write? Two lines date a compose file, and agents write both of them together. The first is the bare dependency list. In the long form of depends_on, per the compose-file services reference, this same key takes a condition instead, which is the part the 2021 corpus never learned: services: app: depends_on: - db The second is the socket-probe healthcheck on the database. healthcheck is a container configuration that makes the runtime probe the service at a fixed interval and report a health status: db: image: postgres:16-alpine healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] Neither line is broken on its own. The first one does exactly what it says: start db before app. The second one does exactly what it says: exit 0 when something answers on the Postgres Unix socket. The problem is what they don't say, and they cluster in generated files because they cluster in the training corpus. Both come from the same generation of quick-start guides, the ones that shipped years before the long-form depends_on with conditions was common knowledge. The evidence that this is a corpus problem, not a reasoning problem, is now measured. Work on updating model knowledge after API changes found that models continue generating outdated API calls "even with access to current documentation" (arXiv:2506.20495). A study of deprecated-API recommendations reports that 37.4 percent of GPT-3.5's API predictions were deprecated, and that fixing this by retraining is impractical because library APIs change too fast (arXiv:2511.21022). Compose YAML is not a Python library call, but the mechanism transfers exactly: the spec moved, the tutorials didn't, and the average of the corpus is the 2021 shape. !Diagram of the inheritance pipeline: dated tutorial books flow into a glowing model core that outputs a compose file card, which feeds a container stack where a race condition plays out Why does the race survive a working demo? Because a warm volume hides it. Postgres only does its slow first-boot work when the data directory is empty. The entrypoint script runs initdb, starts a temporary server for the setup phase, runs anything mounted in /docker-entrypoint-initdb.d/, and only then hands over to the real server as PID 1. On a volume that has already been initialized, the container skips all of that, and the gap between "container running" and "accepting connections" shrinks to near zero. Whoever verifies the compose file, human or agent, usually verifies it on a warm volume. The demo passes. The agent's confidence is not fake; the test genuinely passed on the machine it had. The first cold start is the one that pays for it: CI runner, teammate's laptop, fresh VM. That is the boot where the app container starts a beat after the database container and asks a half-initialized Postgres for a connection. Here is the output of that exact cold start from my run, an alpine app probing db:5432 every half second in a loop. Consider it the shape of every "it works on the second try" bug report you have ever closed: app-1 | ATTEMPT 1: CONNECTION-REFUSED app-1 | ATTEMPT 2: CONNECTION-REFUSED app-1 | ATTEMPT 3: CONNECTION-REFUSED app-1 | ATTEMPT 4: CONNECTION-REFUSED app-1 | ATTEMPT 5: CONNECTED Two seconds of refused connections on a fast local disk. The database log tells you why: the "ready to accept connections" line lands at 08:10:28, after the app has already been asking. On a slower disk, or with init scripts in /docker-entrypoint-initdb.d/, the window stretches to seconds more. If your app has restart: unless-stopped, Compose quietly restarts it until it sticks, the stack comes up, and the race is invisible again until the morning it crash-loops. I documented the anatomy of this race in the healthcheck deep-dive earlier in this series. The part that is new here is where the YAML came from, and the fact that the obvious fix, asking the agent to add a healthcheck, often doesn't fix it. <!-- UNIQUE INSIGHT: the inherited race and the inherited half-fix come from the same corpus generation, so the agent reproduces both together. --> What did I actually run? <!-- ORIGINAL DATA: reproduced locally, Docker Engine 29.1.3, Docker Compose v5.1.4, alpine:3.20 + postgres:16-alpine, 2026-10-29, three runs on fresh volumes. --> Three cold starts on my own host: Docker Engine 29.1.3, Docker Compose v5.1.4, with alpine:3.20 as the app and postgres:16-alpine as the database. Every run started from a deleted volume, so Postgres went through its full first-boot initialization each time. The app is deliberately dumb, a shell loop that tries db:5432 every half second and prints the result, so the timing is visible and nothing else can mask it: services: app: image: alpine:3.20 depends_on: - db command: > sh -c 'i=0; while [ $$i -lt 30 ]; do i=$$((i+1)); if nc -z -w 1 db 5432 2>/dev/null; then echo "ATTEMPT $$i: CONNECTED"; exit 0; else echo "ATTEMPT $$i: CONNECTION-REFUSED"; fi; sleep 0.5; done; echo "GAVE UP"' For the third run, to make the initialization window wide enough to observe, I mounted a one-line init script into /docker-entrypoint-initdb.d/ that sleeps four seconds. The sleep does not create the gap, it only stretches the same initialization phase that already existed in the first run, and every timestamp below comes from docker inspect and the Postgres log, not from my watch. The three runs, all from the same host: | Run | File shape | Outcome | |---|---|---| | 1 | bare depends_on list | 4 refused connections, connected on attempt 5 | | 2 | service_healthy + TCP probe | connected on attempt 1, app held until Healthy | | 3 | service_healthy + socket probe | "healthy" 4s early, 7 refused connections after the verdict | Why doesn't the copied healthcheck fix it? Here is where the inherited fix becomes worse than the inherited race. Ask an agent to fix the startup race and it will, correctly, reach for the long-form dependency. service_healthy is the depends_on condition that holds the dependent container back until the dependency's healthcheck reports healthy: app: depends_on: db: condition: service_healthy That is the right gate. Compose will not start the app until the database's healthcheck reports healthy. But the healthcheck it copies, the same one from the same tutorials, is pg_isready -U postgres, and pg_isready without a host flag connects to the Unix socket. pg_isready is the Postgres client's readiness probe; it exits 0 when a server accepts connections. On the official Postgres image, first boot runs a temporary server for initialization that listens on the Unix socket only. The probe passes while the real server, the one your app connects to over TCP, is not up yet. The maintainers have been explaining this in issue #146 for years: "Simply adding -h 127.0.0.1 to the pg_isready command line should be enough to detect readiness correctly." I ran that combination, the correct gate with the copied probe, and watched docker inspect while it happened. The healthcheck went starting at +0.78 seconds and healthy at +3.02 seconds. The real server accepted its first TCP connection at +7.06 seconds. Four seconds of lie. Compose trusted the verdict, started the app, and the app did this: app-1 | ATTEMPT 1: CONNECTION-REFUSED app-1 | ATTEMPT 2: CONNECTION-REFUSED app-1 | ATTEMPT 3: CONNECTION-REFUSED app-1 | ATTEMPT 4: CONNECTION-REFUSED app-1 | ATTEMPT 5: CONNECTION-REFUSED app-1 | ATTEMPT 6: CONNECTION-REFUSED app-1 | ATTEMPT 7: CONNECTION-REFUSED app-1 | ATTEMPT 8: CONNECTED Seven refused connections after the container was declared healthy. The probe log shows the mechanics, and it is worth reading slowly because it is the whole post in three lines: 08:13:52.4 exit 0 /var/run/postgresql:5432 - accepting connections 08:13:54.5 exit 0 /var/run/postgresql:5432 - accepting connections 08:13:56.5 exit 2 /var/run/postgresql:5432 - no response The first two probes passed against the temporary initialization server, on the socket. Then the entrypoint shut that server down to finish init work, and the third probe failed, no response, while the real server was still seconds away. The status even flipped to unhealthy for a moment during the handoff. If the failure had landed one probe later, after the container was already marked healthy, the streak would have needed three consecutive failures to matter, and by then the app had long since started and crashed. This is the failure mode that makes people conclude the healthcheck pattern "doesn't work" and delete it. The gate worked. The probe answered a different question than the one the app asks. What does the fixed file look like? Three edits to the generated file, and my run 2 output after them: Container racefix-db-1 Started Container racefix-db-1 Waiting Container racefix-db-1 Healthy Container racefix-app-1 Starting Container racefix-app-1 Started app-1 | ATTEMPT 1: CONNECTED The app is not created until db reports Healthy, and its very first connection lands. The full file: services: app: image: alpine:3.20 depends_on: db: condition: service_healthy restart: true command: > sh -c 'i=0; while [ $$i -lt 30 ]; do i=$$((i+1)); if nc -z -w 1 db 5432 2>/dev/null; then echo "ATTEMPT $$i: CONNECTED"; exit 0; else echo "ATTEMPT $$i: CONNECTION-REFUSED"; fi; sleep 0.5; done; echo "GAVE UP"' db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: example volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"] interval: 2s timeout: 3s retries: 10 start_period: 10s volumes: pgdata: The three edits, one line each. condition: service_healthy makes Compose wait for the healthcheck verdict instead of container start, the gate. restart: true re-runs that gate when you explicitly restart the database, so the app is not left pointing at stale connections after a docker compose restart db. And -h 127.0.0.1 forces the probe onto TCP, where the temporary initialization server cannot answer it, the honest probe. The timing knobs around it matter too, start_period especially: they give a slow first boot room to finish before failures start counting. On a cold volume my database needed about seven seconds, so 10 seconds of grace is the floor I would ship, and CI on shared runners gets more. Keep the application-side retry anyway. The gate protects the startup sequence only. If Postgres restarts two hours later, nothing re-runs the gate for you, a limit I pushed on in the exit-0 OOM post from the other angle: a green healthcheck reports its own probe, not your app's reality. How do you review agent YAML in 60 seconds? You do not need to run the stack to catch this class of bug. Read the diff against the current spec, the same way you would review a human's PR. Check the dependency wiring first. A bare depends_on: - db list means no waiting; it needs the long form with condition: service_healthy to gate startup. Check the probe next, because this is the part everyone trusts. pg_isready with no -h flag is a socket probe; on the Postgres image that can pass during initialization. It needs -h 127.0.0.1 (or the service name, if you want the probe to mirror app traffic) to test the path your app actually uses. Check the timing budget after that: no start_period means a slow first boot spends its failure budget before the database had a chance. CI runners are slower than your laptop, so give the budget headroom there. And check what happens after startup: no restart: true on the dependency means a deliberate database restart leaves the app on stale connections. Then fix the input, not just the output. The prompt that produces current YAML is a prompt with current references in it. Paste the startup-order doc and the services reference links into the request, and say "gate the app on service_healthy with a TCP probe" instead of "write me a compose file with Postgres". The model still carries the old corpus, and it will still try the 2021 shape when you let it guess. But it does not argue with a pasted spec. The measured version of this instinct is the ReCode result: outdated API knowledge persists "even with access to current documentation" (arXiv:2506.20495). The documentation you paste is doing real work, and the review pass you run after is doing the rest. One more habit that pays for itself: cold-boot the result before you merge it. On a scratch project, docker compose down -v && docker compose up --wait is a 30-second test. It reproduces exactly the run that fails in CI, and --wait turns the healthcheck into the pass/fail signal instead of a grepped log line. FAQ Doesn't restart: unless-stopped already handle this? It hides it. The app crash-loops until the database happens to be ready, then looks healthy. The race is still there; you just stopped being able to see it. Does the long depends_on form without condition wait for readiness? No. Without condition: service_healthy it is equivalent to the short list: ordering only. Is docker compose up --wait enough for CI? Yes, and it is the right tool: it blocks until healthchecked services report healthy. It does not remove the need for the gate on the app service itself, or for an honest probe. Do I still need app-level retries if the gate works? Yes. The gate runs at startup only. A connection that drops an hour later is the application's problem, and the same retry-with-backoff loop you already write for production networks covers it. Where does this leave the agent? The agent is not wrong that the file works. It worked on the machine it had, with the volume it had, and the corpus it was trained on says that file is what a compose file looks like. The spec moved, the tutorials didn't, and the average of all that 2021 YAML is what comes out when you ask for 2026 infrastructure. The three-line diff between the inherited shape and the current one is small, it is mechanical, and it is exactly the kind of review you were going to do anyway on a file nobody typed by hand. What is the oldest line of config an agent has handed you that you only caught because it failed somewhere strange? I am collecting these for a follow-up, and the comments are where I will read them. Verified against Docker Engine 29.1.3 and Docker Compose v5.1.4 on 2026-10-29, with three cold-start runs on fresh volumes; every timestamp and probe result above is from docker inspect or the Postgres log of those runs. Claims about model behavior are cited to the linked papers, and anything I could not reproduce myself is not in this post. Sources Every claim above traces to these primary sources. Where I could not verify something hands-on, the post says so. - Hero artwork: a robotic arm stamping green checkmarks on a conveyor belt of identical gene - Diagram artwork: dated tutorial books flow into a glowing model core that outputs a compose file card - Docker docs - docker-library/postgres issue #146 - arXiv:2506.20495 - arXiv:2511.21022 - compose-file services reference - docker-library postgres entrypoint - GitHub About the author Kunal Das writes operations engineering notes as he learns them, always against primary sources. Contact: GitHub and dev.to. Follow the Docker tag for the next posts in this series.

    1 reaction
  6. Seems like this situation is about to change. Thank you Ruby core team for your hard work!!! official-images#22616

    31 likes · 2,035 views
  7. 【応急処置】PHP 7.4 のまま CVE-2026-109618 / 109619(RCE)を塞ぐ Docker イメージを公開しました はじめに 2026-10-21、PHP プロジェクトが 認証なしでリモートコード実行(RCE)が可能な脆弱性を 2 件 公表しました(php.net のアナウンス)。 - CVE-2026-109618(GHSA-w8hw-w9fx-j79w): メモリ不足(OOM)の処理が、内部のリクエストデータの初期化途中で PHP を中断させ、壊れたデータがクリーンアップ時に正しいデータとして扱われる - CVE-2026-109619(GHSA-6wmf-4rv5-p3rp): 解放済みのメモリを指すリクエスト用の内部状態が残り、同じワーカーが処理する次のリクエストでそれが使われる 影響を受けるのは、Web リクエストを処理するすべての PHP(PHP-FPM、Apache mod_php、php-cgi など)です。advisory は mod_php のリスクが最も高い としており、アプリケーションのコードが動く前に攻撃が成立する実証コードもあるとしています。 修正版が出たのは PHP 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 だけです。PHP 7.4 は EOL なので修正版は出ません。ただ、深刻度を考慮して、PHP セキュリティチームが 7.4 / 8.0 / 8.1 向けに 最小限のソース backport を公開しています。 一方で、Docker の公式イメージ php:7.4-* が再ビルドされることはありません。FROM php:7.4-apache のまま運用しているシステムは、何もしなければ脆弱なまま です。 そこで、この backport を当てて再ビルドした php:7.4-* 互換のイメージを OSS として公開しました。 GitHub: softjapan/php74-cve-patched softjapan/php74-cve-patched :::note alert このイメージは「応急処置」です。PHP 7.4 を安全なバージョンにするものではありません。 - 直るのは CVE-2026-109618 と CVE-2026-109619 の 2 件だけ です。7.4.33 より後に見つかったほかの問題は直っていません。 - ベース OS の Debian 11 (bullseye) は 2026-08 に LTS が終わっています。OpenSSL や Apache などの OS パッケージは今後更新されません。 - PHP プロジェクトや Docker 社の公式イメージではありません。 恒久的な対策は PHP 8.2 以降への移行 です。このイメージは、移行までの時間を稼ぐために使ってください。 ::: TL;DR - FROM php:7.4-apache + FROM ghcr.io/softjapan/php74-cve-patched:7.4-apache 1. FROM を 1 行差し替える(apache / fpm / cli、amd64 / arm64) 2. イメージを再ビルド・再デプロイし、PHP のワーカーをすべて再起動する 3. check-patched.sh で修正が入ったことを確認する 4. PHP 8.2 以降への移行計画を立てる(ここまでやって対応完了です) まず、自分の環境が脆弱か確認する 困ったことに、backport を当てても php -v は 7.4.33 のまま です。バージョン番号を見ても、対策済みかどうかは分かりません。 そこで、PHP のバイナリの中身を調べて判定するスクリプトを用意しました。 curl -fsSLO https://raw.githubusercontent.com/softjapan/php74-cve-patched/main/scripts/check-patched.sh sh check-patched.sh php:7.4-apache # イメージ sh check-patched.sh --container my-web-1 # 起動中のコンテナ sh check-patched.sh --local # このマシン(VM や物理サーバー) 公式の php:7.4-apache を調べた結果です。 Image: php:7.4-apache PHP version: 7.4.33 VULNERABLE /usr/lib/apache2/modules/libphp7.so VULNERABLE /usr/local/bin/php RESULT: VULNERABLE to CVE-2026-109618 / CVE-2026-109619 パッチを当てたイメージではこうなります。 Image: ghcr.io/softjapan/php74-cve-patched:7.4-apache PHP version: 7.4.33 patched /usr/lib/apache2/modules/libphp7.so patched /usr/local/bin/php RESULT: PATCHED (backport found in every PHP binary) - 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 以降は修正済みと判定します。それより古いバージョンは、backport で追加される関数 zend_new_array_singleton がバイナリにあるかどうかで判定します。 - CLI だけでなく、実際にリクエストを処理する libphp7.so(mod_php)や php-fpm も個別に調べます。CLI だけ直っていて Apache が古いまま、という状態も見つけられます。 - 終了コードは 0 が修正済み、1 が脆弱です。CI に組み込んで、脆弱なイメージをデプロイしないようにもできます。 :::note info この判定は簡易的なものです。ディストリビューションや LTS ベンダーが独自の方法で backport したパッケージは、「脆弱」と判定されることがあります。その場合はベンダーの情報を確認してください。 ::: 使い方 同じイメージを GHCR と Docker Hub の両方で公開しています。タグもダイジェストも同じです。 | 公式イメージ | GHCR | Docker Hub | |---|---|---| | php:7.4-apache | ghcr.io/softjapan/php74-cve-patched:7.4-apache | softjpn/php74-cve-patched:7.4-apache | | php:7.4-fpm | ghcr.io/softjapan/php74-cve-patched:7.4-fpm | softjpn/php74-cve-patched:7.4-fpm | | php:7.4-cli | ghcr.io/softjapan/php74-cve-patched:7.4-cli | softjpn/php74-cve-patched:7.4-cli | 公式イメージと同じ使い方ができることを重視しています。 - docker-php-ext-install / docker-php-ext-configure / pecl / php.ini-production がそのまま使えます - php -m の拡張モジュール一覧が公式イメージと同じであることを CI で確認しています - apt-get install も使えます(apt の参照先を archive.debian.org に変えてあります) Laravel 向けの例です。 FROM ghcr.io/softjapan/php74-cve-patched:7.4-apache RUN apt-get update && apt-get install -y --no-install-recommends \ libzip-dev libpng-dev libjpeg-dev libfreetype6-dev \ && rm -rf /var/lib/apt/lists/* \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j"$(nproc)" gd pdo_mysql zip bcmath opcache \ && a2enmod rewrite :::note alert イメージを差し替えただけでは終わりません。 実行中のコンテナやワーカーは古いバイナリのまま動いています。再ビルド・再デプロイして、PHP のワーカーをすべて再起動してください。最後に check-patched.sh --container <名前> で、動いているコンテナ が修正済みかを確認してください。 ::: 何を変えたのか 変更は最小限にしています。docker-library/php に残っている 最後の PHP 7.4 用 Dockerfile(commit 1361ba95) を元に、変えたのは次の 3 点だけです。 1. apt の参照先を archive.debian.org に変更: bullseye は通常のミラーから消えたため、apt-get update が 404 になります 2. PHP 7.4.33 のソースに backport を適用: patch -p1 --fuzz=0 で当てるので、少しでもずれていればビルドが失敗します 3. 修正が入ったことをビルド中に確認: php、php-fpm、libphp7.so のどれか 1 つでも修正が入っていなければ、ビルドを失敗させます 当てているパッチは、上流の php/php-src@712e60f と バイト単位で同一 です。CI が push のたびと毎週、次の点を確認しています。 - 同梱のパッチが上流の commit と同一であること - 上流の backport ブランチに新しい commit が入っていないこと(入っていれば CI が失敗して知らせます) 当初は社内で php:7.4-apache の上から ./configure をやり直す方法をとっていました。しかしこの方法には、configure のオプションが x86_64 向けに固定される、fpm / cli 版でオプションがずれる、といった問題がありました。公開版では公式の Dockerfile そのものを元にして、マルチアーキテクチャへの対応と公式との互換性を両立しています。 信頼して使うために セキュリティ目的のイメージを、知らない人が配っているものから取ってくるのは不安だと思います。そのため、検証できる材料をそろえました。 - イメージはリポジトリの GitHub Actions だけでビルドし、SBOM と provenance を付けています - cosign(keyless)で署名しています cosign verify ghcr.io/softjapan/php74-cve-patched:7.4-apache \ --certificate-identity-regexp '^https://github.com/softjapan/php74-cve-patched/' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com - 本番環境ではダイジェスト(@sha256:...)で固定してください - このイメージを使わず、自分でビルドする手順 も公開しています。Docker を使わずソースからビルドしている環境向けに、patch -p1 で当てる手順も載せています イメージをすぐ差し替えられない場合の緩和策 advisory には、すぐに更新できない場合の緩和策も載っています。どれも攻撃の経路を一部ふさぐだけで、安全になるわけではない と明記されていますが、差し替えまでのつなぎや多層防御として使えます。 CVE-2026-109618(OOM による状態破損) - memory_limit=-1 を、アプリケーションが上書きできない方法(FPM プールや Apache の php_admin_value)で設定する。どこかの設定で有限の値に戻されていないかも確認する - その代わり、コンテナや cgroup のメモリ制限で、影響を受けたワーカーだけが止まるようにしておく - RLIMIT_AS / RLIMIT_DATA での代用はしない(脆弱な経路を逆に通る原因になる) - post_max_size を小さくする。アップロードがないなら 8M、あるなら 32M。upload_max_filesize は post_max_size より小さくする - Web サーバーやプロキシでも、リクエストサイズ、フィールド数、マルチパートの数、ヘッダー数などを制限する。Content-Length がないリクエストも対象に含める CVE-2026-109619(古いグローバル状態) - session.upload_progress.enabled=0 を設定し、ワーカーをすべて再起動する - php.ini、FPM プール、サーバー設定など、リクエストの解析前に効く場所で設定する。ini_set() や .user.ini では効果がない - 初期化前に警告を出すような、無効な設定や非推奨の設定をリクエストごとに適用しているなら取り除く(@ や display_errors で表示を隠しても意味はない) - 攻撃者が起こせる fatal error やタイムアウトを修正する このイメージには、上記の一部(session.upload_progress.enabled = 0 と、小さめの post_max_size / upload_max_filesize)をまとめた設定ファイルを同梱しています。デフォルトでは無効 です。PHP の挙動は公式イメージと変えていないので、使う場合は次の 1 行を追加してください。 RUN cp /usr/local/share/php74-cve-patched/zz-cve-2026-mitigations.ini "$PHP_INI_DIR/conf.d/" 応急処置のあとにやること 最後にもう一度書いておきます。このイメージは止血であって、治療ではありません。 - [ ] すべての PHP 7.4 環境を洗い出し、check-patched.sh で確認した - [ ] イメージを差し替え、再デプロイし、ワーカーを再起動した - [ ] 動いているコンテナ で check-patched.sh --container が PATCHED になった - [ ] 必要に応じて緩和策の設定も入れた - [ ] PHP 8.2 以降への移行計画と期限を決めた(8.2 も 2026-12-31 に EOL になるので、8.3 以降も検討する) - [ ] OS(Debian 11)も更新されないことを、関係者と共有した まとめ - CVE-2026-109618 / 109619 は、認証なしの RCE です。特に mod_php は最優先で対応してください - PHP 7.4 の公式 Docker イメージは今後も直りません。php:7.4-* を使っていれば脆弱なままです - php74-cve-patched は、FROM を 1 行変えるだけで公式の backport を取り込める 応急処置 です - 恒久的な対策は PHP 8.2 以降への移行です PHP 8.0 / 8.1 版(こちらも公式の backport があります)や Alpine 版も検討しています。必要な方は Issue で教えてください。この記事が役に立ったら、Qiita の LGTM や GitHub の ⭐ をもらえるとうれしいです。同じように困っている人に見つけてもらいやすくなります。 softjapan/php74-cve-patched 参考 - PHP: News Archive 2026(2026-10-21 Security advisory) - GHSA-w8hw-w9fx-j79w(CVE-2026-109618) - GHSA-6wmf-4rv5-p3rp(CVE-2026-109619) - php-src: PHP-7.4-CVE-2026-109618-109619 ブランチ

  8. 【応急処置】PHP 7.4 のまま CVE-2026-109618 / 109619(RCE)を塞ぐ Docker イメージを公開しました はじめに 2026-10-21、PHP プロジェクトが 認証なしでリモートコード実行(RCE)が可能な脆弱性を 2 件 公表しました(php.net のアナウンス)。 CVE-2026-109618(GHSA-w8hw-w9fx-j79w): メモリ不足(OOM)の処理が、内部のリクエストデータの初期化途中で PHP を中断させ、壊れたデータがクリーンアップ時に正しいデータとして扱われる CVE-2026-109619(GHSA-6wmf-4rv5-p3rp): 解放済みのメモリを指すリクエスト用の内部状態が残り、同じワーカーが処理する次のリクエストでそれが使われる 影響を受けるのは、Web リクエストを処理するすべての PHP(PHP-FPM、Apache mod_php、php-cgi など)です。advisory は mod_php のリスクが最も高い としており、アプリケーションのコードが動く前に攻撃が成立する実証コードもあるとしています。 修正版が出たのは PHP 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 だけです。PHP 7.4 は EOL なので修正版は出ません。ただ、深刻度を考慮して、PHP セキュリティチームが 7.4 / 8.0 / 8.1 向けに 最小限のソース backport を公開しています。 一方で、Docker の公式イメージ php:7.4-* が再ビルドされることはありません。FROM php:7.4-apache のまま運用しているシステムは、何もしなければ脆弱なまま です。 そこで、この backport を当てて再ビルドした php:7.4-* 互換のイメージを OSS として公開しました。 softjapan/php74-cve-patched ! このイメージは「応急処置」です。PHP 7.4 を安全なバージョンにするものではありません。 直るのは CVE-2026-109618 と CVE-2026-109619 の 2 件だけ です。7.4.33 より後に見つかったほかの問題は直っていません。 ベース OS の Debian 11 (bullseye) は 2026-08 に LTS が終わっています。OpenSSL や Apache などの OS パッケージは今後更新されません。 PHP プロジェクトや Docker 社の公式イメージではありません。 恒久的な対策は PHP 8.2 以降への移行 です。このイメージは、移行までの時間を稼ぐために使ってください。 TL;DR - FROM php:7.4-apache - FROM ghcr.io/softjapan/php74-cve-patched:7.4-apache FROM を 1 行差し替える(apache / fpm / cli、amd64 / arm64) イメージを再ビルド・再デプロイし、PHP のワーカーをすべて再起動する check-patched.sh で修正が入ったことを確認する PHP 8.2 以降への移行計画を立てる(ここまでやって対応完了です) まず、自分の環境が脆弱か確認する 困ったことに、backport を当てても php -v は 7.4.33 のまま です。バージョン番号を見ても、対策済みかどうかは分かりません。 そこで、PHP のバイナリの中身を調べて判定するスクリプトを用意しました。 curl -fsSLO raw.githubusercontent.com sh check-patched.sh php:7.4-apache # イメージ sh check-patched.sh --container my-web-1 # 起動中のコンテナ sh check-patched.sh --local # このマシン(VM や物理サーバー) 公式の php:7.4-apache を調べた結果です。 Image: php:7.4-apache PHP version: 7.4.33 VULNERABLE /usr/lib/apache2/modules/libphp7.so VULNERABLE /usr/local/bin/php RESULT: VULNERABLE to CVE-2026-109618 / CVE-2026-109619 パッチを当てたイメージではこうなります。 Image: ghcr.io/softjapan/php74-cve-patched:7.4-apache PHP version: 7.4.33 patched /usr/lib/apache2/modules/libphp7.so patched /usr/local/bin/php RESULT: PATCHED (backport found in every PHP binary) 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 以降は修正済みと判定します。それより古いバージョンは、backport で追加される関数 zend_new_array_singleton がバイナリにあるかどうかで判定します。 CLI だけでなく、実際にリクエストを処理する libphp7.so(mod_php)や php-fpm も個別に調べます。CLI だけ直っていて Apache が古いまま、という状態も見つけられます。 終了コードは 0 が修正済み、1 が脆弱です。CI に組み込んで、脆弱なイメージをデプロイしないようにもできます。 ! この判定は簡易的なものです。ディストリビューションや LTS ベンダーが独自の方法で backport したパッケージは、「脆弱」と判定されることがあります。その場合はベンダーの情報を確認してください。 使い方 同じイメージを GHCR と Docker Hub の両方で公開しています。タグもダイジェストも同じです。 公式イメージ GHCR Docker Hub php:7.4-apache ghcr.io/softjapan/php74-cve-patched:7.4-apache softjpn/php74-cve-patched:7.4-apache php:7.4-fpm ghcr.io/softjapan/php74-cve-patched:7.4-fpm softjpn/php74-cve-patched:7.4-fpm php:7.4-cli ghcr.io/softjapan/php74-cve-patched:7.4-cli softjpn/php74-cve-patched:7.4-cli 公式イメージと同じ使い方ができることを重視しています。 docker-php-ext-install / docker-php-ext-configure / pecl / php.ini-production がそのまま使えます php -m の拡張モジュール一覧が公式イメージと同じであることを CI で確認しています apt-get install も使えます(apt の参照先を archive.debian.org に変えてあります) Laravel 向けの例です。 FROM ghcr.io/softjapan/php74-cve-patched:7.4-apache RUN apt-get update && apt-get install -y --no-install-recommends \ libzip-dev libpng-dev libjpeg-dev libfreetype6-dev \ && rm -rf /var/lib/apt/lists/* \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j"$(nproc)" gd pdo_mysql zip bcmath opcache \ && a2enmod rewrite ! イメージを差し替えただけでは終わりません。 実行中のコンテナやワーカーは古いバイナリのまま動いています。再ビルド・再デプロイして、PHP のワーカーをすべて再起動してください。最後に check-patched.sh --container <名前> で、動いているコンテナ が修正済みかを確認してください。 何を変えたのか 変更は最小限にしています。docker-library/php に残っている 最後の PHP 7.4 用 Dockerfile(commit 1361ba95) を元に、変えたのは次の 3 点だけです。 apt の参照先を archive.debian.org に変更: bullseye は通常のミラーから消えたため、apt-get update が 404 になります PHP 7.4.33 のソースに backport を適用: patch -p1 --fuzz=0 で当てるので、少しでもずれていればビルドが失敗します 修正が入ったことをビルド中に確認: php、php-fpm、libphp7.so のどれか 1 つでも修正が入っていなければ、ビルドを失敗させます 当てているパッチは、上流の php/php-src@712e60f と バイト単位で同一 です。CI が push のたびと毎週、次の点を確認しています。 同梱のパッチが上流の commit と同一であること 上流の backport ブランチに新しい commit が入っていないこと(入っていれば CI が失敗して知らせます) 当初は社内で php:7.4-apache の上から ./configure をやり直す方法をとっていました。しかしこの方法には、configure のオプションが x86_64 向けに固定される、fpm / cli 版でオプションがずれる、といった問題がありました。公開版では公式の Dockerfile そのものを元にして、マルチアーキテクチャへの対応と公式との互換性を両立しています。 信頼して使うために セキュリティ目的のイメージを、知らない人が配っているものから取ってくるのは不安だと思います。そのため、検証できる材料をそろえました。 イメージはリポジトリの GitHub Actions だけでビルドし、SBOM と provenance を付けています cosign(keyless)で署名しています cosign verify ghcr.io/softjapan/php74-cve-patched:7.4-apache \ --certificate-identity-regexp '^softjapan/php74-cve-patched' \ --certificate-oidc-issuer token.actions.githubusercontent.com 本番環境ではダイジェスト(@sha256:...)で固定してください このイメージを使わず、自分でビルドする手順 も公開しています。Docker を使わずソースからビルドしている環境向けに、patch -p1 で当てる手順も載せています イメージをすぐ差し替えられない場合の緩和策 advisory には、すぐに更新できない場合の緩和策も載っています。どれも攻撃の経路を一部ふさぐだけで、安全になるわけではない と明記されていますが、差し替えまでのつなぎや多層防御として使えます。 CVE-2026-109618(OOM による状態破損) memory_limit=-1 を、アプリケーションが上書きできない方法(FPM プールや Apache の php_admin_value)で設定する。どこかの設定で有限の値に戻されていないかも確認する その代わり、コンテナや cgroup のメモリ制限で、影響を受けたワーカーだけが止まるようにしておく RLIMIT_AS / RLIMIT_DATA での代用はしない(脆弱な経路を逆に通る原因になる) post_max_size を小さくする。アップロードがないなら 8M、あるなら 32M。upload_max_filesize は post_max_size より小さくする Web サーバーやプロキシでも、リクエストサイズ、フィールド数、マルチパートの数、ヘッダー数などを制限する。Content-Length がないリクエストも対象に含める CVE-2026-109619(古いグローバル状態) session.upload_progress.enabled=0 を設定し、ワーカーをすべて再起動する php.ini、FPM プール、サーバー設定など、リクエストの解析前に効く場所で設定する。ini_set() や .user.ini では効果がない 初期化前に警告を出すような、無効な設定や非推奨の設定をリクエストごとに適用しているなら取り除く(@ や display_errors で表示を隠しても意味はない) 攻撃者が起こせる fatal error やタイムアウトを修正する このイメージには、上記の一部(session.upload_progress.enabled = 0 と、小さめの post_max_size / upload_max_filesize)をまとめた設定ファイルを同梱しています。デフォルトでは無効 です。PHP の挙動は公式イメージと変えていないので、使う場合は次の 1 行を追加してください。 RUN cp /usr/local/share/php74-cve-patched/zz-cve-2026-mitigations.ini "$PHP_INI_DIR/conf.d/" 応急処置のあとにやること 最後にもう一度書いておきます。このイメージは止血であって、治療ではありません。 すべての PHP 7.4 環境を洗い出し、check-patched.sh で確認した イメージを差し替え、再デプロイし、ワーカーを再起動した 動いているコンテナ で check-patched.sh --container が PATCHED になった 必要に応じて緩和策の設定も入れた PHP 8.2 以降への移行計画と期限を決めた(8.2 も 2026-12-31 に EOL になるので、8.3 以降も検討する) OS(Debian 11)も更新されないことを、関係者と共有した まとめ CVE-2026-109618 / 109619 は、認証なしの RCE です。特に mod_php は最優先で対応してください PHP 7.4 の公式 Docker イメージは今後も直りません。php:7.4-* を使っていれば脆弱なままです php74-cve-patched は、FROM を 1 行変えるだけで公式の backport を取り込める 応急処置 です 恒久的な対策は PHP 8.2 以降への移行です PHP 8.0 / 8.1 版(こちらも公式の backport があります)や Alpine 版も検討しています。必要な方は Issue で教えてください。役に立ったら GitHub で ⭐ をもらえるとうれしいです。同じように困っている人に見つけてもらいやすくなります。 softjapan/php74-cve-patched 参考 PHP: News Archive 2026(2026-10-21 Security advisory) GHSA-w8hw-w9fx-j79w(CVE-2026-109618) GHSA-6wmf-4rv5-p3rp(CVE-2026-109619) php-src: PHP-7.4-CVE-2026-109618-109619 ブランチ

  9. :obama_phew: ``` # php -v PHP 8.5.12 (cli) (built: Oct 21 2026 15:49:40) (NTS) Copyright (c) The PHP Group Built by docker-library/php Zend Engine v4.5.12, Copyright (c) Zend Technologies with Zend OPcache v8.5.12, Copyright (c), by Zend Technologies ```

    1 like
  10. Understood, thanks for the reply. I would again suggest that maybe CPython should release versions of 3.11 and 3.12 that either do not bundle pip, bundle a newer version of pip, or bundle a fixed minor release of pip created by the CPython team. I think it's only two versions that need to change. Given that uv seems to have fixed this in their builds, Debian has of course fixed it with their patches to use the system trust store, and the Docker image folks seem amenable to potentially copying the debian approach, I can't say this absolutely critical, but it does seem like it would be a better default than shipping a version of pip that is trivially vulnerable to MITM. Just for fun, I vibed up the intercepting server for about $0.30 in spend on a Chinese model. I wrote an example on this post: python#1142

    2 likes · 1 comment
Trending awards, every completed day and week
Gold
01st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
0Owned by docker-library now

No trending awards yet

None of the repositories docker-library owns has finished in the top three of a completed trending day or week, by stars, forks or PR authors. See who is trending now.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.