2026-03-25

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.
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:
If your tools require glibc (most do), Alpine (musl libc) is not compatible.
Best balance between compatibility, documentation and lightweight footprint.
| Distro | Desktop | Idle RAM | Pros | Cons |
|---|---|---|---|---|
| Xubuntu 24.04 | XFCE | ~600-800 MB | Ubuntu repos/PPAs, huge community | Slightly heavier than Debian |
| Debian 12 + XFCE | XFCE | ~400-600 MB | Lightest, rock-solid | Older packages |
| Ubuntu 24.04 | GNOME | ~1.5-2 GB | Best out-of-box experience | Heavy, wastes RAM |
| Fedora 41 XFCE | XFCE | ~600-800 MB | Latest packages | dnf 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.
All of this is done once on your Mac.
brew install awscli
aws configure
# Enter: Access Key ID, Secret Access Key, Region (e.g., us-east-1), Output (json)
Download from docs.aws.amazon.com/dcv and install the macOS .dmg.
aws ec2 create-key-pair \
--key-name devenv-key \
--query 'KeyMaterial' \
--output text > ~/.ssh/devenv-key.pem
chmod 400 ~/.ssh/devenv-key.pem
# 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.
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"
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 | vCPUs | RAM | Cost/hour | Notes |
|---|---|---|---|---|
t3.large | 2 | 8 GB | $0.0832 | Minimum viable: one IDE at a time |
t3.xlarge | 4 | 16 GB | $0.1664 | Recommended: comfortable for most scenarios |
t3.2xlarge | 8 | 32 GB | $0.3328 | Heavy compilation (Rust), multiple IDEs |
m5.xlarge | 4 | 16 GB | $0.192 | Dedicated 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.
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.
sudo apt update && sudo apt upgrade -y
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.
sudo apt install -y lightdm
sudo systemctl enable lightdm
sudo passwd ubuntu
# Choose a strong password, this is your DCV login credential
# 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
# 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
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.
sudo apt install -y \
build-essential curl wget git unzip \
software-properties-common apt-transport-https \
ca-certificates gnupg lsb-release fontconfig libfuse2
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
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
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm use --lts
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
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source ~/.cargo/env
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.
This is the most important part. The goal is to ensure the instance has no ties to your personal accounts.
# 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
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
sudo systemctl disable cups-browsed 2>/dev/null
sudo systemctl disable avahi-daemon 2>/dev/null
sudo systemctl disable bluetooth 2>/dev/null
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
This is your reusable image. Everything you installed gets baked into it.
# 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
# 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."
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.
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
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
| Scenario | Monthly cost |
|---|---|
| AMI idle (no usage) | ~$1/month |
| 4 sessions of 3h (t3.xlarge) | |
| Stop/Start (EBS always active, 80 GB) | |
| Always on (24/7) | ~$127/month |
The AMI approach saves ~50% compared to Stop/Start and ~97% compared to keeping it always on.
sudo systemctl restart lightdm
sudo systemctl restart dcv-server
# If it persists:
sudo reboot
# 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
# 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
# === 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