Skip to content

Dynamic Website Hosting using AWS

Deploy a production-ready dynamic web application on AWS with EC2, RDS, CloudFront and Cloudflare in front for security and performance.

EC2RDSS3CloudFrontRoute53Certificate ManagerCloudflare
4 min readIntermediate
Section 01

Introduction

Dynamic website hosting runs an actual application server that generates responses at request time — server-side rendering, database queries, authentication, and business logic. This guide deploys a classic, production-proven stack: an EC2 instance running Apache or Nginx with PHP, backed by a managed RDS MySQL database, with static assets offloaded to S3, distributed through CloudFront, secured with ACM, resolved through Route 53, and shielded by Cloudflare.

Compared to the static hosting setup, this architecture trades some operational simplicity for the ability to run real application logic — user accounts, forms, CMS-driven content, e-commerce, and anything else that needs a database.

Section 02

Architecture Diagram

Static assets (images, CSS, JS) are served from S3 via CloudFront, while dynamic requests are routed to the EC2 application server, which reads and writes to the RDS database.

Cloudflare → Route 53 → CloudFront → EC2 (app) + S3 (static) → RDS

Section 03

EC2 Launch

Launch an EC2 instance to host the application server:

terminal
aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.small \
  --key-name my-keypair \
  --security-group-ids sg-0123456789abcdef0 \
  --subnet-id subnet-0123456789abcdef0 \
  --count 1 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=app-server}]'
Choose an instance type sized to actual load (start with t3.small or t3.medium for low/medium traffic) — resize later rather than over-provisioning from day one.
Section 04

Security Groups

Restrict inbound traffic to only what the application needs:

terminal
# Allow HTTPS from anywhere (or restrict to Cloudflare's IP ranges)
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp --port 443 --cidr 0.0.0.0/0

# Allow SSH only from your admin IP
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp --port 22 --cidr 203.0.113.10/32

Never open SSH to 0.0.0.0/0

Restrict port 22 to a known admin IP or, better, use AWS Systems Manager Session Manager and remove the inbound SSH rule entirely.
Section 05

Elastic IP

Allocate and associate an Elastic IP so the instance keeps the same public IP across stops/starts — important since DNS and Cloudflare will point directly at this address.

terminal
aws ec2 allocate-address --domain vpc
aws ec2 associate-address --instance-id i-0123456789abcdef0 --allocation-id eipalloc-0123456789abcdef0
Section 06

Apache / Nginx

Install and configure the web server on the instance:

Apache — Amazon Linux / RHEL
sudo dnf install -y httpd
sudo systemctl enable --now httpd
/etc/nginx/conf.d/app.conf
server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/app/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}
Section 07

PHP

terminal
sudo dnf install -y php php-fpm php-mysqlnd php-json php-mbstring
sudo systemctl enable --now php-fpm
Match the PHP version to your application's requirements, and enable only the extensions actually in use to keep the attack surface small.
Section 08

MySQL

The application connects to a managed RDS MySQL instance rather than a local MySQL server — no database software runs on the EC2 instance itself. Install only the MySQL client for administration:

terminal
sudo dnf install -y mysql
Section 09

RDS Setup

Provision a managed MySQL instance in RDS:

terminal
aws rds create-db-instance \
  --db-instance-identifier app-db \
  --db-instance-class db.t3.micro \
  --engine mysql \
  --engine-version 8.0 \
  --master-username admin \
  --master-user-password 'CHANGE_ME_SECURELY' \
  --allocated-storage 20 \
  --vpc-security-group-ids sg-0123456789abcdef0 \
  --no-publicly-accessible \
  --backup-retention-period 7
  • Set --no-publicly-accessible so RDS is only reachable from inside the VPC.
  • Enable automated backups with at least a 7-day retention period.
  • Place RDS in a private subnet, separate from the public-facing EC2 subnet.
  • Enable Multi-AZ for production workloads that need automatic failover.
Section 10

Database Connection

Only allow the EC2 security group to reach RDS on port 3306:

terminal
aws ec2 authorize-security-group-ingress \
  --group-id sg-rds0123456789 \
  --protocol tcp --port 3306 \
  --source-group sg-0123456789abcdef0
db.php
<?php
$pdo = new PDO(
    "mysql:host=" . getenv('DB_HOST') . ";dbname=" . getenv('DB_NAME'),
    getenv('DB_USER'),
    getenv('DB_PASSWORD'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
Section 11

Environment Variables

Keep credentials out of source control using a local, non-committed env file:

.env
DB_HOST=app-db.abcdefghij.us-east-1.rds.amazonaws.com
DB_NAME=appdb
DB_USER=admin
DB_PASSWORD=CHANGE_ME_SECURELY
APP_ENV=production

Never commit .env

Add .env to .gitignore and use AWS Secrets Manager or SSM Parameter Store for production credential storage instead of a plain file where possible.
Section 12

S3 Storage

Offload user uploads and static assets (images, CSS, JS) to S3 so the EC2 instance stays stateless and easy to scale or replace:

terminal
aws s3api create-bucket --bucket app-static-assets --region us-east-1
aws s3 sync ./public/assets s3://app-static-assets/assets --delete
Section 13

CloudFront

Configure CloudFront with two origins: the S3 bucket for static assets (cached aggressively) and the EC2 instance for dynamic requests (cached minimally or not at all, forwarding cookies/query strings needed by the app):

  • /assets/* behavior → S3 origin, long cache TTL.
  • Default (/*) behavior → EC2 origin, forward cookies and query strings, short/no cache.
Section 14

Route53

Point the domain at CloudFront using an alias A record, as in the static hosting guide:

terminal
aws route53 change-resource-record-sets \
  --hosted-zone-id Z0123456789ABCDEF \
  --change-batch file://route53-change-batch.json
Section 15

Cloudflare

Same configuration approach as the static hosting setup: authoritative nameservers, Full (strict)SSL mode, WAF enabled, and Always Use HTTPS. For dynamic apps, also review Cloudflare's cache rules to ensure HTML/API responses are not cached when they shouldn't be.

Section 16

SSL

Request an ACM certificate in us-east-1 for CloudFront exactly as in the static hosting guide, covering both the apex and www subdomain via DNS validation.

Section 17

Deployment

A typical deployment flow for shipping application code updates:

  1. Push code

    Merge to the main branch, triggering CI.
  2. Run tests

    CI runs the application's automated test suite.
  3. Deploy to EC2

    Pull the latest code onto the instance (or replace it via an AMI/Auto Scaling rollout) and restart PHP-FPM.
  4. Run migrations

    Apply any pending database schema migrations against RDS.
  5. Invalidate CloudFront

    Clear cached static assets that changed in this release.
Section 18

Testing

  • Smoke test the homepage and key user flows after every deploy.
  • Verify database reads/writes succeed against RDS from the app.
  • Confirm static assets load from the CloudFront/S3 path, not the EC2 origin.
  • Load test with a tool like k6 or Apache Bench before high-traffic launches.
Section 19

Monitoring

Enable CloudWatch alarms on EC2 CPU/memory, RDS connections and storage, and CloudFront 4xx/5xx error rates. Ship application logs to CloudWatch Logs for centralized searching.

Section 20

Backups

  • RDS automated backups (already enabled above) plus periodic manual snapshots before major changes.
  • EC2 AMI snapshots of the configured instance for fast disaster recovery.
  • S3 versioning enabled on the assets bucket to recover from accidental overwrites.
Section 21

Security

  • Keep RDS in a private subnet, never publicly accessible.
  • Restrict SSH to Session Manager or a known admin IP only.
  • Enable Cloudflare WAF and rate limiting on login/form endpoints.
  • Patch the EC2 OS and PHP packages on a regular schedule.
  • Store secrets in Secrets Manager / SSM Parameter Store, not in code.
Troubleshooting

Common Issues

502 Bad Gateway

Usually PHP-FPM isn't running or the Nginx fastcgi_passsocket path doesn't match the actual PHP-FPM pool configuration.

Database connection refused

Check that the EC2 security group is allowed inbound on 3306 in the RDS security group, and that the RDS instance is in the available state.

Assets 404 via CloudFront

Confirm the /assets/* cache behavior routes to the S3 origin, not the EC2 default origin.
Optimization

Performance Optimization

  • Enable PHP OPcache to avoid recompiling scripts on every request.
  • Add a query cache or object cache (e.g. Redis/ElastiCache) for hot database reads.
  • Offload all static assets to S3 + CloudFront so EC2 only serves dynamic requests.
  • Right-size the EC2 instance and RDS class based on observed CloudWatch metrics.
Reference

Useful Commands

terminal
# Tail application logs
sudo journalctl -u php-fpm -f

# Restart web stack after a deploy
sudo systemctl restart php-fpm nginx

# Connect to RDS from EC2
mysql -h app-db.abcdefghij.us-east-1.rds.amazonaws.com -u admin -p
Best Practices

Best Practices

  • Keep the application server stateless — sessions/uploads in RDS/S3, not local disk.
  • Automate deployment instead of manually editing files on the instance.
  • Use an Auto Scaling Group + Load Balancer once traffic outgrows a single instance.
  • Separate staging and production environments with distinct RDS instances.
Summary

Summary

This architecture runs a real application server on EC2 with a managed RDS database, offloads static assets to S3/CloudFront, and layers Cloudflare and ACM-issued TLS on top for security and performance — a solid, production-proven foundation for dynamic web applications that can scale from a single instance to a full Auto Scaling fleet as traffic grows.