Skip to content

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

  1. Create VM from Windows Server 2022 image
  2. Name: VDI-DC
  3. Specs: 4 CPU, 8GB RAM, 100GB disk
  4. Record the internal IP address

Step 2: Install Active Directory Domain Services

  1. Open Server Manager
  2. Click ManageAdd Roles and Features
  3. Click Next until Server Roles
  4. Select Active Directory Domain Services
  5. Click Add Features when prompted
  6. Click Next through remaining screens
  7. Click Install
  8. Wait for installation to complete

Step 3: Promote to Domain Controller

  1. In Server Manager, click the warning notification flag
  2. 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

  1. Create VM from Windows 10/11 Pro or Enterprise image
  2. Name: VDI-GoldenImage
  3. Specs: 4 CPU, 8GB RAM, 120GB disk (images already have correct disk size)
  4. Network 1: Internal (to reach DC)
  5. 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.log for 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

  1. Create VM from template
  2. Boot (wait 3-5 minutes for domain join)
  3. Login screen appears (no OOBE)
  4. Login: CORP\user1 + Complex@Pass123!
  5. Verify domain: (Get-WmiObject Win32_ComputerSystem).Domain → shows corp.local
  6. Test RDP from external machine
  7. 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