YOUR DIGITAL HUB
← Back to blog

Architecting PHP on AWS: from MVP to scale

· 10 min read
Cover visual — architecting PHP on AWS from MVP to scale

Why AWS remains the default in 2026

AWS is still the default cloud platform on which we deploy our enterprise clients' PHP workloads. Three reasons, re-validated every year.

  • Breadth. Over 240 services, from classic EC2 to Aurora Serverless v2 and Bedrock for foundation models. Whatever you want to build probably exists as a managed service.
  • Compliance. SOC 2, ISO 27001, HDS, PCI-DSS, FedRAMP. Regulated industries (banking, healthcare, insurance) find the certifications they need.
  • Ecosystem. Terraform, OpenTofu, CDK, Pulumi. IaC tooling is mature. AWS talent is three to five times more abundant than GCP or Azure talent on the French market.

Trade-offs are real. AWS invoices know how to surprise (NAT Gateway, cross-AZ egress, S3 request costs). The complexity of VPC and IAM is an entry cost that must be accepted. And on some simple workloads, Scaleway or OVHcloud managed offerings do the job for half the price.

This article describes how we architect a PHP application on AWS along three maturity tiers: MVP, Growth, Scale. With concrete services, real costs, and the code to go with it.

Tier 1, MVP (0 to 10,000 users)

Goal: ship to production fast, without over-engineering. One EC2 instance is enough, multi-AZ is an unjustified cost at this stage. We still keep IaC discipline and a solid base ready to grow.

   Users
     │
     ▼
 ┌──────────┐     ┌──────────────┐
 │ Route53  │────▶│  CloudFront  │
 └──────────┘     └──────┬───────┘
                         │
                         ▼
                  ┌──────────────┐
                  │ EC2 t3.medium │
                  │ Nginx+PHP-FPM │
                  └──────┬────────┘
                         │
               ┌─────────┴─────────┐
               ▼                    ▼
         ┌──────────┐         ┌──────────┐
         │ RDS PG   │         │    S3    │
         │db.t4g.micro│       │ uploads  │
         └──────────┘         └──────────┘
  • Compute: one EC2 t3.medium (or t4g.medium Graviton for -20%). Nginx + PHP-FPM 8.3, Supervisor for workers.
  • Database: RDS PostgreSQL db.t4g.micro or db.t4g.small. Seven-day automated backups.
  • CDN and TLS: CloudFront in front of the site, ACM for the certificate, Route53 for DNS.
  • Assets: S3 for user uploads, served via CloudFront.
  • Email: SES for transactional, SNS for internal alerts.
  • Monitoring: CloudWatch Logs + 3 alarms (CPU, 5xx errors, disk).

Typical monthly cost at this tier: 80 to 150 EUR, excluding SES (variable with volume).

The classic trap to avoid: jumping to RDS Multi-AZ (+100% on the DB bill) before the SLO requires it. A daily snapshot plus a tested restore script is enough for a B2B app at MVP stage.

Tier 2, Growth (10,000 to 500,000 users)

We take on traffic, spikes, and availability becomes a real commitment. Multi-AZ everywhere on stateful services, horizontal compute, Redis sessions to strip state away from PHP-FPM.

          Users
            │
            ▼
      ┌──────────────┐
      │  CloudFront  │
      └──────┬───────┘
             │
             ▼
      ┌──────────────┐
      │     ALB      │
      └──────┬───────┘
             │
       Auto Scaling Group
       ┌─────┴─────┬─────────┐
       ▼           ▼         ▼
   ┌───────┐   ┌───────┐ ┌───────┐
   │ EC2-A │   │ EC2-B │ │ EC2-C │
   └───┬───┘   └───┬───┘ └───┬───┘
       └─────┬─────┴─────────┘
             │
     ┌───────┼────────────┐
     ▼       ▼            ▼
┌─────────┐ ┌───────────┐ ┌──────┐
│RDS MAZ  │ │ElastiCache│ │  S3  │
│Postgres │ │  Redis    │ │      │
└─────────┘ └───────────┘ └──────┘
  • Compute: ALB + Auto Scaling Group with 2 to 6 EC2 m6i.large (or m7g.large Graviton). Two AZs minimum.
  • Database: RDS PostgreSQL Multi-AZ, db.m6g.large with 100 GB gp3 and provisioned IOPS if needed.
  • Cache and session: ElastiCache Redis 7 in Cluster mode, two nodes, PHP session offloaded (php.ini: session.save_handler = redis).
  • Assets and uploads: S3 + CloudFront.
  • Queue: SQS for async jobs (email, webhooks, background processing).
  • Crons: EventBridge Scheduler triggering Lambdas or on-demand ECS tasks.
  • Observability: CloudWatch Logs Insights, CloudWatch Metrics, X-Ray for tracing, SLO-based dashboards and alarms.

Typical monthly cost: 400 to 800 EUR. Main items are the EC2 ASG, RDS Multi-AZ and CloudFront traffic.

At this tier we always containerize the app, even if it still runs on an AMI. The Dockerfile becomes the deployment interface, which paves the path to Fargate at the next tier.

Tier 3, Scale (500,000 users and beyond)

Multi-region reads or disaster recovery, managed services everywhere, fine-grained auto-scaling, observability front and center.

  • Compute: ECS Fargate (or EKS if the team is kube-native) with service auto-scaling on ALB and CloudWatch metrics.
  • Database: Aurora PostgreSQL Serverless v2, multi-AZ read replicas, Aurora Global Database if cross-region is needed.
  • Cache: ElastiCache Redis in cluster mode enabled, 3 shards minimum, cross-AZ replication.
  • Search: OpenSearch managed or OpenSearch Serverless.
  • Async: SQS + Lambda for short tasks, ECS tasks for long ones.
  • Batch: AWS Batch or ECS scheduled tasks, sometimes Step Functions to orchestrate.
  • CDN: CloudFront with origin shield, Lambda@Edge for routing or A/B testing rules.
  • WAF: AWS WAF in front of CloudFront and ALB, managed OWASP rules plus bot control.
  • Observability: Prometheus and Grafana on EKS for kube teams, or Managed Grafana + Managed Prometheus, Datadog or New Relic depending on ops choice.

Typical monthly cost: 2,000 to 10,000 EUR depending on volume. On one client handling 20M requests per day, we sit at around 6,500 EUR per month in steady state.

Production-ready PHP-FPM Dockerfile

The PHP container is the foundation of deployment. Here is the base we use and harden project by project.

# syntax=docker/dockerfile:1.7

FROM php:8.3-fpm-alpine AS base

RUN apk add --no-cache \
        icu-libs \
        libpq \
        libzip \
        oniguruma \
        tzdata \
    && docker-php-ext-install -j$(nproc) \
        bcmath \
        intl \
        opcache \
        pcntl \
        pdo_pgsql \
        zip \
    && pecl install redis apcu \
    && docker-php-ext-enable redis apcu

RUN { \
        echo 'opcache.enable=1'; \
        echo 'opcache.enable_cli=0'; \
        echo 'opcache.memory_consumption=256'; \
        echo 'opcache.interned_strings_buffer=32'; \
        echo 'opcache.max_accelerated_files=20000'; \
        echo 'opcache.validate_timestamps=0'; \
        echo 'opcache.preload=/app/config/preload.php'; \
        echo 'opcache.preload_user=www-data'; \
        echo 'realpath_cache_size=4096k'; \
        echo 'realpath_cache_ttl=600'; \
    } > /usr/local/etc/php/conf.d/99-opcache-prod.ini

COPY --from=composer:2.8 /usr/bin/composer /usr/bin/composer

FROM base AS deps
WORKDIR /app
COPY composer.json composer.lock symfony.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-progress --no-interaction

FROM base AS runtime
WORKDIR /app
COPY --from=deps /app/vendor ./vendor
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative --no-dev \
    && php bin/console cache:warmup --env=prod --no-debug \
    && chown -R www-data:www-data var public

USER www-data
EXPOSE 9000

HEALTHCHECK --interval=30s --timeout=3s --start-period=20s \
    CMD SCRIPT_NAME=/health SCRIPT_FILENAME=/health REQUEST_METHOD=GET \
        cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1

CMD ["php-fpm", "-F"]

Three non-negotiable things in our production Dockerfiles:

  • Multi-stage so we do not ship Composer and devDependencies in the final image.
  • Hardened OPcache with validate_timestamps=0 and Symfony preload. Steady 10 to 30% latency gain.
  • Non-root user (www-data). We refuse images that run as root.

On ECS Fargate, this container runs in a task with an Nginx sidecar (nginx:1.27-alpine) sharing an ephemeral volume into which Symfony has copied public/.

Terraform module for a PHP Fargate service

We maintain a reusable Terraform module to deploy a PHP service on ECS Fargate with ALB, service auto-scaling and logs. Here is a representative excerpt.

module "php_service" {
  source  = "./modules/fargate-php"

  name            = "invoicing-api"
  cluster_arn     = aws_ecs_cluster.main.arn
  vpc_id          = module.vpc.vpc_id
  private_subnets = module.vpc.private_subnets
  public_subnets  = module.vpc.public_subnets

  container_image = "${aws_ecr_repository.app.repository_url}:${var.image_tag}"
  cpu             = 1024
  memory          = 2048

  desired_count   = 3
  min_capacity    = 3
  max_capacity    = 20
  target_cpu      = 60
  target_alb_rps  = 400

  environment = {
    APP_ENV   = "prod"
    APP_DEBUG = "0"
  }

  secrets = {
    APP_SECRET        = aws_secretsmanager_secret.app_secret.arn
    DATABASE_URL      = aws_secretsmanager_secret.database_url.arn
    REDIS_URL         = aws_secretsmanager_secret.redis_url.arn
    ANTHROPIC_API_KEY = aws_secretsmanager_secret.anthropic.arn
  }

  log_retention_days = 30
  enable_xray        = true
  alb_health_path    = "/health"

  tags = {
    Project     = "invoicing"
    Environment = "prod"
    ManagedBy   = "terraform"
  }
}

The module encapsulates a bundle of best practices:

  • Task role IAM with minimum permissions.
  • Secrets pulled from Secrets Manager, never in clear in the task definition.
  • Service discovery via Cloud Map if multiple services talk internally.
  • Auto-scaling target on CPU AND ALB RPS, with 60-second cooldown.
  • X-Ray sidecar enabled if enable_xray = true.
  • Dedicated log group per service, configurable retention.

PHP specifics on AWS

  • Sessions: offload via Redis as soon as you have more than one instance. session.save_handler = redis, session.save_path = tcp://redis:6379.
  • File uploads: never write on the task disk. Direct S3 uploads (presigned POST) or client-side upload then putObject to S3 with temp cleanup.
  • Application logs: Monolog JSON to stdout, CloudWatch Logs captures the container output, Logs Insights for search.
  • Crons: EventBridge Scheduler + ECS run-task. Never leave a crontab in a container, it does not survive auto-scaling.
  • Workers: a separate ECS service consumes SQS (e.g., bin/console messenger:consume).
  • Warmup: cache:warmup at image build. On Fargate, every cold start during scale-up costs without this step.

AWS security

The minimum rules we do not negotiate on an enterprise PHP workload.

  • VPC with private subnets for tasks and DBs, public subnets only for ALB and NAT Gateway.
  • Security groups tightly scoped, no 0.0.0.0/0 inbound on tasks.
  • IAM least privilege: each task role lists its actions one by one, no *.
  • Secrets Manager for dynamic secrets, KMS for encryption keys.
  • RDS encrypted at rest (KMS), TLS in transit enforced at the driver level.
  • CloudTrail enabled on all accounts, logs centralized in an audit account.
  • GuardDuty enabled, findings forwarded to Security Hub or an external SIEM.
  • AWS WAF in front of CloudFront and the ALB in production.

Observability

Four pillars for PHP in production on AWS:

  1. Logs: Monolog JSON → CloudWatch Logs → Logs Insights for ad hoc, S3 export for long term.
  2. Metrics: CloudWatch Metrics, custom metrics via PutMetricData or EMF (embedded metric format). Dashboards per business domain.
  3. Traces: X-Ray PHP SDK to trace DB, cache and external API calls. Or OpenTelemetry if you prefer vendor neutral.
  4. Alerting: CloudWatch Alarms → SNS → Slack or PagerDuty. Alerts tied to SLOs, not raw CPU.

Real costs and how to reduce them

On 20 AWS engagements, the levers that actually matter:

  • Graviton (ARM): -20 to -40% on EC2, RDS, ElastiCache vs Intel equivalents. Most PHP 8.3 images have been running natively on ARM for a while.
  • Savings Plans compute, 3 years no upfront: about -40% on compute if the load is stable.
  • RDS Reserved Instances 1 year no upfront: -30 to -40% on the DB.
  • S3 Intelligent-Tiering for rarely accessed assets.
  • Healthy CloudFront caching: hit ratio must be > 85%, otherwise you pay for EC2 transfer out.
  • VPC Endpoints for S3, DynamoDB, Secrets Manager: avoids NAT Gateway charges on AWS-internal traffic.

Classic traps we uncover in audits

  • NAT Gateway billed without noticing. $0.045 per GB processed + $0.045 per hour. A workload chatty with the Internet may pay 500 to 1,500 EUR per month on NAT alone.
  • Oversized RDS. db.r6g.xlarge instances running at 8% CPU. Quarterly review mandatory.
  • Cross-AZ transfer out. Often forgotten. An ALB balancing across 3 AZs with all cross-AZ backend traffic can add 10 to 15% to the bill.
  • Idle ALBs. Several "just in case" ALBs. Each costs ~20 EUR per month plus LCUs. Consolidate.
  • Unlimited CloudWatch Logs retention. 5 to 10 years of logs on internal Splunk. 30-day retention + S3 archive is usually enough.
  • EBS gp2. Always migrate to gp3 (cheaper, better baseline performance).

Alternatives where AWS is not the best fit

  • Scaleway, OVHcloud for a single-region SaaS on a tight budget: 2 to 3 times lower compute pricing, French sovereignty, French-language support.
  • GCP if you already use BigQuery or Vertex AI. Cloud Run for containerized PHP is excellent.
  • Azure in Microsoft-heavy environments (Active Directory, Office 365, Dynamics). AAD integration and bundled licensing can justify Azure even for a PHP workload.
  • Platform.sh or Clever Cloud for teams that do not want to manage infrastructure at all, on simpler applications.

Conclusion

AWS is an excellent default for an enterprise PHP application in 2026, provided costs are treated with the same rigor as code. The three tiers MVP, Growth, Scale structure the trajectory: start simple, add managed services as growth demands, gain resilience and elasticity without premature over-engineering.

For AWS planning on your PHP workload, an on-prem to AWS migration or a bill audit, email us at contact@your-digital-hub.com or discover our hosting services and DevOps services.