Safe dev environment on demand with AWS EC2 + AMI

2026-03-25

post-thumb

Contents

There are situations where you need a development environment completely isolated from your main machine: testing third-party repositories, running code you don’t fully trust, working with tools that require native Linux, or simply having a disposable dev desktop that doesn’t compromise your credentials, SSH keys or tokens.

The idea here is simple: set it up once, save it as an image and restore on demand. When you’re done, destroy it. When you need it again, recreate it in minutes. And the cost? About $1/month when idle.


Architecture overview

Day of use:
  AMI Snapshot --> Launch EC2 --> Connect via DCV --> Work --> Terminate EC2

Between uses:
  Only the AMI snapshot exists (~$1/month for 20 GB)
  No running instances, no EBS volumes, no compute charges

Why this approach:

  • You pay ~$1/month when not in use (AMI storage only)
  • Each session starts from a clean, pre-configured image
  • Your macOS stays completely isolated, no shared files or network
  • Amazon DCV provides smooth remote desktop (free on EC2)
  • The instance is destroyed after each use, no residual data

Choosing a distro

If your tools require glibc (most do), Alpine (musl libc) is not compatible.

Best balance between compatibility, documentation and lightweight footprint.

DistroDesktopIdle RAMProsCons
Xubuntu 24.04XFCE~600-800 MBUbuntu repos/PPAs, huge communitySlightly heavier than Debian
Debian 12 + XFCEXFCE~400-600 MBLightest, rock-solidOlder packages
Ubuntu 24.04GNOME~1.5-2 GBBest out-of-box experienceHeavy, wastes RAM
Fedora 41 XFCEXFCE~600-800 MBLatest packagesdnf slower than apt

The strategy: use the official Ubuntu Server 24.04 AMI (free on the AWS Marketplace) and install XFCE on top. Lightweight desktop with full compatibility.


Prerequisites

All of this is done once on your Mac.

1. AWS account and CLI

brew install awscli
aws configure
# Enter: Access Key ID, Secret Access Key, Region (e.g., us-east-1), Output (json)

2. Amazon DCV Client

Download from docs.aws.amazon.com/dcv and install the macOS .dmg.

3. SSH key pair

aws ec2 create-key-pair \
  --key-name devenv-key \
  --query 'KeyMaterial' \
  --output text > ~/.ssh/devenv-key.pem

chmod 400 ~/.ssh/devenv-key.pem

4. Security Group

# Create the security group
aws ec2 create-security-group \
  --group-name devenv-sg \
  --description "Safe dev environment"

# Allow SSH (for initial setup)
aws ec2 authorize-security-group-ingress \
  --group-name devenv-sg \
  --protocol tcp --port 22 \
  --cidr "$(curl -s https://checkip.amazonaws.com)/32"

# Allow DCV (port 8443)
aws ec2 authorize-security-group-ingress \
  --group-name devenv-sg \
  --protocol tcp --port 8443 \
  --cidr "$(curl -s https://checkip.amazonaws.com)/32"

Important: This restricts access to your current public IP only. If your IP changes, update the security group before connecting.


Step 1: Launch the base instance

Find the Ubuntu 24.04 AMI

AMI_ID=$(aws ec2 describe-images \
  --owners 099720109477 \
  --filters \
    "Name=name,Values=ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*" \
    "Name=state,Values=available" \
  --query 'Images | sort_by(@, &CreationDate) | [-1].ImageId' \
  --output text)

echo "Using AMI: $AMI_ID"

Launch the instance

INSTANCE_ID=$(aws ec2 run-instances \
  --image-id "$AMI_ID" \
  --instance-type t3.xlarge \
  --key-name devenv-key \
  --security-groups devenv-sg \
  --block-device-mappings '[{
    "DeviceName": "/dev/sda1",
    "Ebs": {
      "VolumeSize": 80,
      "VolumeType": "gp3",
      "DeleteOnTermination": true
    }
  }]' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=safe-devenv}]' \
  --query 'Instances[0].InstanceId' \
  --output text)

echo "Instance: $INSTANCE_ID"

# Wait for the instance to be running
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"

# Get the public IP
PUBLIC_IP=$(aws ec2 describe-instances \
  --instance-ids "$INSTANCE_ID" \
  --query 'Reservations[0].Instances[0].PublicIpAddress' \
  --output text)

echo "IP: $PUBLIC_IP"

Instance type options

InstancevCPUsRAMCost/hourNotes
t3.large28 GB$0.0832Minimum viable: one IDE at a time
t3.xlarge416 GB$0.1664Recommended: comfortable for most scenarios
t3.2xlarge832 GB$0.3328Heavy compilation (Rust), multiple IDEs
m5.xlarge416 GB$0.192Dedicated CPU, no burst limits

t3 instances are burstable: they earn CPU credits when idle. For typical coding, t3 is sufficient and cheaper. For heavy Rust compilation or test suites, consider m5.


Step 2: Install desktop and DCV

ssh -i ~/.ssh/devenv-key.pem ubuntu@$PUBLIC_IP

Wait ~60 seconds after the instance reports “running” for cloud-init to finish. If you get “Connection refused”, wait and retry.

Update the system

sudo apt update && sudo apt upgrade -y

Install XFCE

sudo apt install -y xfce4 xfce4-goodies xfce4-terminal dbus-x11

# Set XFCE as the default session
echo "xfce4-session" > ~/.xsession

Do not install ubuntu-desktop, it pulls in GNOME and adds ~1.2 GB of RAM overhead.

Display manager

sudo apt install -y lightdm
sudo systemctl enable lightdm

Set a password for the ubuntu user

sudo passwd ubuntu
# Choose a strong password, this is your DCV login credential

Install Amazon DCV

# Import GPG key
wget https://d1uj6qtbmh3dt5.cloudfront.net/NICE-GPG-KEY
gpg --import NICE-GPG-KEY

# Download DCV server
wget https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-ubuntu2404-x86_64.tgz

# Extract and install
tar -xvzf nice-dcv-ubuntu2404-x86_64.tgz
cd nice-dcv-*-x86_64
sudo apt install -y ./nice-dcv-server_*.deb ./nice-dcv-web-viewer_*.deb ./nice-xdcv_*.deb

# Clean up
cd ~ && rm -rf nice-dcv-* NICE-GPG-KEY

Configure DCV

# Enable automatic console session
sudo sed -i '/^\[session-management\/automatic-console-session\]/,/^\[/{s/^#\?owner=.*/owner="ubuntu"/}' \
  /etc/dcv/dcv.conf

# If the section doesn't exist, add it
if ! grep -q "automatic-console-session" /etc/dcv/dcv.conf; then
  sudo tee -a /etc/dcv/dcv.conf > /dev/null <<DCVEOF

[session-management/automatic-console-session]
owner="ubuntu"
DCVEOF
fi

# Enable and start DCV
sudo systemctl enable dcv-server
sudo systemctl start dcv-server

Verify and connect

sudo systemctl status dcv-server
dcv list-sessions
# Should show a "console" session owned by "ubuntu"

On your Mac: open the Amazon DCV Client, connect to $PUBLIC_IP:8443, accept the self-signed certificate and log in with ubuntu and the password you set.


Step 3: Install development tools

Essential packages

sudo apt install -y \
  build-essential curl wget git unzip \
  software-properties-common apt-transport-https \
  ca-certificates gnupg lsb-release fontconfig libfuse2

VS Code

curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
  | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-keyring.gpg

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-keyring.gpg] \
  https://packages.microsoft.com/repos/code stable main" \
  | sudo tee /etc/apt/sources.list.d/vscode.list

sudo apt update && sudo apt install -y code

Warp Terminal

curl -fsSL https://releases.warp.dev/linux/keys/warp.asc \
  | sudo gpg --dearmor -o /usr/share/keyrings/warp-keyring.gpg

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/warp-keyring.gpg] \
  https://releases.warp.dev/linux/deb stable main" \
  | sudo tee /etc/apt/sources.list.d/warp.list

sudo apt update && sudo apt install -y warp-terminal

Node.js (via nvm)

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc

nvm install --lts
nvm use --lts

Go

GO_VERSION="1.23.6"
wget "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz"
rm "go${GO_VERSION}.linux-amd64.tar.gz"

echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc
source ~/.bashrc

Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source ~/.cargo/env

JetBrains Toolbox (GoLand, WebStorm, etc.)

TOOLBOX_URL=$(curl -s "https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release" \
  | grep -oP '"linux":{"link":"\K[^"]+')

wget -O jetbrains-toolbox.tar.gz "$TOOLBOX_URL"
tar -xzf jetbrains-toolbox.tar.gz
sudo mv jetbrains-toolbox-*/jetbrains-toolbox /usr/local/bin/
rm -rf jetbrains-toolbox*

Launch JetBrains Toolbox from the terminal inside the DCV session (not via SSH). It needs a display for the GUI.


Step 4: Security hardening

This is the most important part. The goal is to ensure the instance has no ties to your personal accounts.

Remove personal credentials

# No personal SSH keys
rm -rf ~/.ssh/id_* ~/.ssh/known_hosts

# No Git credentials
git config --global --unset credential.helper 2>/dev/null
rm -f ~/.git-credentials ~/.gitconfig

# Generic identity
git config --global user.name "Dev"
git config --global user.email "dev@localhost"

# No AWS credentials inside the instance
rm -rf ~/.aws/credentials ~/.aws/config

# No browser profiles
rm -rf ~/.mozilla ~/.config/google-chrome ~/.config/chromium

Internal firewall

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 8443/tcp   # DCV
sudo ufw allow 22/tcp     # SSH (you can remove this later if you want)
sudo ufw enable

Disable unnecessary services

sudo systemctl disable cups-browsed 2>/dev/null
sudo systemctl disable avahi-daemon 2>/dev/null
sudo systemctl disable bluetooth 2>/dev/null

Isolation check script

Save as ~/check-isolation.sh inside the instance:

#!/bin/bash
echo "=== Isolation Check ==="

ISSUES=0

check() {
  if [ -e "$1" ]; then
    echo "WARNING: $1 exists -- $2"
    ISSUES=$((ISSUES + 1))
  fi
}

check ~/.ssh/id_rsa "Personal SSH key found"
check ~/.ssh/id_ed25519 "Personal SSH key found"
check ~/.git-credentials "Git credentials found"
check ~/.aws/credentials "AWS credentials found"
check ~/.docker/config.json "Docker login found"
check ~/.npmrc "NPM token may be present"
check ~/.config/gcloud "Google Cloud config found"
check ~/.azure "Azure config found"

GIT_EMAIL=$(git config --global user.email 2>/dev/null || echo "")
if [ -n "$GIT_EMAIL" ] && [ "$GIT_EMAIL" != "dev@localhost" ]; then
  echo "WARNING: Git email is '$GIT_EMAIL' -- should be dev@localhost"
  ISSUES=$((ISSUES + 1))
fi

echo ""
if [ $ISSUES -eq 0 ]; then
  echo "ALL CLEAR -- environment is clean."
else
  echo "$ISSUES issue(s) found. Review above."
fi
chmod +x ~/check-isolation.sh

Step 5: Create the Golden AMI

This is your reusable image. Everything you installed gets baked into it.

Clean up before creating the snapshot

# Clear package cache
sudo apt clean
sudo apt autoremove -y

# Clear temporary files
rm -rf /tmp/* ~/.cache/*

# Clear bash history
history -c
> ~/.bash_history

# Shut down for a clean snapshot
sudo shutdown -h now

Create the AMI (from your Mac)

# Wait for the instance to stop
aws ec2 wait instance-stopped --instance-ids "$INSTANCE_ID"

# Create the AMI
AMI_DEVENV=$(aws ec2 create-image \
  --instance-id "$INSTANCE_ID" \
  --name "safe-devenv-$(date +%Y%m%d)" \
  --description "Safe dev environment: Xubuntu 24.04 + XFCE + DCV + Node/Go/Rust" \
  --no-reboot \
  --query 'ImageId' \
  --output text)

echo "Golden AMI: $AMI_DEVENV"

# Wait for the AMI to be available
aws ec2 wait image-available --image-ids "$AMI_DEVENV"
echo "AMI is ready."

Terminate the base instance

aws ec2 terminate-instances --instance-ids "$INSTANCE_ID"
echo "Instance terminated. Only the AMI snapshot remains."

From this point you only pay for the snapshot: ~$0.05/GB/month. An 80 GB image with ~20 GB used costs about $1/month.


Step 6: Day-to-day usage

Startup script

Save as ~/devenv-start.sh on your Mac:

#!/bin/bash
set -euo pipefail

AMI_ID="ami-XXXXXXXXXXXXXXXXX"  # Your Golden AMI
INSTANCE_TYPE="t3.xlarge"
KEY_NAME="devenv-key"
SG_NAME="devenv-sg"

echo "Launching safe dev environment..."

# Update security group with current IP
MY_IP=$(curl -s https://checkip.amazonaws.com)
SG_ID=$(aws ec2 describe-security-groups \
  --group-names "$SG_NAME" \
  --query 'SecurityGroups[0].GroupId' \
  --output text)

aws ec2 revoke-security-group-ingress --group-id "$SG_ID" \
  --protocol tcp --port 22 --cidr "0.0.0.0/0" 2>/dev/null || true
aws ec2 revoke-security-group-ingress --group-id "$SG_ID" \
  --protocol tcp --port 8443 --cidr "0.0.0.0/0" 2>/dev/null || true

aws ec2 authorize-security-group-ingress --group-id "$SG_ID" \
  --protocol tcp --port 22 --cidr "${MY_IP}/32"
aws ec2 authorize-security-group-ingress --group-id "$SG_ID" \
  --protocol tcp --port 8443 --cidr "${MY_IP}/32"

# Launch
INSTANCE_ID=$(aws ec2 run-instances \
  --image-id "$AMI_ID" \
  --instance-type "$INSTANCE_TYPE" \
  --key-name "$KEY_NAME" \
  --security-group-ids "$SG_ID" \
  --block-device-mappings '[{
    "DeviceName": "/dev/sda1",
    "Ebs": {
      "VolumeSize": 80,
      "VolumeType": "gp3",
      "DeleteOnTermination": true
    }
  }]' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=safe-devenv}]' \
  --query 'Instances[0].InstanceId' \
  --output text)

echo "Instance: $INSTANCE_ID"
echo "Waiting for it to be ready..."

aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"

PUBLIC_IP=$(aws ec2 describe-instances \
  --instance-ids "$INSTANCE_ID" \
  --query 'Reservations[0].Instances[0].PublicIpAddress' \
  --output text)

echo ""
echo "========================================"
echo "  Environment ready!"
echo "  DCV: ${PUBLIC_IP}:8443"
echo "  SSH: ssh -i ~/.ssh/devenv-key.pem ubuntu@${PUBLIC_IP}"
echo "  Instance ID: ${INSTANCE_ID}"
echo "========================================"
chmod +x ~/devenv-start.sh

Teardown script

Save as ~/devenv-stop.sh on your Mac:

#!/bin/bash
set -euo pipefail

echo "Looking for dev environment instances..."

INSTANCE_ID=$(aws ec2 describe-instances \
  --filters \
    "Name=tag:Name,Values=safe-devenv" \
    "Name=instance-state-name,Values=running,stopped" \
  --query 'Reservations[0].Instances[0].InstanceId' \
  --output text)

if [ "$INSTANCE_ID" = "None" ] || [ -z "$INSTANCE_ID" ]; then
  echo "No instance found."
  exit 0
fi

echo "Terminating instance: $INSTANCE_ID"
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID"

echo "Instance terminated. You're only paying for the AMI snapshot."
chmod +x ~/devenv-stop.sh

Estimated cost

ScenarioMonthly cost
AMI idle (no usage)~$1/month
4 sessions of 3h (t3.xlarge)$2 compute + $1 AMI = **$3**
Stop/Start (EBS always active, 80 GB)$6.40 EBS + $2 compute = **$8.40**
Always on (24/7)~$127/month

The AMI approach saves ~50% compared to Stop/Start and ~97% compared to keeping it always on.


Troubleshooting

DCV shows black screen

sudo systemctl restart lightdm
sudo systemctl restart dcv-server
# If it persists:
sudo reboot

Connection refused on DCV

# Check if it's running
sudo systemctl status dcv-server

# Check the port
ss -tlnp | grep 8443

# Check if your current IP matches the security group
curl -s https://checkip.amazonaws.com

Warp Terminal won’t open (GPU issue)

# Force software rendering
LIBGL_ALWAYS_SOFTWARE=1 warp-terminal

# To make it permanent:
sudo sed -i 's|Exec=warp-terminal|Exec=env LIBGL_ALWAYS_SOFTWARE=1 warp-terminal|' \
  /usr/share/applications/dev.warp.Warp.desktop

Quick reference

# === On your Mac ===

# Start environment
~/devenv-start.sh

# Shut down environment
~/devenv-stop.sh

# List your AMIs
aws ec2 describe-images --owners self \
  --query 'Images[*].[ImageId,Name,CreationDate]' \
  --output table

# Delete an old AMI
aws ec2 deregister-image --image-id ami-OLD
aws ec2 delete-snapshot --snapshot-id snap-OLD

# === Inside the instance ===

# Check isolation
~/check-isolation.sh

# Check DCV sessions
dcv list-sessions

# Restart DCV
sudo systemctl restart dcv-server