Docker per PHP in produzione: una guida pratica all'installazione di applicazioni aziendali
Docker fa sì che un'applicazione PHP venga eseguita allo stesso modo su un laptop, in fase di test e in produzione, e rende i rilasci e i rollback routine invece che stressanti. Aggiunge anche parti mobili e una configurazione imprudente è peggiore di un semplice server. Questa guida mostra una linea di base pronta per la produzione con la configurazione reale e quando non dovresti preoccuparti.
In breve
- Docker vale la pena quando desideri ambienti identici, rollback rapidi e onboarding semplice. Per un singolo piccolo sito di brochure su hosting condiviso di solito non è necessario.
- Crea un'immagine immutabile in CI: build in più fasi, solo dipendenze di produzione, utente non root, OPcache attiva, errori registrati e mai visualizzati.
- Rilascia tramite tag immagine (l'hash del commit Git), controlla l'integrità dopo l'avvio e ripristina automaticamente il tag precedente se il controllo fallisce.
- I contenitori sono usa e getta, i dati no: conserva il database e i caricamenti in volumi o servizi gestiti ed eseguine il backup all'esterno Docker.
- Gli errori di produzione più comuni: segreti nell'immagine, porte del database pubblicato, log illimitati e limiti php-fpm che non corrispondono alla memoria di cui disponi.
Per molte applicazioni aziendali il vero costo dell'hosting non è il server ma le persone. Una configurazione comprensibile solo da uno sviluppatore, una distribuzione eseguita manualmente tramite FTP e un ambiente di staging che si comporta in modo diverso dalla produzione sono tutti costosi nel lungo periodo. I contenitori risolvono questo problema impacchettando l'applicazione con la sua esatta versione PHP e le sue estensioni. Eseguiamo i nostri siti PHP e sistemi client su uno stack basato su Docker e questa guida descrive la linea di base da cui partiamo.
Quando Docker ne vale la pena e quando no
- Ne vale la pena: diversi servizi (web, PHP, database, coda), più di uno sviluppatore, necessità di rilasci ripetibili e rollback rapidi o un piano per spostarsi tra fornitori.
- Non necessario: un sito con contenuti semplici che cambia raramente su hosting gestito. La maggiore complessità richiede un profitto.
- Non sostituisce le buone operazioni: sono comunque necessari patch, backup e monitoraggio. Consulta la nostra guida al backup e al monitoraggio.
L'architettura di destinazione
Una configurazione solida e semplice per un'applicazione PHP su un server:
- Proxy inverso e TLS all'edge ( Nginx o Caddy sull'host o un CDN come Cloudflare, consulta la nostra guida alla configurazione Cloudflare).
- Contenitore Web ( Nginx) che serve file statici e trasmette richieste PHP.
- Contenitore PHP-FPM con il tuo codice, integrato nell'immagine.
- Database nel proprio contenitore con un volume persistente, o meglio un servizio di database gestito per i dati critici.
- Volumi solo per i dati che devono sopravvivere: file di database e caricamenti degli utenti.
Un Dockerfile a più fasi
Una build in più fasi installa le dipendenze in una fase e copia solo il risultato in un'immagine runtime ridotta. L'immagine finale non ha Composer, compilatori o strumenti di test, il che la rende più piccola e offre agli aggressori meno risorse da utilizzare:
# ---- stage 1: PHP dependencies ----
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction --optimize-autoloader
# ---- stage 2: runtime ----
FROM php:8.3-fpm-alpine
RUN apk add --no-cache icu-libs libzip \
&& apk add --no-cache --virtual .build icu-dev libzip-dev $PHPIZE_DEPS \
&& docker-php-ext-install -j"$(nproc)" pdo_mysql intl zip opcache \
&& apk del .build
COPY docker/php.ini /usr/local/etc/php/conf.d/zz-app.ini
COPY docker/www.conf /usr/local/etc/php-fpm.d/zz-www.conf
WORKDIR /var/www/app
COPY --from=vendor /app/vendor ./vendor
COPY --chown=www-data:www-data . .
USER www-data
EXPOSE 9000
Scegli deliberatamente la versione PHP e verifica che sia ancora supportata. La nostra guida all'aggiornamento PHP 8 spiega come pianificarlo. Blocca le versioni delle immagini di base e aggiornale in base a una pianificazione, poiché un'immagine che non ricostruisci mai accumula vulnerabilità note. Mantieni un .dockerignore che escluda .git, .env, test e dati locali, in modo che nell'immagine non finisca nulla di sensibile.
Configurazione PHP di produzione e php-fpm
# docker/php.ini
expose_php = 0
display_errors = 0
log_errors = 1
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 12M
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0 ; the code in an image never changes while it runs
# docker/www.conf
[www]
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
request_terminate_timeout = 30s
catch_workers_output = yes
decorate_workers_output = no
L'impostazione di opcache.validate_timestamps = 0 offre le migliori prestazioni per un'immagine immutabile, ma funziona solo se non modifichi mai i file all'interno di un contenitore in esecuzione: rilasci invece una nuova immagine. Dimensiona il pool partendo da numeri reali: misura la memoria media di un PHP lavoratore sotto carico, decidi quanta memoria PHP può utilizzare sul server e dividi. Se un lavoratore utilizza 60 MB e puoi dare a PHP 1 GB, pm.max_children è circa 16. Impostandolo su un valore molto più alto fa semplicemente scambiare il server o interrompere i processi del kernel durante un picco di traffico. Troppo basso e richieste in coda. Controlla il registro php-fpm per gli avvisi "max_children raggiunti".
Un file Componi con le giuste impostazioni predefinite
x-logging: &default-logging
driver: json-file
options: { max-size: "10m", max-file: "5" } # logs cannot fill the disk
services:
web:
image: ghcr.io/your-org/app-web:${TAG}
ports: ["127.0.0.1:8080:80"] # only the local reverse proxy can reach it
depends_on: [php]
restart: unless-stopped
logging: *default-logging
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/health.php"]
interval: 15s
timeout: 3s
retries: 3
php:
image: ghcr.io/your-org/app:${TAG}
env_file: .env # secrets stay on the server, not in the image
restart: unless-stopped
read_only: true
tmpfs: [/tmp]
mem_limit: 512m
volumes: [uploads:/var/www/app/storage/uploads]
logging: *default-logging
db:
image: mysql:8.4
env_file: .env.db
restart: unless-stopped
volumes: [db_data:/var/lib/mysql] # no "ports:" - the database is not public
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
interval: 15s
retries: 5
logging: *default-logging
volumes:
uploads:
db_data:
L'immagine web è una piccola immagine Nginx con la cartella public/ e la configurazione del sito copiate, creata nella stessa pipeline dell'immagine PHP. Punti chiave: associare le porte pubblicate a 127.0.0.1, non pubblicare mai il database, impostare politiche di riavvio, limitare la memoria in modo che un contenitore non possa rimuovere l'host e limitare la dimensione del registro. Ricorda che le porte pubblicate da Docker possono aggirare le regole del firewall dell'host, come spieghiamo nella guida Cloudflare. Non montare mai il socket Docker in un contenitore dell'applicazione, poiché fornisce a quel contenitore il controllo sull'host.
Rilasci e rollback
La caratteristica migliore di una configurazione basata su immagini è che ogni versione è un tag e un rollback è "avvia il tag precedente". Tagga le immagini con l'hash di commit Git, non fare mai affidamento su latest e fai in modo che lo script di rilascio verifichi l'integrità:
#!/usr/bin/env bash
# /srv/app/release.sh <git-sha>
set -euo pipefail
TAG="${1:?usage: release.sh <git-sha>}"
cd /srv/app
PREV="$(cat .current_tag 2>/dev/null || true)"
export TAG
docker compose pull php web
docker compose up -d --no-deps php web
for i in $(seq 1 30); do
if curl -fsS http://127.0.0.1:8080/health.php > /dev/null; then
echo "$TAG" > .current_tag
echo "healthy: $TAG"; exit 0
fi
sleep 2
done
echo "unhealthy after release, rolling back to ${PREV:-none}" >&2
if [ -n "$PREV" ]; then TAG="$PREV" docker compose up -d --no-deps php web; fi
exit 1
Le modifiche al database richiedono attenzione: scrivi le migrazioni in modo che la versione precedente del codice funzioni ancora con il nuovo schema (aggiungi prima le colonne, rimuovile in una versione successiva). Questo è ciò che rende sicuro un rapido rollback. Eseguire un backup del database prima di qualsiasi rilascio che modifichi lo schema.
Costruzione e spedizione da CI
Crea l'immagine una volta in CI e distribuisci esattamente quell'artefatto. Un flusso di lavoro minimo GitHub Actions:
name: build-and-release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/your-org/app:${{ github.sha }}
- name: Release over SSH
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
install -m 600 /dev/null key && echo "$DEPLOY_KEY" > key
ssh -i key -o StrictHostKeyChecking=accept-new deploy@your-server "/srv/app/release.sh ${{ github.sha }}"
Esegui i test e l'analisi statica nella stessa pipeline prima che l'immagine venga creata e utilizza una chiave di distribuzione che possa eseguire solo lo script di rilascio. Aggiungi una scansione della vulnerabilità dell'immagine se il tuo profilo di rischio lo richiede.
Log, dati e sicurezza
- I log vanno su stdout e stderr, vengono ruotati da Docker e, cosa importante, spediti in un luogo centrale in modo che sopravvivano al contenitore.
- I dati vivono fuori dal contenitore. Un volume non è un backup. Utilizza l'approccio descritto nella nostra guida al backup: crittografato, off-site, con ripristino testato.
- I segreti non vengono mai inseriti nelle immagini. Passali in fase di esecuzione tramite file env o un gestore di segreti e tienili fuori da Git.
- Esegui come utente non root, utilizza un filesystem di sola lettura ove possibile e rilascia ciò che non ti serve.
- Mantieni le immagini fresche. Ricostruisci regolarmente per acquisire correzioni di sicurezza nell'immagine di base e PHP.
- Applica i controlli a livello di applicazione dalla nostra checklist di sicurezza PHP, poiché i contenitori non risolvono il codice non sicuro.
Una lista di controllo per la preparazione alla produzione
- Immagine a più fasi con solo dipendenze di produzione, immagine di base bloccata, utente non root.
- OPcache abilitato, errori registrati non visualizzati, dimensioni php-fpm dalla memoria misurata.
- Componi file con policy di riavvio, controlli di integrità, limiti di memoria, rotazione dei log e nessuna porta del database pubblico.
- Rilasci tramite tag commit-hash con controllo di integrità e rollback automatici.
- Backup e ripristino testato per il database e i caricamenti.
- Monitoraggio e avvisi su integrità, disco, errori e certificati.
Come possiamo aiutare
Containerizziamo le applicazioni PHP, impostiamo pipeline e rilasci CI e li colleghiamo a pagamenti, CRM e messaggistica. Consulta il nostro servizio di integrazioni o prenota una chiamata ed esamineremo il tuo attuale processo di hosting e rilascio.
Domande frequenti
Docker è utile per eseguire PHP in produzione?
Sì, se usato bene. Fornisce ambienti identici, versioni ripetibili e rollback rapidi. Necessita di una corretta configurazione: immagini immutabili, controlli di integrità, limiti delle risorse, rotazione dei log, gestione dei segreti e backup dei dati all'esterno del contenitore.
Il database dovrebbe essere eseguito in Docker in produzione?
Può essere utilizzato per sistemi di piccole e medie dimensioni se utilizza un volume persistente, non viene mai esposto pubblicamente e se sono stati testati i backup. Per i dati critici molti team preferiscono un servizio di database gestito, che gestisce il failover, gli aggiornamenti e i backup per te.
Come posso eseguire il rollback di una versione Docker ?
Tagga ogni immagine con l'hash del commit Git e mantieni il tag precedente. Per eseguire il rollback, avvia i contenitori con il tag precedente. Scrivi le migrazioni del database in modo che il codice precedente funzioni ancora con lo schema più recente.
Quanti lavoratori php-fpm dovrei configurare?
Misura la memoria media di un lavoratore sotto carico, decidi quanta memoria PHP può utilizzare e dividi. L'impostazione di un valore troppo alto provoca lo scambio, mentre un valore troppo basso provoca richieste in coda. Guarda il registro php-fpm per gli avvisi relativi a max_children.
Qual è il più grande errore di sicurezza Docker con le app PHP ?
Inserimento di segreti nell'immagine o in Git e pubblicazione delle porte del database su Internet. Evitare inoltre di eseguire come root, montare il socket Docker nei contenitori e non aggiornare mai l'immagine di base.
Chiamata conoscitiva gratuita
Portate uno stack disordinato o un'integrazione bloccata - Integrono mapperà la prima mossa più intelligente.
Ottenete una lettura senior su trappole di budget, dipendenze e se conviene comprare, assemblare o costruire - prima che parta il lavoro a pagamento. Il CEO resta nel loop.