VDI Admin Quick Guide
Quick guide for setting up Domain Controller and Golden Image for VDI.
Prerequisites: - Windows Server 2022 image - Windows 10/11 Pro or Enterprise image - Meneja platform access - DC Internal IP address will be recorded during setup
Part 1: Domain Controller
Step 1: Create Domain Controller VM
- Create VM from Windows Server 2022 image
- Name:
VDI-DC - Specs: 4 CPU, 8GB RAM, 100GB disk
- Record the internal IP address
Step 2: Install Active Directory Domain Services
- Open Server Manager
- Click Manage → Add Roles and Features
- Click Next until Server Roles
- Select Active Directory Domain Services
- Click Add Features when prompted
- Click Next through remaining screens
- Click Install
- Wait for installation to complete
Step 3: Promote to Domain Controller
- In Server Manager, click the warning notification flag
- Click Promote this server to a domain controller
Configuration:
Deployment:
- Select: Add a new forest
- Domain name: corp.local (or your choice)
Domain Controller Options:
- Functional level: Windows Server 2016 or later
- Enable: DNS server
- Enable: Global Catalog
- Create DSRM password: Complex@Pass123! (record this)
NetBIOS name:
- Auto-fills as CORP (leave default)
Paths: - Leave defaults
Click Install
Server will reboot automatically. Wait 5-10 minutes for reboot to complete.
Step 4: Create Users and Profiles
After reboot, log in as: CORP\Administrator
Open PowerShell as Administrator:
Create Organizational Unit:
Import-Module ActiveDirectory
New-ADOrganizationalUnit -Name "VDI-Users" -Path "DC=corp,DC=local"
Create user accounts:
# Set password for all users
$password = ConvertTo-SecureString "TempPass123!" -AsPlainText -Force
# Create users (repeat for each user)
foreach ($username in @("user1", "user2", "user3")) {
New-ADUser -Name $username `
-SamAccountName $username `
-UserPrincipalName "$username@corp.local" `
-Path "OU=VDI-Users,DC=corp,DC=local" `
-AccountPassword $password `
-Enabled $true `
-PasswordNeverExpires $true
# Create profile folder
New-Item -ItemType Directory -Path "C:\Profiles\$username" -Force
Set-ADUser -Identity $username -ProfilePath "\\$env:COMPUTERNAME\Profiles$\$username"
}
Setup roaming profiles:
# Create profiles folder and share
New-Item -ItemType Directory -Path "C:\Profiles" -Force
New-SmbShare -Name "Profiles$" -Path "C:\Profiles" -FullAccess "Everyone"
Verify everything works:
Get-ADUser -Filter * -SearchBase "OU=VDI-Users,DC=corp,DC=local"
Get-SmbShare -Name "Profiles$"
Domain Controller setup complete.
Part 2: Golden Image
Step 1: Create Golden Image VM
- Create VM from Windows 10/11 Pro or Enterprise image
- Name:
VDI-GoldenImage - Specs: 4 CPU, 8GB RAM, 120GB disk (images already have correct disk size)
- Network 1: Internal (to reach DC)
- Network 2: Public (for RDP access)
Step 2: Configure DNS and Disable Updates
Open PowerShell as Administrator:
Disable Windows Update:
Set-Service -Name wuauserv -StartupType Disabled
Stop-Service -Name wuauserv
Configure DNS:
# Set DNS to DC's internal IP (replace with your DC IP)
Get-NetAdapter | Where-Object {$_.Status -eq "Up"} |
Set-DnsClientServerAddress -ServerAddresses "10.x.x.x" # Replace with DC IP
Verify DNS:
Get-DnsClientServerAddress
nslookup corp.local # Should resolve to DC IP
ping corp.local # Should respond
Step 3: Optimize for VDI
Disable unnecessary services:
# Hibernation
powercfg /h off
# Windows Search
Stop-Service -Name "WSearch" -Force
Set-Service -Name "WSearch" -StartupType Disabled
# Superfetch
Stop-Service -Name "SysMain" -Force
Set-Service -Name "SysMain" -StartupType Disabled
# Telemetry
Stop-Service -Name "DiagTrack" -Force
Set-Service -Name "DiagTrack" -StartupType Disabled
Enable Remote Desktop:
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 1
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
Step 4: Cleanup
⚠️ CRITICAL: Verify NOT domain-joined:
(Get-WmiObject Win32_ComputerSystem).Domain
# Must show: WORKGROUP (not corp.local)
Clean up:
# Remove temp files
Remove-Item "$env:TEMP\*" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "C:\Windows\Temp\*" -Recurse -Force -ErrorAction SilentlyContinue
# Clear event logs
wevtutil el | ForEach-Object { wevtutil cl $_ 2>$null }
# Remove Windows Store apps (they cause Sysprep issues)
Get-AppxPackage -AllUsers | Remove-AppxPackage -ErrorAction SilentlyContinue
# Disable BitLocker if enabled
Disable-BitLocker -MountPoint "C:" -ErrorAction SilentlyContinue
Step 5: Create Domain Join Script and Scheduled Task
NOTE this is just one way to domain join. you can use unattend directly or djoin or otherwise.
Create the domain join PowerShell script:
# Create scripts directory
New-Item -Path "C:\Scripts" -ItemType Directory -Force
# Create the domain join PowerShell script
$joinScript = @'
$logFile = "C:\Windows\Temp\domain-join.log"
function Write-Log {
param($Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$timestamp - $Message" | Out-File -FilePath $logFile -Append -Force
}
try {
Write-Log "===== Domain Join Script Started ====="
Write-Log "Computer Name: $env:COMPUTERNAME"
# Check if already domain joined
$currentDomain = (Get-WmiObject Win32_ComputerSystem).Domain
if ($currentDomain -ne "WORKGROUP") {
Write-Log "Already domain joined to: $currentDomain"
Unregister-ScheduledTask -TaskName "VDI-DomainJoin" -Confirm:$false -ErrorAction SilentlyContinue
Write-Log "Task unregistered. Domain join complete."
exit 0
}
# Wait for network
Write-Log "Waiting 60 seconds for network..."
Start-Sleep -Seconds 60
# Test DNS
Write-Log "Testing DNS for corp.local..."
try {
$dns = Resolve-DnsName corp.local -ErrorAction Stop
Write-Log "DNS OK: $($dns.IPAddress)"
} catch {
Write-Log "DNS failed: $($_.Exception.Message). Will retry on next boot."
exit 1
}
# Domain join
Write-Log "Joining domain corp.local..."
$password = ConvertTo-SecureString "Complex@Pass123!" -AsPlainText -Force
$cred = New-Object PSCredential("CORP\Administrator", $password)
Add-Computer -DomainName corp.local -Credential $cred -Force -ErrorAction Stop
Write-Log "Domain join successful!"
# Unregister task
Unregister-ScheduledTask -TaskName "VDI-DomainJoin" -Confirm:$false -ErrorAction SilentlyContinue
# Restart
Write-Log "Restarting in 10 seconds..."
Start-Sleep -Seconds 10
Restart-Computer -Force
} catch {
Write-Log "ERROR: $($_.Exception.Message)"
Write-Log "Will retry on next boot"
exit 1
}
'@
Set-Content -Path "C:\Scripts\JoinDomain.ps1" -Value $joinScript
Important: Update the
Complex@Pass123!password in the script above to match your actual Domain Administrator password.
Create scheduled task to run at startup:
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File C:\Scripts\JoinDomain.ps1"
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
Register-ScheduledTask -TaskName "VDI-DomainJoin" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "Automatic domain join for VDI"
How it works: This scheduled task runs at every system startup as SYSTEM (no user login required). The script checks if already domain-joined, waits for network, joins the domain, then deletes itself. If it fails, it automatically retries on next boot. All activity is logged to
C:\Windows\Temp\domain-join.logfor troubleshooting.
Create unattend.xml to skip OOBE:
notepad C:\Windows\System32\Sysprep\unattend.xml
Copy this XML into Notepad and Save:
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<TimeZone>UTC</TimeZone>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<HideLocalAccountScreen>true</HideLocalAccountScreen>
<ProtectYourPC>1</ProtectYourPC>
<SkipUserOOBE>true</SkipUserOOBE>
<SkipMachineOOBE>true</SkipMachineOOBE>
</OOBE>
</component>
</settings>
</unattend>
Verify files created:
Test-Path C:\Windows\System32\Sysprep\unattend.xml # Should return: True
Test-Path C:\Scripts\JoinDomain.ps1 # Should return: True
Get-ScheduledTask -TaskName "VDI-DomainJoin" # Should show the task
Step 6: Flush Changes and Create Snapshot
Flush all filesystem changes:
# Sync filesystem
Write-VolumeCache C
# Verify no pending operations
Get-Volume | Format-Table DriveLetter, FileSystem, OperationalStatus
In Meneja:
1. Navigate to the VDI-GoldenImage VM
2. Select Disks tab
3. Select the boot disk
4. Click Create Snapshot
5. Name: pre-sysprep-snapshot
6. Click Create
Note: This snapshot allows you to roll back to this state for future updates or troubleshooting. You can create new template versions by reverting to this snapshot, making updates, and running Sysprep again.
Step 7: Run Sysprep
C:\Windows\System32\Sysprep\sysprep.exe /oobe /generalize /shutdown /mode:vm
VM will shut down automatically. DO NOT boot it again before converting to template.
What happens on VM created from template:
- VM boots and skips OOBE screens (unattend.xml)
- VDI system sets the hostname
- Scheduled task "VDI-DomainJoin" runs at startup
- Script waits 60 seconds for network, then joins domain
- VM joins corp.local domain with its final hostname
- Task deletes itself after successful join
- VM restarts automatically
- After restart, boots to domain login screen
- All activity logged to C:\Windows\Temp\domain-join.log
Step 8: Convert to Template
In Meneja:
1. Navigate to the shutdown VDI-GoldenImage VM
2. Click Convert to Template
3. Enter template name: VDI-Win11-Template-v1
4. Click Convert
Golden Image template created successfully.
Testing
Test the Template
- Create VM from template
- Boot (wait 3-5 minutes for domain join)
- Login screen appears (no OOBE)
- Login:
CORP\user1+Complex@Pass123! - Verify domain:
(Get-WmiObject Win32_ComputerSystem).Domain→ showscorp.local - Test RDP from external machine
- Delete test VM
If login works, template is ready for VDI use.
Troubleshooting
Domain Controller
Can't promote to DC - password error:
$password = ConvertTo-SecureString "YourPassword123!" -AsPlainText -Force
Set-LocalUser -Name "Administrator" -Password $password
Can't import ActiveDirectory module: - Wait 5 minutes after reboot - Services need time to start
Users can't access profiles share:
# Check share exists
Get-SmbShare -Name "Profiles$"
# Fix permissions
icacls C:\Profiles /grant "Everyone:(OI)(CI)F"
Golden Image
Can't resolve domain name: - Check DNS is set to DC's internal IP - Network settings → Adapter → IPv4 → DNS
Sysprep fails - BitLocker:
Disable-BitLocker -MountPoint "C:"
# Wait for decryption to complete
Get-BitLockerVolume -MountPoint "C:"
Sysprep fails - Apps:
Get-AppxPackage -AllUsers | Remove-AppxPackage -ErrorAction SilentlyContinue
Domain join fails:
- Verify DNS points to DC: nslookup corp.local
- Ping DC: ping corp.local
- Check DC is running
Trust relationship error:
Cause: Golden image was domain-joined before Sysprep.
Fix: Create VM from snapshot, boot it, remove from domain, re-Sysprep:
Remove-Computer -WorkgroupName "WORKGROUP" -Force -Restart
# After restart, verify WORKGROUP, then run Sysprep again
RDP not working:
# Enable RDP
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 0
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
Quick Commands
**Check domain:**
```powershell
Get-ADDomain
Get-ADUser -Filter * -SearchBase "OU=VDI-Users,DC=corp,DC=local"
Check DNS:
nslookup corp.local
Get-DnsClientServerAddress
Check RDP:
Get-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections