Deploy Script Ran but Site Not Updated: Fix Silent Failures
A deploy script or CI pipeline can exit 0 while the web server keeps serving old files. Learn how to trace what is actually running, fix silent failure points, and add post-deploy verification on Linux and Windows.
Rao Aadil, India 9 min read
Confirm the symptom: deploy says success, site unchanged
A deploy script ran but site not updated is a silent failure: the script or CI pipeline prints success, returns exit code 0, but the browser still shows the old HTML, assets, or behavior. Confirm exactly what the script reported and what the live site is serving.
Capture the exit code from the deploy command first. In Bash, run the script and then immediately echo the special variable $?:
./deploy.sh
echo $?
A 0 means the script exited normally, but it does not mean the site was updated. In PowerShell, after running a native command or a script that calls exit, check $LASTEXITCODE:
.\deploy.ps1
echo $LASTEXITCODE
When $LASTEXITCODE is 0, the script reported success. Fetch the live site and look for a marker that only exists in the old version. On Linux, use curl and grep:
curl -s https://example.com | grep 'old-version'
On Windows, use Invoke-WebRequest and Select-String:
Invoke-WebRequest -Uri https://example.com -UseBasicParsing |
Select-Object -ExpandProperty Content |
Select-String 'old-version'
Finding the old marker means the site is serving stale content and the deploy did not take effect.
“It ran, something broke, and the terminal that had the output is closed.”
Output is streamed and kept, so the failure is still there to read afterwards. It reads the actual error rather than reporting that the script exited non-zero, and can fix the script with you.
It reads the real state of the server before it says anything, shows you the exact command, and waits for your approval. Windows and Linux, over the SSH access you already have.
Where to look first: what the server is actually serving
The web server process may still hold old files in memory, or it may have started before the new files were copied. A process that is still running from an earlier time can serve outdated content even after new files are on disk. Check the process start time to see whether the running process predates the deploy.
Run ps on Linux to get the process start time, or systemctl status if it runs as a service:
ps -p <pid> -o lstart
systemctl status <service>
Replace <pid> with the process ID of your app server or nginx worker, and <service> with the unit name such as nginx or myapp. The systemctl status output shows an "Active:" line with the time the service was started.
Windows has Get-Process for the same check:
Get-Process <name> | Select-Object StartTime
For IIS, the worker process is often named w3wp; for nginx on Windows, use nginx. If the start time is older than your last deploy, the process has not been restarted or recycled.
Containerized apps need a different check: compare the running container's image to the image you just built. List running containers with docker ps:
docker ps --format '{{.ID}} {{.Image}}'
On Windows PowerShell, quote the Go template with double quotes:
docker ps --format "{{.ID}} {{.Image}}"
Then inspect the running container's image ID:
docker inspect <container_id> --format '{{.Image}}'
And list local images with digests to see what you actually built:
docker images --digests | grep <image_name>
If the running container's image ID does not match the newest image digest, the container was not recreated from the new build.
Trace the deploy path: where did the new files actually go?
The deploy script may have copied files to the wrong location, or the web root is not what you think it is. Check the target directory's timestamp and contents on the server.
Look at the web root with a long listing on Linux:
ls -la /var/www/myapp
File modification times should match your deploy time. On Windows, list the directory:
dir C:\inetpub\wwwroot\myapp
or, for nginx on Windows, dir C:\nginx\html\myapp. If the timestamps are old, the new files never arrived.
Also compare the artifact hash from your CI system to the hash on the server. On Linux, use sha256sum:
sha256sum /path/to/file
Windows uses Get-FileHash:
Get-FileHash C:\path\to\file -Algorithm SHA256
Run this on both the build artifact and the file in the web root; if the hashes differ, the copy step failed or the wrong file was transferred.
Common path mistakes include the script writing to a different folder than the web root, an environment variable expanding to an empty path (so files land in the current working directory), or the build artifact never being copied because an earlier step silently failed. Add echo or Write-Host statements to your deploy script to print the destination path and verify it is correct.
Find the cause: exit code 0 but one step silently failed
A deploy script can exit 0 while a step that should have updated the site did nothing useful. Several commands pass but leave the site unchanged:
nginx -ttests the configuration and returns 0, but if you forget to reload, the running nginx keeps the old config and old file handles. You must callnginx -s reloadorsystemctl reload nginxafter copying files.git pullon a detached HEAD can return 0 but not update the working tree because HEAD is not on a branch. Checkgit statusbefore relying on the pull.scpto a stale hostname or IP can copy files to the wrong server and still succeed. Verify the target host in the script matches the production server.docker compose up -ddoes not rebuild if the image tag is unchanged and you do not pass--buildor--force-recreate. The container keeps running with the old image even though a new image may exist locally.
To confirm nginx reloaded correctly on Linux:
sudo systemctl reload nginx
systemctl status nginx --no-pager
Look for an "active (running)" state and no error messages. On Windows, run nginx -s reload and then inspect the error log, typically at C:\nginx\logs\error.log (the path may differ on your installation). A failed reload will write an error entry there.
For IIS, the application pool may not have been recycled after files were copied to wwwroot, so the worker process still holds old code or content. Recycle the app pool explicitly:
appcmd recycle apppool /apppool.name:DefaultAppPool
Or in PowerShell:
Restart-WebAppPool -Name DefaultAppPool
Note that recycling can drop in-flight requests, but it is a standard way to force IIS to pick up new files.
Remember that browser cache or a CDN cache can make the site look unchanged even when the server has been updated. A hard refresh (Ctrl+F5) or adding a cache-busting query string like ?v=123 can reveal whether the client is holding old content. If a CDN is involved, purge its cache according to your provider's instructions.
Fix the deploy script so it actually updates the site
Stop relying on a plain file copy into the live web root while the server is running. Use an atomic swap on Linux to avoid serving a mix of old and new files.
Create a timestamped release directory, copy the new files there, and update a symlink that your web server uses as its document root:
TIMESTAMP=$(date +%Y%m%d%H%M%S)
mkdir -p /var/www/releases/$TIMESTAMP
cp -a /var/www/staging/. /var/www/releases/$TIMESTAMP/
ln -sfn /var/www/releases/$TIMESTAMP /var/www/current
Point nginx or your app server at /var/www/current. The symlink switch is atomic, so the server either sees the old release or the new one, never a partial copy.
On Windows with IIS, stop the site or recycle the application pool around the file copy. For example:
Import-Module WebAdministration
Stop-WebSite -Name "MySite"
Copy-Item -Path C:\staging\* -Destination C:\inetpub\wwwroot\myapp -Recurse -Force
Start-WebSite -Name "MySite"
If you prefer not to stop the site, recycle the app pool after copying:
Restart-WebAppPool -Name DefaultAppPool
For nginx on Windows, use a similar release directory pattern. Stop the service, swap the current folder with the new release using Move-Item, then start the service:
$timestamp = Get-Date -Format "yyyyMMddHHmmss"
New-Item -ItemType Directory -Path C:\nginx\html\releases\$timestamp
Copy-Item -Path C:\staging\* -Destination C:\nginx\html\releases\$timestamp -Recurse -Force
Stop-Service nginx
if (Test-Path C:\nginx\html\current) {
Move-Item C:\nginx\html\current C:\nginx\html\previous
}
Move-Item C:\nginx\html\releases\$timestamp C:\nginx\html\current
Start-Service nginx
This causes a brief downtime while the service is stopped, but it prevents serving partially copied files.
Add explicit reload or restart steps after copying. On Linux, use sudo systemctl reload nginx for nginx or sudo systemctl restart myapp for an app server. On Windows, use Restart-Service nginx for nginx. Avoid iisreset in production if possible: it stops all IIS services and drops all sessions across every site on the server. Recycle the specific application pool or stop and start the specific site instead.
For Docker deployments, use a unique image tag for each build, such as myapp:$CI_COMMIT_SHA or myapp:$(date +%s), and recreate containers with:
docker compose up -d --build --force-recreate
This forces Compose to rebuild the image and recreate the containers, rather than reusing a container that was started from an old image.
Prevent silent deploy failures with post-deploy verification
The most reliable way to catch a deploy that exits 0 but does not update the site is to add a version endpoint to your application. This endpoint returns the deployed commit SHA, build number, or another unique version string, for example {"version":"abc123"}.
After the deploy, have the script call that endpoint and compare the response to the expected version. On Linux:
EXPECTED="abc123"
ACTUAL=$(curl -s https://example.com/version | jq -r .version)
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "Version mismatch: expected $EXPECTED, got $ACTUAL"
exit 1
fi
On Windows PowerShell:
$expected = "abc123"
$actual = (Invoke-WebRequest -Uri https://example.com/version -UseBasicParsing).Content |
ConvertFrom-Json |
Select-Object -ExpandProperty version
if ($actual -ne $expected) {
Write-Error "Version mismatch: expected $expected, got $actual"
exit 1
}
If the version does not match or the endpoint does not respond, the script exits with a non-zero code, turning a silent failure into a visible CI failure.
Some deployment tools can run a verification command you define after the deploy, stream the output, and include the result in an audit log. That gives you a record of what was checked and what was actually served, without having to write the check manually into every pipeline.