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.

Doing this with brynko devOps Agent

“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.

Download for WindowsSee what else it does

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 -t tests the configuration and returns 0, but if you forget to reload, the running nginx keeps the old config and old file handles. You must call nginx -s reload or systemctl reload nginx after copying files.
  • git pull on a detached HEAD can return 0 but not update the working tree because HEAD is not on a branch. Check git status before relying on the pull.
  • scp to 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 -d does not rebuild if the image tag is unchanged and you do not pass --build or --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.

Related reading

Fix Deploy Scripts That Exit 0 but Don't Update

Use a tool that runs your deploy script, streams the output, and runs a verification command after the deploy so a silent failure becomes a visible one in the audit log.

An AI teammate that builds, runs, fixes, deploys and operates your software, on your machine and on your servers. It reads the real state of your systems, shows you every command before it runs, and never touches production without your approval.

Works with Docker Docker Compose nginx IIS Tomcat MySQL / MariaDB PostgreSQL Redis Linux servers Windows servers AWS Jenkins Let's Encrypt Node.js Python Java .NET Go PHP
  • Every change asks first, and shows you the exact command or diff before it runs.
  • File edits are snapshotted and reversible. Deleted folders are archived, not removed.
  • Private key material never reaches the model, under any setting.
brynko devOps Agent
Deployments

this project has no deploy yet, get it onto staging

read the project checked the server drafted the deploy script

It is a Node app with a Dockerfile but no deploy script. I have drafted one that builds the image, replaces the container and reloads nginx; approve it on the card and it is saved to your scripts under Staging. Then shall I run it?

go ahead

streamed the run container replaced site responded

Finished in 1 minute 52. The new container is running, nginx reloaded cleanly, and staging is returning 200. Next time it is one click in Scripts, or just ask.

Deployed · staging healthy

Free download for Windows. 300 free credits on signup, no card, no subscription.

Questions

Common questions

Why does my deploy script return success but the website still shows the old version?
The script can exit with code 0 even if a necessary step did not actually update the running process. For example, nginx was not reloaded, the app pool not recycled, or files were copied to the wrong directory. Exit codes only reflect whether commands returned success, not whether the site now serves new content.
How do I check which version of the app is actually running on the server?
Check process start times, running container image IDs, and file hashes in the web root. You can also curl or invoke a version endpoint if your app has one, and compare the returned version to the expected build.
Can nginx or IIS cache the old site after a deploy?
Nginx does not cache static files by default unless explicitly configured, but it may keep old file handles or serve from memory until reloaded. IIS application pools can hold old assemblies until recycled. Browser and CDN caches also commonly serve stale content even after the server is updated.
How do I make my deploy script fail if the site is not updated?
Add a post-deploy verification step that calls a version endpoint and compares the returned commit SHA or build version to the expected one. Exit with a non-zero code if the version does not match or the endpoint does not respond, which will mark the CI job as failed.
What is the correct way to swap new files into place without downtime on Linux and Windows?
On Linux, use a timestamped release directory and update a symlink atomically with ln -sfn. On Windows with IIS, stop the site or recycle the app pool before copying, then start it again; for nginx on Windows, stop the service, swap directories with Move-Item, and start the service. These methods avoid serving partially copied files.

Keep reading