Skip to content

Docker Compose Examples

Drop-in compose stacks for common use cases. Adapt the image tag, passwords and volume paths to your needs.

Minimal local dev

services:
  web:
    image: erseco/alpine-php-webserver
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./php:/var/www/html

With MariaDB

services:
  web:
    image: erseco/alpine-php-webserver
    restart: unless-stopped
    environment:
      date_timezone: Europe/Madrid
      memory_limit: 256M
    ports:
      - "8080:8080"
    volumes:
      - ./app:/var/www/html
    depends_on:
      - db

  db:
    image: mariadb:lts
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: rootpw
      MARIADB_DATABASE: app
      MARIADB_USER: app
      MARIADB_PASSWORD: app
    volumes:
      - mariadb:/var/lib/mysql

volumes:
  mariadb:

With PostgreSQL

services:
  web:
    image: erseco/alpine-php-webserver
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./app:/var/www/html
    depends_on:
      - db

  db:
    image: postgres:alpine
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: app
      POSTGRES_USER: app
      POSTGRES_DB: app
    volumes:
      - postgres:/var/lib/postgresql

volumes:
  postgres:

Behind a reverse proxy (Traefik)

No host port required — the reverse proxy talks to the service over the internal network.

services:
  web:
    image: erseco/alpine-php-webserver
    restart: unless-stopped
    environment:
      REAL_IP_HEADER: X-Forwarded-For
      REAL_IP_RECURSIVE: "on"
      REAL_IP_FROM: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
    volumes:
      - ./app:/var/www/html
    networks:
      - proxy
      - default
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.app.rule=Host(`app.example.com`)"
      - "traefik.http.routers.app.entrypoints=websecure"
      - "traefik.http.routers.app.tls=true"
      - "traefik.http.routers.app.tls.certresolver=letsencrypt"
      - "traefik.http.services.app.loadbalancer.server.port=8080"

networks:
  proxy:
    external: true

See Reverse Proxy & Trusted IPs for Nginx, Apache, Caddy and Cloudflare variants.

Building your own image on top

Use build: instead of image: when your project needs Composer or custom config baked in.

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - "8080:8080"

A matching Dockerfile:

FROM erseco/alpine-php-webserver:latest

USER root
RUN apk add --no-cache composer
USER nobody

COPY --chown=nobody:nobody ./ /var/www/html
WORKDIR /var/www/html

RUN composer install --no-dev --optimize-autoloader --no-interaction --no-progress

USER root
RUN apk del composer
USER nobody

See Composer & Building for the full recipe.

Production tuning

services:
  web:
    image: erseco/alpine-php-webserver
    restart: unless-stopped
    environment:
      # Serve uploaded files / large POSTs
      client_max_body_size: 64M
      post_max_size: 64M
      upload_max_filesize: 64M
      memory_limit: 256M
      max_execution_time: 60
      fastcgi_read_timeout: 90s
      fastcgi_send_timeout: 90s

      # OPcache for production
      opcache_enable: "1"
      opcache_memory_consumption: "256"
      opcache_max_accelerated_files: "20000"
      opcache_validate_timestamps: "0"

      # Locale / timezone
      date_timezone: UTC
      intl_default_locale: en_US
    volumes:
      - ./app:/var/www/html

OPcache with opcache_validate_timestamps=0

Production images that opt into opcache_validate_timestamps=0 must be rebuilt (or the container restarted) on every deploy. Otherwise OPcache will keep serving the previous revision of your PHP files.