Guide: technical checklist to prepare a Debian VPS with Docker Compose for PHP apps in production

By · Published on · 9 min read

Patch panel with fiber optic connectors and cables installed in a rack
Professional network infrastructure visual for server and VPS environments. Photo: Brett Sayles / Pexels

Technical checklist to prepare a Debian VPS with Docker Compose for PHP applications in production. This step‑by‑step covers sysctl, ulimits, journald, swap and cgroups. Apply the adjustments, validate with simple commands and reduce the risk of OOMs, lost logs and network or file‑descriptor bottlenecks.

What this checklist covers and when to use it

This checklist focuses on preparing the Debian host and Docker for PHP monoliths or PHP stacks running under Compose. It prioritizes stability, predictability and observability before fine‑tuning for performance.

Use it when provisioning a new VPS, migrating hosts or standardizing environments. It also helps reduce incidents from OOMs, logs without retention and system limits.

Apply these changes first in staging. Changes to the kernel, cgroups or the Docker service can affect all running containers.

  • Debian 11/12 host with systemd and Docker installed
  • Docker Compose v2 (docker compose CLI)
  • PHP stacks with separated services (web, php‑fpm, database, cache)

Kernel, systemd and Docker versions determine cgroups, logging and limits. On recent Debian releases, cgroup v2 is the default and Docker typically uses the systemd cgroup driver.

Check these items before proceeding. Ensure there is free space in /var for journald and that you have root privileges on the host.

  • Updated kernel and systemd with cgroup v2 enabled
  • Docker Engine 20.10+ and Compose v2
  • Root access to the host and free space in /var
# Verificar cgroup v2
stat -fc %T /sys/fs/cgroup  # esperado: cgroup2fs

# Verificar driver de cgroup do Docker
docker info | grep -i 'Cgroup Driver'  # esperado: systemd

# Versões
docker --version
docker compose version

Which sysctl settings to adjust on Debian for Docker/PHP?

A few kernel parameters help avoid queue saturation, ephemeral port exhaustion and connection limits. Configure them under /etc/sysctl.d for persistence and load them without rebooting.

If you see intermittent issues on the Docker bridge, investigate network queues and interfaces. See the Docker bridge troubleshooting guide for causes and fixes at Docker bridge drops connections.

The values below are a starting point. Tune them according to your host memory, traffic pattern and connection profile.

  • Enable forwarding for Docker networks
  • Increase socket/backlog and SYN backlog
  • Expand ephemeral port range for many outgoing connections
  • Size conntrack according to load and available memory
# /etc/sysctl.d/99-docker-php.conf
net.ipv4.ip_forward = 1
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.ip_local_port_range = 1024 65535

# Conntrack: comece moderado e ajuste com métricas
net.netfilter.nf_conntrack_max = 131072

# Aplicar agora
sysctl --system

# Checar
sysctl -n net.ipv4.ip_forward net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.ip_local_port_range net.netfilter.nf_conntrack_max

How to configure ulimits on the host and in containers

File‑descriptor and process limits prevent errors like “Too many open files.” For services managed by systemd, use service overrides. For containers, set ulimits in Compose.

Ensure the host limit is higher than the container limit; otherwise Docker cannot apply the requested value. Restarting the Docker service will interrupt running containers. For diagnosing FD issues in PHP‑FPM, see the troubleshooting article Fix 'Too many open files' on Linux (PHP‑FPM).

  • Raise fs.file‑max and LimitNOFILE for docker.service
  • Define ulimits per service in docker‑compose.yml
  • Align limits with PHP‑FPM settings (pm.max_children, sockets, connections)
# 1) Limite global de FDs do kernel
sysctl -w fs.file-max=500000
# Para persistir: adicione fs.file-max = 500000 em /etc/sysctl.d/99-docker-php.conf

# 2) Override do systemd para o Docker
mkdir -p /etc/systemd/system/docker.service.d
cat >/etc/systemd/system/docker.service.d/override.conf <<'EOF'
[Service]
LimitNOFILE=1048576
LimitNPROC=131072
EOF
systemctl daemon-reload && systemctl restart docker

# 3) docker-compose.yml (exemplo por serviço)
# services:
#   php:
#     image: your-php-image:latest
#     ulimits:
#       nofile: 65536
#       nproc: 4096
#       core: 0

# Diagnóstico de FDs em runtime
# docker compose exec php sh -lc 'ulimit -n; ulimit -u'

Journald: retention, tags and disk impact

Using journald as the container log driver centralizes logs and makes retention and compression simpler. Set quotas and retention to avoid filling the VPS disk.

Standardize container tags for consistent filtering. For details on rotation and compression on low‑disk VPSes, see the logs retention tutorial Docker logs: journald, rsyslog & logrotate.

  • Set Storage=persistent to keep logs after reboots
  • Limit SystemMaxUse and MaxRetentionSec
  • Use log‑driver=journald with a consistent tag
# /etc/systemd/journald.conf (trecho)
[Journal]
Storage=persistent
SystemMaxUse=1G
SystemMaxFileSize=128M
MaxRetentionSec=14day
RateLimitIntervalSec=30s
RateLimitBurst=10000

systemctl restart systemd-journald
journalctl --disk-usage

# /etc/docker/daemon.json (trecho)
{
  "log-driver": "journald",
  "log-opts": {"tag": "{{.Name}}"},
  "exec-opts": ["native.cgroupdriver=systemd"]
}

systemctl restart docker && sleep 2 && docker info | grep -i log

Swap and OOM: a safe production strategy

A small swap area reduces OOM kills during short memory spikes. For PHP workloads, keep swappiness low and monitor page faults.

Create a swapfile when swap is missing and validate it. Swap on slow disks can increase latency. For troubleshooting nighttime I/O and swap spikes, see Fix I/O spikes and swap on a VPS.

  • Create a swapfile sized to your RAM and traffic pattern
  • Set vm.swappiness to 10–20 as a starting point
  • Align memswap with mem_limit for critical containers
# Criar swapfile (ex.: 2G)
fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

# Reduzir swappiness
sysctl -w vm.swappiness=10
# Persistir em /etc/sysctl.d/99-docker-php.conf
# vm.swappiness = 10

# Conferir swap
swapon --show
cat /proc/sys/vm/swappiness

# Ajuste por container no compose (evita uso de swap pelo container)
# services:
#   php:
#     mem_limit: 1g
#     memswap_limit: 1g  # total = memória + swap; igual à mem_limit ≈ sem swap no container

Cgroups v2 and Docker Compose: how to limit resources

Cgroups v2 exposes limits for CPU, memory, PIDs and I/O. In local Compose, deploy.* fields are ignored; use direct limit fields instead.

Validate that Docker uses the systemd cgroup driver and inspect limits with docker inspect or by reading files under /sys/fs/cgroup. For a full walkthrough see cgroup v2 + systemd ResourceControl on Docker.

  • Use mem_limit, memswap_limit, cpus and pids_limit in Compose
  • Tune blkio_config to avoid starving the disk
  • Avoid deploy.resources in Compose (it's ignored outside Swarm)
# docker-compose.yml (exemplo simplificado)
version: "3.9"
services:
  php:
    image: your-php-image:latest
    cpus: "1.5"
    mem_limit: 1g
    memswap_limit: 1g
    pids_limit: 4096
    ulimits:
      nofile: 65536
      nproc: 4096
    blkio_config:
      weight: 600

# Verificações
# docker compose up -d
# docker inspect --format '{{json .HostConfig.NanoCpus}}' <container>
# docker compose exec php sh -lc 'cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory.limit_in_bytes'

Quick checks and automating the checklist

After applying the changes, run objective checks to confirm sysctl, limits, logging and cgroups. These checks should be repeatable.

Automate the checklist with Ansible or scripts. If a value diverges, fix it in the persistent files: sysctl.d, daemon.json and systemd overrides.

Item Command Expected output
Cgroup v2 stat -fc %T /sys/fs/cgroup cgroup2fs
Docker cgroup driver docker info | grep -i 'Cgroup Driver' systemd
Ephemeral ports sysctl -n net.ipv4.ip_local_port_range 1024 65535
Backlogs (somaxconn/SYN) sysctl -n net.core.somaxconn net.ipv4.tcp_max_syn_backlog >= 1024 / >= 1024
Conntrack sysctl -n net.netfilter.nf_conntrack_max > 65536 (tune to load)
Swap and swappiness swapon --show; cat /proc/sys/vm/swappiness swap active; 10–20
Journald retention journalctl --disk-usage <= configured quota (e.g. ~1G)
FDs limit (host) cat /proc/sys/fs/file-max >= 500000 (adjust as needed)
FDs limit (container) docker compose exec php sh -lc 'ulimit -n' >= 65536
# Validação resumida
set -euo pipefail
printf "Cgroup: "; stat -fc %T /sys/fs/cgroup
printf "Cgroup Driver: "; docker info 2>/dev/null | awk -F: '/Cgroup Driver/{print $2}'
printf "Ports: "; sysctl -n net.ipv4.ip_local_port_range
printf "Backlogs: "; sysctl -n net.core.somaxconn net.ipv4.tcp_max_syn_backlog
printf "Conntrack: "; sysctl -n net.netfilter.nf_conntrack_max
printf "Swap: "; swapon --show | tail -n +2 | wc -l; cat /proc/sys/vm/swappiness
journalctl --disk-usage || true
printf "FDs host: "; cat /proc/sys/fs/file-max
# FDs container (requer serviço 'php')
docker compose exec -T php sh -lc 'echo -n "FDs container: "; ulimit -n' || true

Frequently asked questions

What swappiness should I use in production for PHP on Docker?

Use a low swappiness, between 10 and 20, to avoid aggressive swapping and higher latency. Keep a small swap area to absorb short memory spikes. Monitor OOM kills and page faults and tune according to traffic patterns and host RAM.

Is journald better than json‑file for container logs?

On a VPS, journald centralizes logs with the host and makes retention and compression easier. It prevents uncontrolled growth typical of json‑file. If you use json‑file, configure max‑size and max‑file. With journald, set SystemMaxUse and retention and monitor with journalctl --disk-usage.

Does docker‑compose deploy.resources work outside Swarm?

No. deploy.resources is ignored by the standalone Compose. To enforce limits, use cpus, mem_limit, memswap_limit, pids_limit, blkio_config and ulimits. Validate with docker inspect and by reading cgroup files inside the container.

What nofile value should I set for PHP‑FPM?

Start with 65536 per PHP container and adjust based on metrics. Consider concurrent connections, sockets, open files and logs. Ensure the host-wide fs.file‑max and LimitNOFILE are well above container settings. Measure peak FD usage during critical times.

How to prevent logs from filling the VPS disk?

Limit journald with SystemMaxUse and MaxRetentionSec and standardize tags. For json‑file, set max‑size and max‑file. Monitor journalctl --disk-usage and create alerts before the disk reaches ~80%. Periodic cleanup of images and unused volumes also helps.

Should I disable swap entirely in production?

Not usually. A small swap helps avoid OOM kills during brief spikes. Disabling swap increases the chance of process restarts. Prefer a low swappiness and set clear container limits (mem_limit and memswap_limit) to control memory without starving the host.

Want a checklist review before deployment?

Send your docker-compose.yml and the outputs of the checks. We can review limits, sysctl and journald settings for a smoother rollout.

Discuss the checklist

See also