Skip to main content

Documentation Index

Fetch the complete documentation index at: https://csdocs.chocolatemoo53.com/llms.txt

Use this file to discover all available pages before exploring further.

This guide explains how to create a cloud workstation on AWS using a graphics-accelerated EC2 instance. You can install any game or application you want, run advanced features like DLSS and Ray Tracing, and keep costs as low as possible.

Pricing overview

Instance costs: Running a G4DN.xlarge for 10 hours in us-west-2 typically costs around $2.50.**Storage will cost 0.015 a gig per month.**For example, 128 GB = 1.92/month.Bandwidth: AWS gives you 100 GB free per month. At 25 Mbps, that covers roughly 20 hours of game streaming before any additional charges apply.These figures assume you follow all steps in this guide for maximum cost efficiency and are not using other AWS services that consume extra bandwidth. Instance pricing is best-case — not every instance will be this cheap per hour.

Compared to other providers

AWS gives you more control and can cost less per month than Shadow or Paperspace. Shadow charges a flat $37.99/month in the United States — it may actually be cheaper if you plan to stream for more than 20 hours per month. Paperspace appears to be a good alternative, but GPU costs can approach a dollar per hour, storage costs are high, and new accounts no longer get Windows Server support.
1

Request a limit increase

AWS requires you to request a quota increase before you can launch GPU instances. Open the AWS console and search for Service Quotas.Navigate to AWS Services → Amazon Elastic Compute Cloud (Amazon EC2), then search for G. Select All G and VT Spot Instance Requests and click Request quota increase. Request at least 4 vCPUs (this is the minimum; you can request more).AWS may close your support case — if that happens, reopen it and explain your reasoning again. If your account is too new, they may decline. When writing your request message:
  • State your own reason in your own words
  • Mention the specific applications or workflow you want to support
  • Be polite and thankful
  • Do not copy-paste the example below verbatim
“Hello, I was interested in starting a small Workstation instance on EC2 so I can improve my workflow and access my work from wherever I am. I am simply requesting a limit increase of 4 or 8 vCPUs in the us-blank-2 region.”
Check your email frequently for a response.
2

Create a Windows Server instance

AWS supports many GPU instance types. The most widely available is the G4DN, which uses a Tesla T4 (roughly comparable to an RTX 2080) and runs the vast majority of games successfully. Newer generations offer more performance for similar or lower cost:
  • G5 — closer to 30-series NVIDIA consumer GPUs
  • G6 — closer to 40-series NVIDIA consumer GPUs, with options for partial or full NVIDIA L40
The region you choose does not have to be the closest one geographically. For example, Oregon (us-west-2) may have more G5 and G6 availability than Northern California (us-west-1). Evaluate your options using the AWS spot instance pricing page.Spot instances are cheaper than on-demand and are rarely interrupted. The availability of spot capacity depends on the region — less popular regions may actually have more.Once you’ve decided, log into your AWS dashboard and navigate to EC2, then click the orange Launch instances button.Configure name and tagsUnder Name and tags, click Add additional tags. Enter your instance name and select Volumes under Resource types — this ensures the storage volume gets the same name tag, which matters for the automation scripts later.Choose an instance typeThe G4DN.xlarge is recommended for its wide availability. Select it from the instance type list.Create a key pairUnder Key pair (login), create a new key pair and download the file. You’ll need this to retrieve the Administrator password for your new server.Configure the security group (firewall)Select Edit on the Network settings panel. Add a rule for RDP with source set to My IP — this is required for the initial setup. Opening streaming ports is optional if you plan to use Tailscale (recommended), since Tailscale creates a secure tunnel and you won’t need to expose ports publicly.
If you cannot use Tailscale and need to open Sunshine ports directly, add the following inbound rules to your security group:TCP: 35043 47984 47989 47995 47996 48010UDP: 47998 47999 48000 48010
If you plan to use Amazon DCV without Tailscale, consult the streaming technology page for the port to open. Using Tailscale is more secure and avoids exposing this port publicly.
Configure storage256 GB is the recommended storage size. For larger game libraries, 512 GB is still affordable. When configuring the volume:
  • Change the volume type to GP3 for better performance and value
  • Click the blue Advanced link and uncheck “Delete on termination” — this is critical; if you skip this step, your data will be deleted when the instance stops
Enable spot instancesUnder Advanced details, scroll down and enable the Request Spot Instances option. If you don’t use spot instances, you may get an error since your quota increase was only approved for spot instances.LaunchReview the summary on the right side, then click Launch instance. It can take up to 4 minutes for the instance to fully provision.To retrieve your Administrator password, select your instance with the checkbox, click Connect, and open the RDP client tab. Upload your key file to decrypt the password, then copy the instance’s IP address.Connect to the instance using an RDP client. On Windows, search for “RDP” in the start menu. On macOS, use the Windows App from the App Store. Finally, if you use Linux, install Remmina (this is just a general recommendation):
sudo apt install remmina
3

Run the script

Before running the script, review the streaming technology documentation to decide which streaming option you want to use — Parsec, Sunshine, or Amazon DCV.Generate IAM credentialsThe script needs AWS root keys to download GPU drivers. Visit the IAM security credentials page, scroll down to Access keys, and click Create access key. Save these keys somewhere safe — you’ll need them again. Do not publish them publicly or use them on untrusted devices.Run the scriptOn your server, open PowerShell as Administrator and run:
iex "(New-Object Net.WebClient).DownloadFile('https://is.gd/UTAo8K', '$env:UserProfile\cloudstreaming.zip'); Expand-Archive '$env:UserProfile\cloudstreaming.zip' -Force; & '$env:UserProfile\cloudstreaming\cloudstreaming-main\starthere.ps1'"
Step 2 of the script will ask whether you want to install Tailscale. The recommended answer is yes. Tailscale will open automatically, and you can log in using any supported method or scan a QR code. Once installed, create a free Tailscale account on your personal computer and install the Tailscale app there as well.
If you cannot use Tailscale at all and cannot use subnet routes, you can use Dynamic DNS to get a stable domain name that points to your instance. Duck DNS is a free option (and is itself hosted on AWS).This is needed because without a stable address, Moonlight will become unpaired from your server every time the instance gets a new IP address, requiring you to manually re-pair.To set up Duck DNS:
  1. Create an account using one of the sign-in options at the top of the Duck DNS site
  2. Go to Domains and create a new domain (e.g., myworkstation.duckdns.org)
  3. Enter the IP address you used for RDP into the IP address field
Downsides: This approach is less secure than Tailscale and requires manually updating the IP address each time the instance starts. It is still less work than re-pairing every Moonlight device each time.
Connect to your streaming technologyWhen the script reaches step 3 and installs your video drivers, it will ask you to restart. Before you do, complete the connection steps for your chosen streaming technology:
Make sure you are logged in to Parsec on both your personal computer and the server. They sync automatically — your server will appear in Parsec whenever it is available. That’s all you need to do.
If you see only a black screen after connecting, or you see your desktop but cannot interact with it, try the following:
  1. In Moonlight’s settings, make sure Capture system keys is enabled
  2. Press Windows + P (or Command + P on Mac)
  3. Use the Up arrow once or the Down arrow twice, then press Enter
Your display should become usable at some point during this process. If not, something went wrong during setup that you may have missed. You can open an issue on the CloudStreaming GitHub repository.
Manage bandwidthSet your maximum bitrate to 10–15 Mbps in Parsec or Moonlight (the cap is 25 Mbps) to avoid unexpected bandwidth costs. If your personal computer supports it, enable the H.265 codec in Moonlight — it uses less bandwidth than H.264. Some computers cannot decode H.265 in real time, in which case H.264 is required.Verify your storageAfter stopping your instance for the first time, check the AWS Volumes dashboard after a couple of minutes to confirm your storage volume is still there. If it is gone, your volume was set to delete on termination — you must recreate the instance with that option unchecked.
Do not install games or applications yet. Complete steps 4 and 4.5 first — the reason is explained there.
4

Optimize storage costs

If you plan to use your server only for a short period (a few days) and then delete it, you can skip this step. Just make sure to delete the instance storage when you are fully done. For long-term use, this step saves a significant amount of money.
When your instance is not running, you are still paying for the GP3 volume attached to it. This step automates a conversion: when the instance is terminated, a Lambda function takes a snapshot of your GP3 volume, converts it to a cold HDD (sc1) volume (much cheaper), then deletes the snapshot. When you start the instance again (via the script in step 4.5), it reverses the process.Create the Lambda function (durable)Recently, AWS introduced a feature where you can make a “durable” function which “checkpoint progress and resume after interruptions” making it much more suitable for this exact kind of function, but this does change the fundamentals of the script. Below is an updated script to use for this special mode, and it’s highly recommended you turn it on.
import boto3
import botocore

instance_name = 'Workstation'
instance_region = 'us-west-2'

ec2 = boto3.client('ec2', region_name=instance_region)

def handler(event, context):

    volume = context.step("find-volume", find_volume)

    if not volume:
        print("No available volumes found to delete.")
        return

    context.step("wait-for-existing-snapshot", wait_for_existing_snapshot)

    snapshot_id = context.step("create-snapshot", create_snapshot, volume)

    context.wait(
        "wait-snapshot-complete",
        is_snapshot_complete,
        snapshot_id
    )

    new_volume_id = context.step("create-sc1-volume", create_sc1_volume, volume, snapshot_id)

    context.step("cleanup", cleanup_resources, volume, snapshot_id)

    return new_volume_id


def find_volume():
    volumes = ec2.describe_volumes(
        Filters=[
            {'Name': 'status', 'Values': ['available']},
            {'Name': 'tag:Name', 'Values': [instance_name]}
        ]
    )['Volumes']

    return volumes[0] if volumes else None


def wait_for_existing_snapshot():
    snapshots = ec2.describe_snapshots(
        Filters=[
            {'Name': 'description', 'Values': [f'Snapshot for {instance_name}']},
            {'Name': 'status', 'Values': ['pending']}
        ]
    )['Snapshots']

    if snapshots:
        raise Exception("Snapshot already in progress")  
        # Durable runtime will retry this step later


def create_snapshot(volume):
    response = ec2.create_snapshot(
        VolumeId=volume['VolumeId'],
        Description=f"Snapshot for {instance_name}"
    )
    return response['SnapshotId']


def is_snapshot_complete(snapshot_id):
    snapshots = ec2.describe_snapshots(SnapshotIds=[snapshot_id])['Snapshots']
    return snapshots[0]['State'] == 'completed'


def create_sc1_volume(volume, snapshot_id):
    response = ec2.create_volume(
        SnapshotId=snapshot_id,
        VolumeType='sc1',
        AvailabilityZone=volume['AvailabilityZone'],
        TagSpecifications=[
            {
                'ResourceType': 'volume',
                'Tags': [{'Key': 'Name', 'Value': instance_name}]
            }
        ]
    )
    return response['VolumeId']


def cleanup_resources(volume, snapshot_id):
    ec2.delete_snapshot(SnapshotId=snapshot_id)
    ec2.delete_volume(VolumeId=volume['VolumeId'])
This allows you to not have to worry about your data not being properly saved, unlike the script below, but just in case, the script below is still available for anyone who needs it. Create the Lambda functionSearch for Lambda in the AWS console and click Create function. Select Python as the runtime. Paste the following code into the lambda_function.py editor:
import boto3
import botocore

instance_name = 'yourinstancename'
instance_region = 'your-instance-region'

def lambda_handler(event, context):
    ec2 = boto3.client('ec2')
    
    # Connect to region
    ec2 = boto3.client('ec2',region_name=instance_region)
    res_client = boto3.resource('ec2', region_name=instance_region)
    
    # Get all available volumes    
    volumetodelete = ec2.describe_volumes(Filters=[{'Name': 'status', 'Values': ['available']},
                                        {'Name': 'tag:Name', 'Values': [instance_name]}])['Volumes']

        # Create a snapshot of the GP3 volume
        snapshot_response = ec2.create_snapshot(VolumeId=volumetodelete, Description=f"Snapshot for {instance_name}")
        snapshot_id = snapshot_response['SnapshotId']
        
        # Wait for the snapshot to be completed
        ec2.get_waiter('snapshot_completed').wait(SnapshotIds=[snapshot_id])
        
        print(f'Snapshot {snapshot_id} created.')
        
        # Create a COLD HDD (sc1) volume from the snapshot
        cold_hdd_response = ec2.create_volume(
            SnapshotId=snapshot_id,
            VolumeType='sc1',
            AvailabilityZone=volume['AvailabilityZone'],
            TagSpecifications=[
                {
                    'ResourceType': 'volume',
                    'Tags': [
                        {
                            'Key': 'Name',
                            'Value': instance_name
                        },
                    ]
                },
            ]
        )
        cold_hdd_volume_id = cold_hdd_response['VolumeId']
        
        print(f'New sc1 volume {cold_hdd_volume_id} created.')
        
        # Tag the COLD HDD volume with the instance's name
        ec2.create_tags(Resources=[cold_hdd_volume_id], Tags=[{'Key': 'Name', 'Value': instance_name}])
        
        # Delete the snapshot and GP3 volume
        ec2.delete_snapshot(SnapshotId=snapshot_id)
        print(f'Snapshot {snapshot_id} deleted.')
        ec2.delete_volume(VolumeId=gp3_volume['VolumeId'])
        print(f'GP3 volume {gp3_volume["VolumeId"]} deleted.')
Replace yourinstancename and your-instance-region with your actual values before saving.Set the timeoutGo to the General configuration tab and click Edit. Set the timeout to the maximum of 15 minutes. The first snapshot (and any snapshot after large data changes) can take a long time, and a shorter timeout will cause the function to fail.Grant permissionsStill in the General configuration tab, click the IAM role link at the bottom (labeled something like View the xxxxxxx-role-xxxx role on the IAM console). On the IAM page, click Add permissions and attach the AmazonEC2FullAccess policy. This allows the function to manage your EC2 volumes.Set up the EventBridge triggerSearch for EventBridge in the AWS console and select your region. Create a new rule with any name and description. For the event pattern, configure:
  • Service: EC2
  • Event type: EC2 State-change Notification
  • Specific state(s): terminated
  • Target: Lambda → select the Lambda function you just created
Run the initial conversionYour volume is currently sitting idle as a GP3 volume. You need to convert it manually the first time. Go to the Test tab in the Lambda console and click Invoke. Watch the logs to confirm the function completes successfully.The first run may fail due to timeout if the snapshot takes longer than 15 minutes. This is normal for a large initial snapshot. Subsequent snapshots complete much faster as long as you haven’t made major changes since the last run.
If you install a large number of games or files between sessions, the Lambda function may time out before the snapshot finishes. If this happens, monitor your AWS dashboard after stopping the instance.If you see your GP3 volume still sitting there (not converted), you can convert it manually:
  1. Go to Snapshots and select the snapshot that was created
  2. Click Create volume from snapshot and choose SC1 as the volume type
  3. Delete the original GP3 volume and the snapshot
  4. Name the new SC1 volume the same as your instance name
Once your instance is fully set up with all your applications and games installed, the Lambda function will rarely time out and will handle everything automatically.
5

Automate instance startup

This step sets up a script on your personal computer that starts your instance automatically — converting the SC1 volume back to GP3, creating an AMI, launching a spot instance, and cleaning up — all without logging into the AWS console each time.Install and configure the AWS CLIInstall the AWS CLI on your personal computer. Then open your terminal and run:
aws configure
Enter the credentials you created earlier. Set the output format to json and the region to your workstation’s region.Download and configure the startup scriptChoose the script for your operating system, fill in the values at the top, and save the file.
# Define parameters
$InstanceName = 'yourinstancename'
$TargetInstanceType = 'g4dn.xlarge'  # Specify the desired instance type
$SecurityGroupId = 'sg-xxxxxxxxxxxxxx'  # Specify the desired security group ID
$Region = 'yourinstanceregion'  # Specify your AWS region

# Get the volume ID for the instance
$VolumeId = aws ec2 describe-volumes --filters "Name=tag:Name,Values=$InstanceName" `
    "Name=status,Values=available" "Name=volume-type,Values=sc1" `
    --query "Volumes[0].VolumeId" --output text --region $Region

if (-not $VolumeId -or $VolumeId -eq "None") {
    Write-Error "Error: Unable to retrieve valid volume ID for the specified instance name."
    exit 1
}

# Create a snapshot of the volume
$SnapshotId = aws ec2 create-snapshot --volume-id $VolumeId --description "Snapshot for AMI" `
    --query "SnapshotId" --output text --region $Region

# Wait for the snapshot to be completed
Write-Host "Waiting for snapshot $SnapshotId to complete..."
do {
    Start-Sleep -Seconds 10
    $SnapshotStatus = aws ec2 describe-snapshots --snapshot-ids $SnapshotId --query "Snapshots[0].State" --output text --region $Region
} while ($SnapshotStatus -ne 'completed')

# Register an AMI from the snapshot
$AMIId = aws ec2 register-image --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"SnapshotId":"'$SnapshotId'","VolumeType":"gp3","DeleteOnTermination":false}}]' `
    --name "AMI for $InstanceName" --description "AMI created from cold HDD snapshot" `
    --architecture x86_64 --root-device-name "/dev/sda1" --query "ImageId" --output text --region $Region

Write-Host "AMI $AMIId registered from snapshot $SnapshotId"

# Request a Spot instance with specified parameters
$RequestId = aws ec2 request-spot-instances --instance-count 1 --type "one-time" `
    --launch-specification "{\"ImageId\":\"$AMIId\",\"InstanceType\":\"$TargetInstanceType\",\"SecurityGroupIds\":[\"$SecurityGroupId\"]}" `
    --query "SpotInstanceRequests[0].SpotInstanceRequestId" --output text --region $Region

Write-Host "Spot instance requested with ID: $RequestId"

# Wait for the Spot instance request to be fulfilled
Write-Host "Waiting for Spot instance request $RequestId to be fulfilled..."
do {
    Start-Sleep -Seconds 10
    $SpotRequestStatus = aws ec2 describe-spot-instance-requests --spot-instance-request-ids $RequestId --query "SpotInstanceRequests[0].Status.Code" --output text --region $Region
} while ($SpotRequestStatus -ne 'fulfilled')

# Get the Spot instance ID
$InstanceId = aws ec2 describe-spot-instance-requests --spot-instance-request-ids $RequestId --query "SpotInstanceRequests[0].InstanceId" --output text --region $Region

if (-not $InstanceId -or $InstanceId -eq "None") {
    Write-Error "Error: Unable to get spot instance ID, it may not have provisioned. Try again later."
    Write-Host "Deleting AMI and snapshot..."
    aws ec2 deregister-image --image-id $AMIId --region $Region
    aws ec2 delete-snapshot --snapshot-id $SnapshotId --region $Region
    exit 1
}

Write-Host "Spot instance $InstanceId launched from AMI $AMIId with instance type $TargetInstanceType, gp3 volume, and security group $SecurityGroupId"

# Tagging the launched instance with the specified name tag
aws ec2 create-tags --resources $InstanceId --tags Key=Name,Value=$InstanceName --region $Region

# Get GP3 volume
$GP3VolumeId = aws ec2 describe-volumes --filters "Name=volume-type,Values=gp3" --query "Volumes[0].VolumeId" --output text --region $Region

# Tagging the storage with the specified name tag
aws ec2 create-tags --resources $GP3VolumeId --tags Key=Name,Value=$InstanceName --region $Region

Write-Host "Tags added to the launched instance and storage."

Write-Host "Deleting cold HDD, AMI, and snapshot..."
aws ec2 deregister-image --image-id $AMIId --region $Region
aws ec2 delete-snapshot --snapshot-id $SnapshotId --region $Region
aws ec2 delete-volume --volume-id $VolumeId --region $Region
Save as a .ps1 file. To run it, open PowerShell, navigate to the directory where the file is saved, and run:
./start-server.ps1
Use a text editor like Notepad++ (Windows) or TextEdit (macOS) to fill in the values at the top of the file before saving.
Monitor the AWS dashboard while this script runs. If something goes wrong mid-execution, you could incur unexpected charges or accidentally delete data. Check the dashboard periodically to make sure everything is proceeding as expected.

Enjoy your instance!

You now have a cost-effective, GPU-accelerated workstation on AWS. You got the best pricing by using spot instances, automated driver setup with the CloudStreaming script, reduced storage costs with the SC1/GP3 Lambda conversion, simplified instance startup with the automation script, and stable connectivity via Tailscale or Dynamic DNS — all without paying for a static IP address.