Skip to content

Deploy a Website from GitHub to Windows Server

decerto edited this page Aug 11, 2026 · 1 revision

Deploy a website from GitHub to Windows Server

Getting code from a repository onto a Windows server, repeatedly, without downtime and without breaking the live site when a build fails. Three approaches, then the details that actually bite on Windows.


The three approaches

1. Pull on the server

The server clones the repository and pulls when you tell it to. Simple, no secrets leave the machine, works with private repositories through a deploy key — an SSH key authorised for one repository, read-only. This is what WinPanel does: it generates the key for you and shows you where to paste it.

2. Push from CI

GitHub Actions builds, then copies the result over WinRM, SSH or a file share. Good if you already have a pipeline. It means giving CI credentials to your server, and the build happens on a machine that is not the one it will run on.

3. Webhook

The repository calls a URL on your server when you push, and the server deploys. Fast, but now you have an internet-facing endpoint that runs code, so it needs a shared secret and signature verification.


What a deploy has to do

More than git pull, if it is not to be a nervous moment:

  1. Fetch the new code — into a folder that is not the one being served.
  2. Install dependencies and build — there, not in the live folder.
  3. Swap it in — one rename, so the site is never half-updated.
  4. Restart the service and wait for it to answer.
  5. Roll back if it does not. This is the step everybody skips, and it is the one that matters at 6pm on a Friday.

The reason for the separate folder is Windows-specific and non-negotiable: a running process holds its files open, so writing over a live app produces EPERM and EBUSY errors rather than an update.

C:\Sites\my-app\
  release\      <- live, being served right now
  .staging\     <- the new version, being built
  .previous\    <- the last version known to have worked

Build in .staging, rename release to .previous, rename .staging to release, restart. If it will not start, rename back. The site is down for the length of two renames — milliseconds — instead of the length of an npm install.


The Windows gotchas

Never rename a folder containing a pnpm node_modules. On Windows pnpm links packages with junctions that store absolute paths, so moving the folder leaves every link pointing at somewhere that no longer exists, and the app cannot find its own dependencies. Either install with --config.node-linker=hoisted, or install after the rename rather than before.

Long paths. node_modules will exceed 260 characters. Set LongPathsEnabled to 1 under HKLM\SYSTEM\CurrentControlSet\Control\FileSystem and reboot, or installs fail with ENAMETOOLONG on packages picked seemingly at random.

NODE_ENV=production breaks the install step. npm and yarn read it and skip devDependencies — which is where your bundler and framework live. Set it for the build and the run, but not for the install.

spawn EINVAL. Node 20.12 and later refuse to spawn .cmd and .bat files without a shell, so scripts that call npm.cmd fail as of a Node upgrade. Invoke the CLI's .js entry point with node, or pass shell: true.

Defender. Excluding your sites folder from real-time scanning takes minutes off every large install. It is the single biggest speed-up available on a Windows build server.

The service account. It has a different PATH, no user profile, and no npm cache in your home directory. Anything that works interactively and fails in CI is usually this.


Doing it with WinPanel

Point a site at a repository and it does the whole list. It works out how to build your project by inspecting it — package manager, build steps, entry point, whether the frontend builds into the backend — and shows you the plan as a winpanel.json you can confirm and then commit, so later deploys need no setup:

{
  "runtime": "node",
  "packageManager": "pnpm",
  "steps": [
    { "name": "Install packages", "command": "pnpm", "args": ["install"] },
    { "name": "Build",            "command": "pnpm", "args": ["run", "build"] }
  ],
  "app": { "portEnvVar": "PORT", "healthCheckPath": "/" }
}

Every deploy streams its log live, keeps the previous version, restarts the service, waits for a health check, and rolls back if the new build will not start. Commands are limited to a known set of tools — npm, pnpm, yarn, bun, node, npx, dotnet — because that file comes from your repository and is treated as untrusted input.


Related

Clone this wiki locally