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.
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
EC2 Launch
Launch an EC2 instance to host the application server:
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}]'t3.small or t3.medium for low/medium traffic) — resize later rather than over-provisioning from day one.Security Groups
Restrict inbound traffic to only what the application needs:
# 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/32Never 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.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.
aws ec2 allocate-address --domain vpc
aws ec2 associate-address --instance-id i-0123456789abcdef0 --allocation-id eipalloc-0123456789abcdef0Apache / Nginx
Install and configure the web server on the instance:
sudo dnf install -y httpd
sudo systemctl enable --now httpdserver {
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;
}
}PHP
sudo dnf install -y php php-fpm php-mysqlnd php-json php-mbstring
sudo systemctl enable --now php-fpmMySQL
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:
sudo dnf install -y mysqlRDS Setup
Provision a managed MySQL instance in RDS:
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.
Database Connection
Only allow the EC2 security group to reach RDS on port 3306:
aws ec2 authorize-security-group-ingress \
--group-id sg-rds0123456789 \
--protocol tcp --port 3306 \
--source-group sg-0123456789abcdef0<?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]
);Environment Variables
Keep credentials out of source control using a local, non-committed env file:
DB_HOST=app-db.abcdefghij.us-east-1.rds.amazonaws.com
DB_NAME=appdb
DB_USER=admin
DB_PASSWORD=CHANGE_ME_SECURELY
APP_ENV=productionNever 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.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:
aws s3api create-bucket --bucket app-static-assets --region us-east-1
aws s3 sync ./public/assets s3://app-static-assets/assets --deleteCloudFront
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.
Route53
Point the domain at CloudFront using an alias A record, as in the static hosting guide:
aws route53 change-resource-record-sets \
--hosted-zone-id Z0123456789ABCDEF \
--change-batch file://route53-change-batch.jsonCloudflare
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.
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.
Deployment
A typical deployment flow for shipping application code updates:
Push code
Merge to the main branch, triggering CI.Run tests
CI runs the application's automated test suite.Deploy to EC2
Pull the latest code onto the instance (or replace it via an AMI/Auto Scaling rollout) and restart PHP-FPM.Run migrations
Apply any pending database schema migrations against RDS.Invalidate CloudFront
Clear cached static assets that changed in this release.
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.
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.
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.
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.
Common Issues
502 Bad Gateway
Usually PHP-FPM isn't running or the Nginxfastcgi_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 theavailable state.Assets 404 via CloudFront
Confirm the/assets/* cache behavior routes to the S3 origin, not the EC2 default origin.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.
Useful Commands
# 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 -pBest 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
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.