How to deploy to AWS 
⚠️ Future Feature: This guide describes the planned AWS deployment architecture for production environments. The deployment automation and Infrastructure as Code templates are in development.
This guide walks you through deploying the Panels application to Amazon Web Services using best practices for production environments.
Prerequisites 
- AWS account with appropriate permissions
- AWS CLI installed and configured
- Docker installed locally
- Terraform installed (optional, for Infrastructure as Code)
- Domain name for your application (recommended)
Step 1: Set Up AWS Resources 
Option A: Manual Setup (Quick Start) 
Create VPC and Subnets 
bash
# Create VPC
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=panels-vpc}]'
# Create public subnets (for load balancer)
aws ec2 create-subnet --vpc-id vpc-xxxxxx --cidr-block 10.0.1.0/24 --availability-zone us-west-2a
aws ec2 create-subnet --vpc-id vpc-xxxxxx --cidr-block 10.0.2.0/24 --availability-zone us-west-2b
# Create private subnets (for application)
aws ec2 create-subnet --vpc-id vpc-xxxxxx --cidr-block 10.0.3.0/24 --availability-zone us-west-2a
aws ec2 create-subnet --vpc-id vpc-xxxxxx --cidr-block 10.0.4.0/24 --availability-zone us-west-2bCreate Security Groups 
bash
# Security group for load balancer
aws ec2 create-security-group \
  --group-name panels-alb-sg \
  --description "Security group for Panels ALB" \
  --vpc-id vpc-xxxxxx
# Allow HTTP and HTTPS traffic
aws ec2 authorize-security-group-ingress \
  --group-id sg-xxxxxx \
  --protocol tcp \
  --port 80 \
  --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
  --group-id sg-xxxxxx \
  --protocol tcp \
  --port 443 \
  --cidr 0.0.0.0/0Option B: Infrastructure as Code (Recommended) 
Create a Terraform configuration:
hcl
# infrastructure/main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
provider "aws" {
  region = var.aws_region
}
# VPC Configuration
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  
  name = "panels-vpc"
  cidr = "10.0.0.0/16"
  
  azs             = ["${var.aws_region}a", "${var.aws_region}b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
  
  enable_nat_gateway = true
  enable_vpn_gateway = false
  
  tags = {
    Environment = var.environment
    Project     = "panels"
  }
}
# RDS Database
resource "aws_db_instance" "postgres" {
  identifier = "panels-${var.environment}"
  
  engine         = "postgres"
  engine_version = "16.1"
  instance_class = "db.t3.micro"
  
  allocated_storage     = 20
  max_allocated_storage = 100
  storage_encrypted     = true
  
  db_name  = "panels"
  username = var.db_username
  password = var.db_password
  
  vpc_security_group_ids = [aws_security_group.database.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name
  
  backup_retention_period = 7
  backup_window          = "03:00-04:00"
  maintenance_window     = "sun:04:00-sun:05:00"
  
  skip_final_snapshot = var.environment != "production"
  
  tags = {
    Environment = var.environment
    Project     = "panels"
  }
}
# ElastiCache Redis
resource "aws_elasticache_subnet_group" "main" {
  name       = "panels-cache-subnet"
  subnet_ids = module.vpc.private_subnets
}
resource "aws_elasticache_replication_group" "redis" {
  replication_group_id       = "panels-${var.environment}"
  description                = "Redis cluster for Panels"
  
  node_type            = "cache.t3.micro"
  port                 = 6379
  parameter_group_name = "default.redis7"
  
  num_cache_clusters = 2
  
  subnet_group_name  = aws_elasticache_subnet_group.main.name
  security_group_ids = [aws_security_group.cache.id]
  
  at_rest_encryption_enabled = true
  transit_encryption_enabled = true
  
  tags = {
    Environment = var.environment
    Project     = "panels"
  }
}Apply the infrastructure:
bash
cd infrastructure
terraform init
terraform plan -var-file="environments/${ENVIRONMENT}.tfvars"
terraform apply -var-file="environments/${ENVIRONMENT}.tfvars"Step 2: Build and Push Docker Images 
Build Application Images 
bash
# Build API image
docker build -t panels-api:latest -f apps/services/Dockerfile .
# Build frontend image  
docker build -t panels-app:latest -f apps/app/Dockerfile .
# Tag images for ECR
docker tag panels-api:latest ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/panels-api:latest
docker tag panels-app:latest ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/panels-app:latestCreate ECR Repositories 
bash
# Create ECR repositories
aws ecr create-repository --repository-name panels-api
aws ecr create-repository --repository-name panels-app
# Get login token and authenticate Docker
aws ecr get-login-password --region $AWS_REGION | \
  docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.comPush Images to ECR 
bash
# Push images
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/panels-api:latest
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/panels-app:latestStep 3: Set Up ECS Cluster 
Create ECS Cluster 
bash
# Create ECS cluster
aws ecs create-cluster --cluster-name panels-cluster --capacity-providers FARGATECreate Task Definitions 
json
// ecs/api-task-definition.json
{
  "family": "panels-api",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::ACCOUNT:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::ACCOUNT:role/ecsTaskRole",
  "containerDefinitions": [
    {
      "name": "panels-api",
      "image": "ACCOUNT.dkr.ecr.REGION.amazonaws.com/panels-api:latest",
      "portMappings": [
        {
          "containerPort": 3001,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {
          "name": "NODE_ENV",
          "value": "production"
        },
        {
          "name": "DATABASE_HOST",
          "value": "panels-production.xxxxxx.us-west-2.rds.amazonaws.com"
        },
        {
          "name": "REDIS_HOST", 
          "value": "panels-production.xxxxxx.cache.amazonaws.com"
        }
      ],
      "secrets": [
        {
          "name": "DATABASE_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:us-west-2:ACCOUNT:secret:panels/database:password::"
        },
        {
          "name": "JWT_SECRET",
          "valueFrom": "arn:aws:secretsmanager:us-west-2:ACCOUNT:secret:panels/jwt:secret::"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/panels-api",
          "awslogs-region": "us-west-2",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:3001/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3
      }
    }
  ]
}Register Task Definitions 
bash
# Register task definitions
aws ecs register-task-definition --cli-input-json file://ecs/api-task-definition.json
aws ecs register-task-definition --cli-input-json file://ecs/app-task-definition.jsonStep 4: Set Up Application Load Balancer 
Create Application Load Balancer 
bash
# Create ALB
aws elbv2 create-load-balancer \
  --name panels-alb \
  --subnets subnet-xxxxxx subnet-yyyyyy \
  --security-groups sg-xxxxxx \
  --scheme internet-facing \
  --type applicationCreate Target Groups 
bash
# API target group
aws elbv2 create-target-group \
  --name panels-api-tg \
  --protocol HTTP \
  --port 3001 \
  --vpc-id vpc-xxxxxx \
  --target-type ip \
  --health-check-path /health \
  --health-check-interval-seconds 30
# Frontend target group
aws elbv2 create-target-group \
  --name panels-app-tg \
  --protocol HTTP \
  --port 3000 \
  --vpc-id vpc-xxxxxx \
  --target-type ip \
  --health-check-path /healthConfigure Listeners 
bash
# HTTPS listener (requires SSL certificate)
aws elbv2 create-listener \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-west-2:ACCOUNT:loadbalancer/app/panels-alb/xxxxxx \
  --protocol HTTPS \
  --port 443 \
  --certificates CertificateArn=arn:aws:acm:us-west-2:ACCOUNT:certificate/xxxxxx \
  --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-west-2:ACCOUNT:targetgroup/panels-app-tg/xxxxxx
# API listener rule
aws elbv2 create-rule \
  --listener-arn arn:aws:elasticloadbalancing:us-west-2:ACCOUNT:listener/app/panels-alb/xxxxxx/xxxxxx \
  --conditions Field=path-pattern,Values='/api/*' \
  --priority 100 \
  --actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-west-2:ACCOUNT:targetgroup/panels-api-tg/xxxxxxStep 5: Create ECS Services 
API Service 
bash
aws ecs create-service \
  --cluster panels-cluster \
  --service-name panels-api \
  --task-definition panels-api:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-xxxxxx,subnet-yyyyyy],securityGroups=[sg-xxxxxx],assignPublicIp=DISABLED}" \
  --load-balancers targetGroupArn=arn:aws:elasticloadbalancing:us-west-2:ACCOUNT:targetgroup/panels-api-tg/xxxxxx,containerName=panels-api,containerPort=3001Frontend Service 
bash
aws ecs create-service \
  --cluster panels-cluster \
  --service-name panels-app \
  --task-definition panels-app:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-xxxxxx,subnet-yyyyyy],securityGroups=[sg-xxxxxx],assignPublicIp=DISABLED}" \
  --load-balancers targetGroupArn=arn:aws:elasticloadbalancing:us-west-2:ACCOUNT:targetgroup/panels-app-tg/xxxxxx,containerName=panels-app,containerPort=3000Step 6: Configure Secrets Management 
Store Secrets in AWS Secrets Manager 
bash
# Database password
aws secretsmanager create-secret \
  --name "panels/database" \
  --description "Database credentials for Panels" \
  --secret-string '{"username":"panels","password":"your-secure-password"}'
# JWT secret
aws secretsmanager create-secret \
  --name "panels/jwt" \
  --description "JWT secret for Panels" \
  --secret-string '{"secret":"your-jwt-secret"}'
# API keys
aws secretsmanager create-secret \
  --name "panels/api-keys" \
  --description "External API keys for Panels" \
  --secret-string '{"medplum_client_secret":"your-medplum-secret"}'Step 7: Set Up Monitoring and Logging 
CloudWatch Log Groups 
bash
# Create log groups
aws logs create-log-group --log-group-name /ecs/panels-api
aws logs create-log-group --log-group-name /ecs/panels-app
aws logs create-log-group --log-group-name /aws/ecs/panels-clusterCloudWatch Alarms 
bash
# High CPU alarm
aws cloudwatch put-metric-alarm \
  --alarm-name "panels-api-high-cpu" \
  --alarm-description "Alarm when API CPU exceeds 80%" \
  --metric-name CPUUtilization \
  --namespace AWS/ECS \
  --statistic Average \
  --period 300 \
  --threshold 80 \
  --comparison-operator GreaterThanThreshold \
  --dimensions Name=ServiceName,Value=panels-api Name=ClusterName,Value=panels-cluster \
  --evaluation-periods 2
# Database connection alarm
aws cloudwatch put-metric-alarm \
  --alarm-name "panels-db-connections" \
  --alarm-description "Alarm when DB connections are high" \
  --metric-name DatabaseConnections \
  --namespace AWS/RDS \
  --statistic Average \
  --period 300 \
  --threshold 80 \
  --comparison-operator GreaterThanThreshold \
  --dimensions Name=DBInstanceIdentifier,Value=panels-production \
  --evaluation-periods 2Step 8: Configure Auto Scaling 
ECS Service Auto Scaling 
bash
# Register scalable target
aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/panels-cluster/panels-api \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 \
  --max-capacity 10
# Create scaling policy
aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/panels-cluster/panels-api \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name panels-api-scaling-policy \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 70.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
    },
    "ScaleOutCooldown": 300,
    "ScaleInCooldown": 300
  }'Step 9: Set Up SSL/TLS 
Request SSL Certificate 
bash
# Request certificate from ACM
aws acm request-certificate \
  --domain-name panels.yourdomain.com \
  --domain-name *.panels.yourdomain.com \
  --validation-method DNS \
  --subject-alternative-names api.panels.yourdomain.comConfigure Route 53 (if using) 
bash
# Create hosted zone
aws route53 create-hosted-zone \
  --name panels.yourdomain.com \
  --caller-reference $(date +%s)
# Create A record pointing to ALB
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "panels.yourdomain.com",
        "Type": "A",
        "AliasTarget": {
          "DNSName": "panels-alb-xxxxxxxxx.us-west-2.elb.amazonaws.com",
          "EvaluateTargetHealth": false,
          "HostedZoneId": "Z1H1FL5HABSF5"
        }
      }
    }]
  }'Step 10: Deploy and Verify 
Run Database Migrations 
bash
# Connect to ECS task to run migrations
aws ecs run-task \
  --cluster panels-cluster \
  --task-definition panels-api:1 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-xxxxxx],securityGroups=[sg-xxxxxx],assignPublicIp=ENABLED}" \
  --overrides '{
    "containerOverrides": [{
      "name": "panels-api",
      "command": ["npm", "run", "migration:run"]
    }]
  }'Verify Deployment 
bash
# Check service status
aws ecs describe-services --cluster panels-cluster --services panels-api panels-app
# Check target group health
aws elbv2 describe-target-health --target-group-arn arn:aws:elasticloadbalancing:us-west-2:ACCOUNT:targetgroup/panels-api-tg/xxxxxx
# Test endpoints
curl https://panels.yourdomain.com/health
curl https://panels.yourdomain.com/api/healthBest Practices 
Security 
- Use IAM roles with least privilege access
- Enable VPC Flow Logs for network monitoring
- Use AWS Secrets Manager for sensitive data
- Enable encryption at rest and in transit
- Regularly rotate secrets and certificates
Performance 
- Use CloudFront for static asset caching
- Enable ALB access logs for monitoring
- Set up RDS read replicas for read-heavy workloads
- Use ElastiCache for application caching
- Monitor and optimize database queries
Cost Optimization 
- Use Spot instances for non-critical workloads
- Set up billing alerts for cost monitoring
- Right-size instances based on usage patterns
- Use Reserved Instances for predictable workloads
- Enable AWS Cost Explorer for cost analysis
Troubleshooting 
Common Issues 
Q: ECS tasks fail to start
- Check CloudWatch logs for error messages
- Verify IAM permissions for task execution role
- Ensure ECR images are accessible
- Validate environment variables and secrets
Q: Load balancer health checks fail
- Verify security group rules allow ALB to reach targets
- Check application health check endpoint
- Ensure correct target group configuration
- Review ECS service network configuration
Q: Database connection issues
- Verify security group allows connections from ECS
- Check database subnet group configuration
- Validate connection string and credentials
- Ensure database is in available state
Q: High costs
- Review CloudWatch billing dashboard
- Check for over-provisioned resources
- Consider using Spot instances
- Optimize data transfer costs
Next Steps 
- How to set up monitoring - Configure comprehensive monitoring
- How to backup data - Set up automated backups
- How to scale the system - Scale for high availability
- Terraform modules - Use Infrastructure as Code
Related Topics 
- AWS deployment architecture - Understand the design decisions
- Security best practices - Secure your deployment
- Monitoring setup - Monitor your application