A backup is only useful if you can bring the system back from it. For my WGU cloud computing capstone, I wanted to test that end to end: if a small business lost its on-premises server, could I restore its application and data in AWS without paying for a second environment to run all year?
Bayou Outdoor Supply was the capstone scenario, a small Gulf South distributor whose order system depended on one server. I built a Flask and PostgreSQL order application on an Ubuntu VM in Proxmox, then designed an offsite recovery path around hourly database backups in Amazon S3. The recovery environment stays off until someone needs it.
Stack: Proxmox, Ubuntu, Docker Compose, Flask, PostgreSQL, Amazon S3, EC2, VPC, IAM, Terraform, and cloud-init.
The recovery path
During normal operation, a scheduled pg_dump job compresses the database and sends it to S3. The hourly schedule defines the one-hour recovery point objective: in a worst-case outage just before the next backup, up to an hour of changes could be lost.

Before the simulated outage, I checked that the primary database held three sample orders.

The S3 objects show successive hourly backups. Getting them off the Proxmox host was the point: a local backup would disappear with the same failure.
For recovery, I run terraform apply. Terraform creates a VPC, public subnet, route, security group, IAM role, instance profile, and EC2 instance. The instance's user data installs the dependencies, starts the Docker Compose stack, finds the newest S3 object, and restores the SQL dump into PostgreSQL.
I used an instance profile so the recovery server could read the backup bucket without an AWS access key in the bootstrap script. The policy grants bucket listing and object retrieval, not write access.
resource "aws_iam_instance_profile" "ec2_profile" {
name_prefix = "${var.project_name}-profile-"
role = aws_iam_role.ec2_role.name
}The restore script chooses the latest backup by S3's LastModified value:
LATEST_KEY=$(aws s3api list-objects-v2 \
--bucket "$BUCKET" \
--prefix "$PREFIX" \
--query 'reverse(sort_by(Contents,&LastModified))[0].Key' \
--output text)That selection is only one part of the process. The script also checks that the download and extracted SQL are nonempty before feeding the dump to psql in the database container. See the full bootstrap and restore code.
Pulling the plug, then checking the data
I simulated losing the on-premises environment and brought up the AWS side from Terraform. The controlled test took 3 minutes 55 seconds from starting terraform apply to validating the application's health endpoint. That is the measured recovery time for this lab run, not a promise that every outage would take four minutes.
I did not want to stop at “the instance is running.” I checked the containers, called /health, and queried /orders to see whether the three original records had come back.

The recovered app returned {"status":"ok"} and the three orders visible before the outage.
The result mattered because it tested the whole chain: backup creation, offsite storage, infrastructure provisioning, application startup, and data restoration. I also destroyed and re-provisioned the AWS infrastructure to check that the process was repeatable.
What I would improve next
This was a capstone lab, not a production deployment. The current demo uses static database credentials in user data, serves the application over HTTP, and requires an operator to start recovery. I would move secrets to a managed store, add TLS and a proper entry point, and plan for DNS failover and monitoring before using this for a real business.
I would also make the bootstrap fail loudly if the restore fails. The current script tolerates a restore-command failure, so a successful Terraform apply by itself is not proof that the data is back. An automated post-restore check should compare expected records and only declare recovery complete when the application and data both pass.
That is the lesson I took away from this project: disaster recovery is a tested process, not a collection of backup files. The infrastructure came up, but the part I cared most about was seeing the original orders come back.

