Loosely Decoupled
GuidePowerShellAutomation

Restore a 7 Days to Die Dedicated Server Backup Without Merging the Old World

A copied restore such as xcopy /E keeps files the backup lacks. Rename the live Saves folder aside and move the archive’s in; the old world stays beside it.

On a workbench under a lamp, two miniature landscapes sit side by side, one clean and one with a stray toy tractor left in it. A small monkey with a cyan ID badge sits holding the tractor.
On this page

TL;DR

Stop the server, rename the live Saves folder, and move the archive’s Saves into its place. Do not copy the archive over the live folder. A copy that merges, such as xcopy /E /I /Y, replaces the files that are in both and leaves every file that exists only in the live folder. A restore meant to undo damage then keeps part of the damage. In testing, a stray file added to the live world survived an xcopy /E /I /Y restore, and was gone after the rename-and-move.

The function later in the article does the whole restore. The commands it is made of, run on the stopped server, stop at the first error. They also check that the archive has a Saves folder with files in it, and no GeneratedWorlds folder, before they touch the live one:

PowerShell
$dir = 'C:\7DaysToDieServer'
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$live = Join-Path $dir 'Saves'
$staging = Join-Path $dir "Saves.restoring-$stamp"
Expand-Archive -LiteralPath 'C:\backups\7dtd-backup-20260925-142839.zip' `
    -DestinationPath $staging -ErrorAction Stop
$saves = Join-Path $staging 'Saves'
$files = $null
if (Test-Path -LiteralPath $saves -PathType Container) {
    $files = Get-ChildItem -LiteralPath $saves -Recurse -File `
        -ErrorAction SilentlyContinue
}
if (-not $files) {
    throw 'The archive holds no files under Saves.'
}
if (Test-Path -LiteralPath (Join-Path $staging 'GeneratedWorlds')) {
    throw 'The archive holds GeneratedWorlds. Restore both folders by hand.'
}
Rename-Item -LiteralPath $live -NewName "Saves.before-restore-$stamp" -ErrorAction Stop
Move-Item -LiteralPath (Join-Path $staging 'Saves') -Destination $live -ErrorAction Stop
Remove-Item -LiteralPath $staging -Recurse

The old world stays beside the new one as Saves.before-restore-<stamp>. If the commands stop on an error, they leave a Saves.restoring-<stamp> folder to delete. The function also checks the archive before it stops anything, and checks the restored files against the archive before it starts the server again. If that check fails, it undoes the restore. The game-specific parts are the Saves folder and the archive’s layout.

The previous parts install, configure and update the server. This part is the one you hope not to need: getting the world back from a backup. The backup itself is the S3 backup article, and this part starts from one of its zip files. Its Restore a backup section has the Read-S3Object download.

1. See why xcopy /E merges instead of restoring

Each archive the S3 backup article makes is a zip of the Saves folder and serverconfig.xml. On Windows PowerShell 5.1, Compress-Archive writes the entry names with backslashes, such as Saves\W\G\main.ttw.

To see the problem, I made a small folder shaped like a world, with main.ttw and players.xml in it, and archived it with Compress-Archive. Then I damaged the folder: I changed players.xml and added a file, corrupt.dat, that the archive doesn’t have. Then I restored by copying:

Command
PS> Expand-Archive C:\demo\b.zip -DestinationPath C:\demo\x
PS> xcopy C:\demo\x\Saves C:\demo\srv\Saves /E /I /Y | Out-Null
PS> Get-ChildItem C:\demo\srv\Saves -Recurse -File | ForEach-Object Name

Output

corrupt.dat
main.ttw
players.xml

players.xml is back to the archive’s copy, because it exists in both places. corrupt.dat is still there, because the archive has no copy to replace it with. The restore undid some of the damage and kept the rest, and nothing says so. A new file in a world’s folder is a small example. What it stands for is anything the server wrote after the backup: a file that was damaged, one a newer game version added, one left by a crashed save.

2. Rename the live world folder aside and move the archive’s in

This function stops the server if it is running, swaps the folders, checks the result, and starts the server again if it was running:

PowerShell
function Restore-GameWorld {
    param(
        [Parameter(Mandatory)][string]$ArchivePath,
        [string]$ServerDir = 'C:\7DaysToDieServer',
        [scriptblock]$StopServer = { Stop-GameServer },
        [scriptblock]$StartServer = {
            Start-ScheduledTask -TaskName '7DaysToDieServer' -ErrorAction Stop
        }
    )
    Add-Type -AssemblyName System.IO.Compression.FileSystem
    $zip = [IO.Compression.ZipFile]::OpenRead($ArchivePath)
    try {
        $expected = @{}
        $hasTerrain = $false
        foreach ($entry in $zip.Entries) {
            $name = $entry.FullName.Replace('/', '\')
            if ($name -like 'GeneratedWorlds\*') { $hasTerrain = $true }
            if ($name -like 'Saves\*' -and $entry.Name) {
                $expected[$name] = $entry.Length
            }
        }
    }
    finally { $zip.Dispose() }
    if ($hasTerrain) {
        throw ('The archive holds GeneratedWorlds, which this function does not ' +
            'restore. Restore both folders by hand, with the server stopped.')
    }
    if ($expected.Count -eq 0) {
        $leaf = Split-Path $ArchivePath -Leaf
        throw "The archive holds no files under Saves: $leaf"
    }
    $exe = Join-Path $ServerDir '7DaysToDieServer.exe'
    $running = @(Get-Process 7DaysToDieServer -ErrorAction SilentlyContinue)
    $mine = @($running | Where-Object { $_.Path -eq $exe })
    $wasRunning = $mine.Count -gt 0
    if ($running.Count -gt $mine.Count) {
        throw ('A 7DaysToDieServer process is running from another folder. ' +
            'Stop it first.')
    }
    $stamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff'
    $asideName = "Saves.before-restore-$stamp"
    $live = Join-Path $ServerDir 'Saves'
    $aside = Join-Path $ServerDir $asideName
    $staging = Join-Path $ServerDir "Saves.restoring-$stamp"
    if ($wasRunning) { & $StopServer }
    try {
        Expand-Archive -LiteralPath $ArchivePath -DestinationPath $staging `
            -ErrorAction Stop
    }
    catch {
        Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue
        if ($wasRunning) { & $StartServer }
        throw ('Could not unpack the archive, so nothing was restored: ' +
            $_.Exception.Message)
    }
    $hadLive = Test-Path -LiteralPath $live
    if ($hadLive) {
        try {
            Rename-Item -LiteralPath $live -NewName $asideName -ErrorAction Stop
        }
        catch {
            Remove-Item -LiteralPath $staging -Recurse -Force `
                -ErrorAction SilentlyContinue
            if ($wasRunning) { & $StartServer }
            throw ('Could not move the live Saves aside, so nothing was restored: ' +
                $_.Exception.Message)
        }
    }
    $restored = Join-Path $staging 'Saves'
    $actual = @{}
    try {
        Move-Item -LiteralPath $restored -Destination $live -ErrorAction Stop
        $walk = {
            param($folder, $prefix)
            foreach ($item in Get-ChildItem -LiteralPath $folder -Force) {
                $name = "$prefix\$($item.Name)"
                if ($item.PSIsContainer) { & $walk $item.FullName $name }
                else { $actual[$name] = $item.Length }
            }
        }
        & $walk $live 'Saves'
        $missing = @($expected.Keys | Where-Object { $actual[$_] -ne $expected[$_] })
        $extra = @($actual.Keys | Where-Object { -not $expected.ContainsKey($_) })
        if ($missing + $extra) {
            $list = ($missing + $extra) -join ', '
            throw "The restored Saves doesn't match the archive: $list"
        }
    }
    catch {
        $message = $_.Exception.Message
        try {
            if (Test-Path -LiteralPath $live) {
                $failed = "Saves.failed-restore-$stamp"
                Rename-Item -LiteralPath $live -NewName $failed -ErrorAction Stop
            }
            if ($hadLive) {
                Rename-Item -LiteralPath $aside -NewName 'Saves' -ErrorAction Stop
            }
        }
        catch {
            throw ("The restore failed ($message) and could not be undone: " +
                "$($_.Exception.Message) The old world is at $aside, and the " +
                'server was not started.')
        }
        Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue
        if ($wasRunning) { & $StartServer }
        throw "The restore was undone. $message"
    }
    try { Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction Stop }
    catch { Write-Warning "Could not remove ${staging}: $($_.Exception.Message)" }
    if ($wasRunning) { & $StartServer }
    "Restored Saves from $(Split-Path $ArchivePath -Leaf)."
    if ($hadLive) { "The old Saves is at $aside." }
}

Run it as Restore-GameWorld -ArchivePath C:\backups\7dtd-backup-20260925-142839.zip. Stop-GameServer is the function from the part on stopping the server, so define it first. With a telnet password, read it once and pass it through -StopServer: $telnet = Read-Host 'Telnet password' -AsSecureString, then Restore-GameWorld -ArchivePath <zip> -StopServer { Stop-GameServer -Password $telnet }. The default start command is that part’s scheduled task, which throws if the task is missing or can’t start. The function expects Saves beside 7DaysToDieServer.exe, which is where UserDataFolder puts it in the earlier part on configuring the server.

What each part is for:

  • It reads the archive’s file list first and stops if there are no files under Saves, before the server is touched. Windows PowerShell 5.1 leaves an empty folder out of a zip, so a backup of an empty Saves holds no world files. So does an archive that another tool made from the configuration alone. The S3 backup script stops when Saves is missing, so it doesn’t write one. Restoring one of those would remove your world and put nothing back.
  • It stops with an error if any 7DaysToDieServer process is running from another folder. That holds even while the server in -ServerDir is running too, because it can only stop the one that lives in -ServerDir.
  • It unpacks into a folder beside Saves, on the same drive, so that moving it into place is a rename and not a copy. If the archive can’t be unpacked, as happened with an entry whose path was too long, it removes the partial copy and stops before anything is moved. It starts the server again if it had stopped it.
  • It treats removing its own unpacked folder, after a good restore, as housekeeping: if that fails, it warns and still starts the server, and you delete the Saves.restoring-<stamp> folder later.
  • It renames the live folder with a stamp that includes milliseconds, and never deletes it. Every restore leaves its own displaced folder, even two in the same second.
  • If the live folder can’t be renamed, it removes the unpacked copy as best it can and stops with an error. Nothing was restored, and nothing was lost. If the function had stopped the server, it starts it again before it reports the error, because the old world is untouched.
  • It compares the restored folder with the archive, by file name and size, before the server starts. It builds the names from the items in each folder, not from full paths, because the same folder can come back as RUSTAD~1 or RustAdmin and a prefix cut by length then goes wrong. The server rewrites files as soon as it starts, so this is the only moment the comparison means anything. If the comparison fails, the function renames the restored folder to Saves.failed-restore-<stamp>, puts the old folder back, and starts the server again if it had stopped it. Putting the old folder back can fail too, for example because another process has a file in the restored folder open. Then the function says where the old world is and does not start the server.

The comparison catches missing and extra files and files of the wrong size. It doesn’t catch a damaged archive. A zip with one byte flipped in the middle of a file passed it, and so did Expand-Archive. Keep more than one backup. On the real server, with it running and corrupt.dat in the live world, the output was:

Command
PS> Restore-GameWorld -ArchivePath $zip

Output

Stopped, exit code 0.
Restored Saves from 7dtd-backup-20260925-142839.zip.
The old Saves is at C:\7DaysToDieServer\Saves.before-restore-20261003-170512-418.

The server was stopped over telnet, the old folder was renamed, and the new one had exactly the archive’s files. corrupt.dat was in Saves.before-restore-* and not in Saves. The server started again and logged its ready line.

What the function doesn’t restore

The archive’s serverconfig.xml stays in the zip and isn’t copied anywhere, so your myserver.xml and its settings stay as they are. A random-gen world also keeps its terrain in GeneratedWorlds, beside Saves. The S3 backup article tells you to add that folder to the archive yourself. The script it publishes archives only Saves and serverconfig.xml, so a backup made with it can’t bring back a random-gen world’s terrain. Change the backup first. This function restores Saves only. If the archive holds GeneratedWorlds, the function stops with an error, because it would otherwise restart the server on a restored save with the old terrain. Restore both folders by hand, with the server stopped.

Restore with the server stopped

The function only stops and starts the server when one is running. On a stopped server it swaps the folders and leaves the server stopped. Start it yourself afterwards, and look at the log for StartGame done.

Common failures

Live folder can’t be moved aside

Error

Could not move the live Saves aside, so nothing was restored: Access to the path 'C:\7DaysToDieServer\Saves' is denied.

A file in the live folder is open. I reproduced it by holding one open from another process: the rename was denied, the function removed its unpacked copy, the live world was untouched, and no Saves.before-restore-* folder was created. Close whatever has the file open, which is usually the server, and run it again.

Archive has no world files

Error

The archive holds no files under Saves: 7dtd-backup-config-only.zip

The archive has no world files in it. With the server running, the function stopped before touching it: the process kept its ID and the live Saves was unchanged. Pick an older archive.

Old world folders fill the disk

The old and new worlds sit side by side until you delete the old one. A new Navezgane world was 17.2 MB, and a played one is larger. Delete Saves.before-restore-* folders by hand once you have checked the restored world.

What I didn’t test

I didn’t download an archive from S3. The archive in the tests came from the same Compress-Archive call the backup script uses. I didn’t restore a world someone had played in, so the damage in the tests is a changed file and an extra file. And I didn’t test a restore that fails in the middle of the final move, which I couldn’t cause.

Tested with

OS
Windows Server 2025 Datacenter (10.0.26100), virtual machine
PowerShell
Windows PowerShell 5.1, elevated
Game
7 Days to Die dedicated server (294420), V 3.2.0 (b10)
Date
October 2026

New articles, by email.

A short note when something new is published.