Loosely Decoupled
GuidePowerShellAutomation

Move a 7 Days to Die Dedicated Server to a New Windows Server by Restoring Its Last Backup

A 7 Days to Die dedicated server running the old world on a new Windows Server, its build and save files checked against the old one before players move.

Two men carry a glass case holding a tiny glowing landscape across a dark field in a wheelbarrow, from a dark shed toward a lit one. The tiny landscape is cyan.
On this page

TL;DR

Move the world in this order: stop the old server, take one last backup of it, build the new server, restore that backup onto it, check the world, and only then give players the new address. Stopping first means nothing can be played on the old server after the backup that the new one doesn’t have. The world and the server’s settings are the only things the new machine can’t get from Steam, and the backup holds both. That holds for a Navezgane world. A random-gen world also keeps its terrain in GeneratedWorlds, which this method doesn’t move, and mods aren’t archived either.

On the old server, in an elevated PowerShell with Stop-GameServer from the part on starting and stopping the server defined:

PowerShell
$telnet = Read-Host 'Telnet password (Enter if none)' -AsSecureString
Stop-GameServer -Password $telnet
Start-ScheduledTask -TaskName 7DaysToDieServerS3Backup

Step 1 waits for the task, checks that the backup started after the stop, and turns off the old server’s tasks. On the new server, install the game and its prerequisites with the earlier parts. Then download the newest archive as SYSTEM, copy myserver.xml out of it, and run Restore-GameWorld from the part on restoring without merging. The game-specific parts are the save folder, the config file and the log lines that show the world loaded.

The step most likely to stop you is the download. The backup task wrote the archive with the credentials in SYSTEM’s profile, so the download has to run as SYSTEM too, and the backup user’s policy has to allow s3:GetObject. The CloudFormation template in the backup article creates a user whose policy does.

The backup article started from a goal: a server I can replace without a second thought. This part is the replacing: the migration of a dedicated server from one Windows machine to another, world and settings included. My server runs on Windows Server 2019, and I want it on 2025. I rehearsed the move on two Windows Server 2025 machines. The world was a new Navezgane that nobody had played, so what follows is the order and the commands, not a move with players in it.

1. Stop the old server and take the last backup

Do these on the old server, in this order. Stopping the server first closes the window in which a player could change the world after the backup. The backup then copies a world that nothing is writing to, which the backup article lists under “Saves are captured mid-write” as the case it can’t promise.

PowerShell
$task = '7DaysToDieServerS3Backup'
Disable-ScheduledTask -TaskName $task | Out-Null
while ((Get-ScheduledTask -TaskName $task).State -eq 'Running') { Start-Sleep -Seconds 5 }
$telnet = Read-Host 'Telnet password (Enter if none)' -AsSecureString
Stop-GameServer -Password $telnet
$stopped = Get-Date
$stopped = $stopped.AddTicks(-($stopped.Ticks % [TimeSpan]::TicksPerSecond))
Enable-ScheduledTask -TaskName $task | Out-Null
Start-ScheduledTask -TaskName $task
Start-Sleep -Seconds 5
while ((Get-ScheduledTask -TaskName $task).State -eq 'Running') {
    Start-Sleep -Seconds 1
}
$info = Get-ScheduledTaskInfo -TaskName $task
if ($info.LastRunTime -lt $stopped) {
    throw 'The backup that finished started before the server stopped. Run it again.'
}
$info.LastTaskResult
Disable-ScheduledTask -TaskName 7DaysToDieServer
Disable-ScheduledTask -TaskName $task

The password is read as a secure string, so it stays out of the console history. With no telnet password set, press Enter and nothing is sent. The last two lines turn off the old server’s tasks before anyone is told the new address. 7DaysToDieServer starts the server at every boot, so a reboot would otherwise bring the old world back where a player could still reach it. The backup task would write an archive of the old world with a newer name than any the new server makes. A restore that takes the newest archive would then pick it. Both tasks read Disabled afterwards.

Disabling the backup task first keeps its daily trigger from starting a run while the server stops, and the first loop waits out a run that had already begun, because starting a running task does nothing and a backup that began before the stop could hold a world that was still being written. The task is enabled again only to take this last backup. Task Scheduler records the last run time to the second, so $stopped is cut to the second too, and the check fails only for a run that started before the stop. If the daily time falls in this window, run the step again.

The result must be 0. The script reads the object back from S3 and compares its size before it exits 0, so a 0 means the archive is in the bucket. The newest log under backup-logs names it:

Text
Upload successful: 1806071 bytes, STANDARD_IA

The backup took seven seconds here and the archive was 1.8 MB, because the world was new. Note the archive’s name, which is a timestamp. On a world people have played it is larger.

This step assumes the backup task from the backup article is already running on the old server, with its script archiving myserver.xml, as the configuration part says to change it. If the archive holds the shipped serverconfig.xml instead, your settings exist only on the old server, and step 4 has a way to copy them.

2. Build the new server

On the new server, in an elevated PowerShell, follow the earlier parts for what each owns:

  1. Install SteamCMD and the server. It took about eight minutes here, which is most of the time the move takes.
  2. Install the Visual C++ runtime from the configuration part, step 1, and open the firewall ports from its step 5. Skip its steps 2 to 4, because the settings come from the archive.
  3. Install AWS Tools for all users, from step 1 of the backup article. The runtime and the AWS modules together took 25 seconds.
  4. Write the backup user’s access key into SYSTEM’s profile, as step 2 of the backup article does, and the bucket name into C:\Scripts\backup-config.txt, as its step 3 does. The new server uses the same key as the old one.

This part moves a world on one of the maps the game ships, such as Navezgane. It doesn’t move a random-gen world: that one also needs its GeneratedWorlds folder, and Restore-GameWorld refuses an archive that holds one, so steps 3 and 4 stop there. For a random-gen world, copy Saves and GeneratedWorlds across by hand with both servers stopped instead; I didn’t test that. Don’t start the new server yet.

3. Download the latest archive as SYSTEM

The key is in SYSTEM’s profile, so the download runs as SYSTEM, the way the backup does. A scheduled task is the way the backup article runs something as SYSTEM. None of the earlier parts downloads an archive this way, so this step is new. Save this as C:\Scripts\Get-LatestBackup.ps1:

Get-LatestBackup.ps1
$ErrorActionPreference = 'Stop'
Import-Module AWS.Tools.S3
$bucket = (Get-Content 'C:\Scripts\backup-config.txt' -First 1).Trim()
$prefix = '7days-backups/'
$latest = Get-S3Object -BucketName $bucket -Prefix $prefix |
    Where-Object { $_.Key -match '7dtd-backup-[^/]+\.zip$' } |
    Sort-Object LastModified -Descending | Select-Object -First 1
if (-not $latest) { throw "No archive under $prefix in $bucket." }
New-Item -ItemType Directory -Path C:\backups -Force | Out-Null
$zip = Join-Path C:\backups (Split-Path $latest.Key -Leaf)
Read-S3Object -BucketName $bucket -Key $latest.Key -File $zip | Out-Null
$size = (Get-Item $zip).Length
if ($size -ne $latest.Size) { throw "Size mismatch: S3 $($latest.Size), local $size." }
"Downloaded $zip ($size bytes)"

It picks the archive that S3 says is newest, downloads it to C:\backups, and stops if the local size differs from the object’s. Then run it once as SYSTEM, wait for it and remove the task:

PowerShell
$command = '& C:\Scripts\Get-LatestBackup.ps1 *> C:\Scripts\get-latest.log'
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument "-NoProfile -ExecutionPolicy Bypass -Command `"$command`""
Register-ScheduledTask -TaskName 'GetLatestBackup' -Action $action `
    -User 'SYSTEM' -RunLevel Highest | Out-Null
Start-ScheduledTask -TaskName 'GetLatestBackup'
Start-Sleep -Seconds 3
while ((Get-ScheduledTask -TaskName 'GetLatestBackup').State -eq 'Running') {
    Start-Sleep -Seconds 1
}
(Get-ScheduledTaskInfo -TaskName 'GetLatestBackup').LastTaskResult
Get-Content C:\Scripts\get-latest.log
Unregister-ScheduledTask -TaskName 'GetLatestBackup' -Confirm:$false

The result is 0 and the log reads:

Text
Downloaded C:\backups\7dtd-backup-20261005-063440.zip (1806071 bytes)

The name is the one step 1 noted. The download took five seconds.

4. Copy the settings and restore the world saves

The restore function doesn’t touch settings: its archive’s serverconfig.xml or myserver.xml stays in the zip. A move needs them, so copy the settings file out of the archive first:

PowerShell
$zip = (Get-ChildItem C:\backups\7dtd-backup-*.zip |
    Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName
$unpacked = Join-Path $env:TEMP 'archive-config'
Expand-Archive -LiteralPath $zip -DestinationPath $unpacked -Force
Copy-Item (Join-Path $unpacked 'myserver.xml') C:\7DaysToDieServer\myserver.xml
Remove-Item $unpacked -Recurse

The copy from the archive had the same SHA-256 as the old server’s file, so the name, password, slots and UserDataFolder came across with it. UserDataFolder puts Saves beside the executable, and the restore function looks for it there. If the archive holds serverconfig.xml and not myserver.xml, it has the shipped settings: copy your myserver.xml from the old server by hand.

Then define Restore-GameWorld from the restore part and run it on the archive:

PowerShell
Restore-GameWorld -ArchivePath $zip
Command
PS> Restore-GameWorld -ArchivePath $zip

Output

Restored Saves from 7dtd-backup-20261005-063440.zip.

There was no old Saves on the new server, so the function printed no line about one, and it started nothing because no server was running. It finished in under a second. It compared every restored file with the archive by name and size before it returned.

5. Start the server and check the world

Register the task from step 1 of the part on starting at boot and start it with its step 3. The server was ready 54 seconds after the start here. Then read three things.

The log names the world and the game name from the settings you copied, and the world loading:

PowerShell
Select-String -Path C:\7DaysToDieServer\output_log.txt `
    -Pattern 'GamePref.GameName =', 'GamePref.GameWorld =', 'createWorld',
    'StartGame done'

Each log line starts with a timestamp, which is left out here.

Text
GamePref.GameName = MyGame
GamePref.GameWorld = Navezgane
INF createWorld: Navezgane, Navezgane (src: GameData, DeviceLocal), MyGame, GameModeSurvival
INF createWorld() done
INF StartGame done

The build matches the old server’s, read on both machines:

PowerShell
Select-String -Pattern '"buildid"' `
    -Path C:\7DaysToDieServer\steamapps\appmanifest_294420.acf

Both read 24994542. If yours differ, Steam published a build between the two installs and the new server is the newer one. The update part covers how the server’s build is compared with the one Steam publishes. I didn’t test loading an older world on a newer build.

Then stop the new server with Stop-GameServer -Password $telnet, the function from step 4 of the start-at-boot part, with $telnet read as in step 1. It exited with code 0 in four seconds, the task read Ready with a last result of 0, and the log had SaveAndCleanupWorld. Run this on both machines, once the new server has stopped, and compare the two outputs:

PowerShell
$root = 'C:\7DaysToDieServer\Saves'
Get-ChildItem $root -Recurse -File | Where-Object Extension -ne '.bak' |
    Sort-Object FullName | ForEach-Object {
        '{0} {1} {2}' -f $_.FullName.Substring($root.Length), $_.Length,
            (Get-FileHash $_.FullName).Hash
    } | Out-File C:\saves-manifest.txt -Encoding ascii

Copy the old server’s file to the new one as C:\old-manifest.txt and run Compare-Object (Get-Content C:\old-manifest.txt) (Get-Content C:\saves-manifest.txt). No output means the two lists are identical, and here they were: 61 files, size for size and hash for hash. To see it report a difference, I changed one byte of sdcs_profiles.sdf on the new server and ran the command again. It printed that file’s line twice, once for each hash, with => on the new server’s line and <= on the old one’s, and printed nothing again once I put the byte back. The server had started and stopped on the new machine, and the old one had not run since its backup. The .bak files are left out because the server writes new ones when it starts, and the new machine’s differed from the old. On a world people played, I can’t say whether a start and a stop leaves every other hash alone.

6. Move the players and the backups

Start the new server again with Start-ScheduledTask -TaskName 7DaysToDieServer, and give players its address. Whether they can reach it depends on the network in front of it, which the configuration part leaves to you.

Then save the script and register the task on the new server with steps 4 and 5 of the backup article. Change the script’s $Config line to myserver.xml, as the configuration part says, or the new archives hold the shipped settings. Run the task once from the scheduler. Here it exited 0 and wrote a second archive, 1.8 MB, and a listing of the bucket as SYSTEM showed both archives.

The old server’s tasks have been off since step 1; leave it stopped. Deleting it is yours to do, after the new one has made good backups.

Check that the key reaches only its bucket

The key should reach one bucket and no other. As SYSTEM on the new server, a listing of a different bucket that exists, one the user’s policy doesn’t name, was refused. I tried one other bucket:

Error

User: arn:aws:iam::<account>:user/<backup user> is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::<other bucket>" because no identity-based policy allows the s3:ListBucket action

A download of an archive in the right bucket succeeded with the same credentials, which is what step 3 did.

Common failures

Download fails with is not authorized to perform: s3:GetObject

Error

User: arn:aws:iam::<account>:user/<backup user> is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::<bucket>/7days-backups/<archive>" because no identity-based policy allows the s3:GetObject action

The backup user’s policy lacks s3:GetObject. I removed it from the test user’s policy and ran the download as SYSTEM, and it failed this way while listing the bucket still worked. Add the permission, as the template in the backup article has it, and run step 3 again.

Backup on the old server fails with AWS Tools for PowerShell not available

Error

ERROR: AWS Tools for PowerShell not available to this account: The specified module 'AWS.Tools.SecurityToken' was not loaded because no valid module file was found in any module directory.. Install AWS.Tools.S3 and AWS.Tools.SecurityToken with -Scope AllUsers

The task’s result was 1 and nothing reached S3. The old server had AWS.Tools.S3 and not AWS.Tools.SecurityToken; it is what step 1 of the backup article installs. Run step 1 of the backup article, which installs both for all users, and start the task again.

What I didn’t test

  • A world people had played, or a client connecting to the new server, or the ports from outside the machine.
  • A reboot of the new server, so the at-boot trigger is registered and not seen to fire.
  • The old server’s backup task running after the move. Disable-ScheduledTask on both tasks was run, and both read Disabled.
  • Steps 2 and 3 of the backup article on either machine. The access key was written into SYSTEM’s profile on both for me, and I took the bucket name from the stack’s output.

Tested with

OS
Windows Server 2025 Datacenter (10.0.26100)
PowerShell
Windows PowerShell 5.1, elevated
Game
7 Days to Die dedicated server (294420), build 24994542
Move
old server to new server by restoring the S3 backup, with a new Navezgane world
Date
October 2026

New articles, by email.

A short note when something new is published.