Environment context: These procedures were developed for SQL Server environments using log shipping as the primary disaster recovery strategy, shipping transaction logs from a primary data center to a geographically separate secondary site.
- Overview
- Prerequisites
- Schedule Recommendations
- Setup via SSMS
- Setup via dbatools
- Copy SQL Server Objects to the Secondary
- Monitoring Log Shipping
- Failover Procedures
Log shipping maintains a warm standby copy of one or more databases by continuously backing up transaction logs on the primary server, copying them to the secondary server, and restoring them. The process involves three SQL Agent jobs:
- Backup job (runs on the primary) — backs up the transaction log to a shared location.
- Copy job (runs on the secondary) — copies the log backup files from the shared location to a local folder on the secondary.
- Restore job (runs on the secondary) — restores the copied log backups to the secondary database.
graph LR;
PS(Primary Server) -->|1. Backup| BS(Backup Share)
BS -->|2. Copy| SS(Secondary Server)
SS -->|3. Restore| DB[(Standby Database)]
The secondary database remains in either NORECOVERY or STANDBY mode until a failover is needed. STANDBY mode allows read-only access to the secondary, which can be useful for reporting, but each restore cycle will disconnect active readers.
- The database must be in Full or Bulk-Logged recovery model. Simple recovery model does not generate transaction log backups.
- A network share accessible by both the primary and secondary SQL Server service accounts for log backup file transfer.
- SQL Server Agent must be running on both servers.
- The service accounts on both servers need read/write access to the backup share.
Verify the recovery model of your databases:
SELECT name, recovery_model_desc
FROM sys.databases
WHERE database_id > 4;Get-DbaDbRecoveryModel -SqlInstance PrimaryServer01 -ExcludeSystemDbIf any databases need to be changed to Full recovery:
ALTER DATABASE [DatabaseName] SET RECOVERY FULL;Set-DbaDbRecoveryModel -SqlInstance PrimaryServer01 -Database DatabaseName -RecoveryModel FullThe schedule for each job should align with your Recovery Point Objective (RPO) and Recovery Time Objective (RTO).
| Job | Recommended Frequency | Notes |
|---|---|---|
| Backup | Every 5 minutes | Keeps potential data loss window small. |
| Copy | Every 5 minutes | Should match or closely follow the backup schedule. |
| Restore | Every 2–4 hours | Can be more frequent if a tighter RTO is needed. Frequent restores on STANDBY databases will interrupt read-only users. |
These are starting points. Adjust based on transaction volume, network bandwidth between sites, and your SLA requirements.
The SSMS wizard is the most straightforward approach. Rather than reproducing the wizard steps here, this guide covers the process well:
How to create SQL Server Log Shipping using SSMS
While log shipping can be scripted entirely in T-SQL, it's a multi-step process requiring commands on both the primary and secondary servers. If a single step is missed, initialization will fail. The SSMS wizard or the dbatools method below are more reliable approaches.
The Invoke-DbaDbLogShipping command configures the same backup, copy, and restore jobs that the SSMS wizard creates — without needing to open SSMS or click through the wizard.
The following example loops through all user databases on the primary server and configures log shipping to the secondary:
$cred = Get-Credential "$env:USERDOMAIN\$env:USERNAME"
# Exclude utility and infrastructure databases that don't need log shipping
$databases = Get-DbaDatabase -SqlInstance PrimaryServer01 -ExcludeSystem `
-ExcludeDatabase ReportServer, ReportServerTempDB, DBAOps, SSISDB |
Select-Object -ExpandProperty Name |
Sort-Object
foreach ($db in $databases) {
$splatLogShip = @{
SourceSqlInstance = 'PrimaryServer01'
SourceSqlCredential = $cred
DestinationSqlInstance = 'SecondaryServer01'
DestinationSqlCredential = $cred
Database = $db
SharedPath = '\\PrimaryServer01\Backups\LogShipping\Source'
LocalPath = 'X:\Backups\LogShipping\Source'
BackupScheduleFrequencyType = 'Daily'
BackupScheduleFrequencyInterval = 1
BackupScheduleFrequencySubdayType = 'Minutes'
BackupScheduleFrequencySubdayInterval = 5
CompressBackup = $true
GenerateFullBackup = $true
CopyDestinationFolder = '\\SecondaryServer01\Backups\LogShipping\Destination'
CopyScheduleFrequencyType = 'Daily'
CopyScheduleFrequencyInterval = 1
CopyScheduleFrequencySubdayType = 'Minutes'
CopyScheduleFrequencySubdayInterval = 5
RestoreAlertThreshold = 300
RestoreScheduleFrequencyType = 'Daily'
RestoreScheduleFrequencyInterval = 1
RestoreScheduleFrequencySubdayType = 'Hours'
RestoreScheduleFrequencySubdayInterval = 4
}
Invoke-DbaDbLogShipping @splatLogShip
}Key parameters to pay attention to:
SharedPath— the UNC path where the primary writes log backups. Both servers must have access.LocalPath— the local path on the primary that maps to theSharedPathUNC.GenerateFullBackup— when$true, takes a fresh full backup to initialize the secondary. Set to$falseif you want to initialize from an existing backup.RestoreAlertThreshold— minutes before an alert fires if restores fall behind (300 = 5 hours).
Log shipping only replicates database contents. It does not transfer server-level objects such as logins, Agent jobs, linked servers, or other configurations. If you need to bring the secondary online for DR, these objects must already be in place.
The Start-DbaMigration command copies all server-level objects from one instance to another. The -Exclude Databases flag skips the databases themselves (since log shipping handles those).
$splatMigrate = @{
Source = 'PrimaryServer01'
Destination = 'SecondaryServer01'
Exclude = 'Databases'
}
Start-DbaMigration @splatMigrate -VerboseThis copies:
- Logins (including SID mapping to avoid orphaned users)
- SQL Agent jobs, operators, and alerts
- Linked servers
- Database mail configuration
- Credentials
- Server configuration settings (sp_configure)
- Audits, endpoints, and extended events
- Custom error messages
- Startup procedures
- All other server-level objects
Run this periodically — any logins or Agent jobs created on the primary after the initial copy won't exist on the secondary until you run it again. Consider scheduling this as a weekly or monthly maintenance task:
# Sync just logins (lightweight, good for frequent runs)
Copy-DbaLogin -Source PrimaryServer01 -Destination SecondaryServer01
# Sync Agent jobs
Copy-DbaAgentJob -Source PrimaryServer01 -Destination SecondaryServer01Log shipping status should be monitored to catch backup, copy, or restore failures before they become a DR gap.
Check the current state of log shipping on the secondary:
SELECT
secondary_database,
last_copied_file,
last_copied_date,
last_restored_file,
last_restored_date,
last_restored_latency
FROM msdb.dbo.log_shipping_monitor_secondary;Get-DbaDbLogShipError -SqlInstance SecondaryServer01
# Check overall log shipping status
$splatQuery = @{
SqlInstance = 'SecondaryServer01'
Database = 'msdb'
Query = @"
SELECT
secondary_database,
last_copied_file,
last_copied_date,
last_restored_file,
last_restored_date,
last_restored_latency
FROM dbo.log_shipping_monitor_secondary;
"@
}
Invoke-DbaQuery @splatQueryKey things to watch for: last_restored_latency climbing beyond your alert threshold, and gaps between last_copied_date and last_restored_date that exceed your restore schedule interval.
For detailed failover, failback, and traffic redirection procedures, see [[Log-Shipping-Failover|Log Shipping Failover and Failback Procedures]].
- [[Disaster-Recovery|Disaster Recovery Index]]