← All posts
Beginner12 min read

Save server costs!

Deploy your backend on your own Windows PC with WSL2 and Docker

You built a backend. It works on your laptop. Now you want other people to use it.

The usual advice is to pay for a cloud server. But you probably already own a computer that can do the job. This guide shows you how to turn a Windows PC into a real server using two free tools: WSL2 and Docker.

By the end you will have your backend running on your own machine, restarting itself when it crashes, and updating with a single command.

Who this is for

You should be comfortable running commands in a terminal and using git. You do not need to know Linux, Docker, or anything about servers. Every term gets explained the first time it shows up.

The two tools, in plain English

WSL2 stands for Windows Subsystem for Linux, version 2. It runs a real Linux system inside your Windows PC. Not a simulation — an actual Linux, in a window, sharing your files. Almost all servers in the world run Linux, so this lets you practice on the real thing.

Docker puts each app inside its own sealed box, called a container. The box holds the app and everything it needs to run: the right language version, the right libraries, the right settings. Move the box to another computer and it behaves exactly the same.

Here is how the three pieces stack up:

How WSL2 and Docker sit inside your Windows PCAn outer box labelled Your Windows PC contains a box labelled WSL2, which contains a box labelled Docker. Inside Docker are four small boxes: API server, Background worker, PostgreSQL, and Web app.Your Windows PCThe physical computer sitting on your deskWSL2— a real Linux system running inside WindowsDocker— keeps each app in its own sealed boxAPIserverport 3000Backgroundworkerno portPostgreSQLport 5432Webappport 8080
Three layers, each one living inside the one above it. Your PC runs WSL2, WSL2 runs Docker, Docker runs your apps.

Read it from the outside in. Your PC runs WSL2. WSL2 runs Docker. Docker runs your apps. Each layer sits inside the one above it.

Why bother instead of just paying for cloud hosting?

Three honest reasons:

And the honest downsides, because they matter:

This is a great fit for a side project, a demo, an internal tool, or a portfolio piece. It is a bad fit for anything people are paying you for.

Step 1 — Install WSL2

Open PowerShell as Administrator and run:

wsl --install

That one command installs WSL2 and Ubuntu, a popular flavour of Linux. Restart your PC when it asks.

After the restart, Ubuntu opens and asks you to pick a username and password. Choose anything you like, but write the password down — you will need it for the sudo command later.

Check it worked:

wsl --version

If you see version numbers, you are done. Everything from here happens inside Ubuntu, not Windows.

Step 2 — Install Docker

Inside your Ubuntu terminal, run these three commands one at a time:

sudo apt update

Refreshes the list of installable software.

sudo apt install -y docker.io docker-compose-v2

Installs Docker itself.

sudo usermod -aG docker $USER

Lets you run Docker without typing sudo every time.

Now close the Ubuntu window and open it again. That last command only takes effect in a fresh session — this trips up almost everyone the first time.

Check it worked:

docker run hello-world

If you see a friendly welcome message, Docker is working.

Step 3 — Get your code onto the machine

Clone your project inside Ubuntu:

git clone https://github.com/your-name/your-backend.git
cd your-backend

Important: keep your code inside the Linux file system (/home/you/...), not on the Windows side (/mnt/c/...). Files on the Windows side are much slower for Docker to read, and this is the single most common reason a WSL2 setup feels sluggish.

Step 4 — Describe your app in a Dockerfile

A Dockerfile is a recipe. It tells Docker how to build the box your app runs in. Create a file called Dockerfile in your project root:

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Line by line:

Why copy package.json before the rest of the code? Docker caches each step. Your dependencies change rarely, your code changes constantly. This order means rebuilds skip the slow install step and finish in seconds instead of minutes.

Step 5 — Run it

Build the box:

docker build -t my-backend .

Then start it:

docker run -d \
  --name my-backend \
  --restart unless-stopped \
  --memory 512m \
  -p 3000:3000 \
  my-backend

Those flags matter, so here is what each one does:

Check it is alive:

curl http://localhost:3000

Why the memory cap matters

If one app uses all the memory on the machine, Linux starts killing programs to survive. This is called an OOM — out of memory. Without limits, a single runaway app can take down everything else on the server.

--memory 512m contains the damage. The greedy app gets killed, everything else keeps running, and --restart unless-stopped brings it back. One broken app stays one broken app.

Step 6 — Let other people reach it

Right now the app only answers on your own machine. Here is the full path a real visitor's request has to travel:

The path a request takes to your backendFive steps connected left to right by arrows: someone's browser, your home router, Windows on port 3000, WSL2 Linux, and finally your API container.Someone'sbrowserYour homerouterWindows(port 3000)WSL2LinuxYour APIcontainer
A request travels through every layer on the way in, and back out the same way with the answer.

To open that path you need two things:

  1. A port forward on your router. In your router's admin page, forward external port 3000 to your PC's local IP address. Every router words this differently — search for your model plus "port forwarding".
  2. A way to find your home, since most home internet connections change their address periodically. A free dynamic DNS service such as DuckDNS gives you a fixed name like myproject.duckdns.org that follows the changes.

Before you open a port, read this. Opening a port makes your machine reachable by anyone on the internet — including automated scanners, which will find it within hours. Never expose a database port. Put a password on everything. If you are unsure, use a tunnel service like Cloudflare Tunnel instead, which gives you a public URL without opening anything.

Step 7 — Ship changes with one command

You do not want to type five commands every time you fix a bug. This is the loop you actually want:

The deploy loop, start to finishFive numbered steps in a row: change code, git push, run the deploy script, Docker builds a new container, then the old container is swapped for the new one. An arrow loops back to the start.1You changesome code2git push3Run thedeploy script4Docker buildsa new box5Old box out,new box inrepeat for every change
The update loop. Once the script exists, shipping a change is one command.

Save this as deploy.sh in your project:

#!/usr/bin/env bash
set -euo pipefail

echo "→ Fetching latest code"
git pull

echo "→ Building new image"
docker build -t my-backend .

echo "→ Swapping containers"
docker stop my-backend || true
docker rm my-backend || true
docker run -d \
  --name my-backend \
  --restart unless-stopped \
  --memory 512m \
  -p 3000:3000 \
  my-backend

echo "→ Checking health"
sleep 3
curl -fsS http://localhost:3000 > /dev/null && echo "✓ Deployed" || echo "✗ Not responding"

Make it runnable once:

chmod +x deploy.sh

From now on, shipping a change is:

./deploy.sh

The set -euo pipefail line at the top is worth understanding: it tells the script to stop immediately if any step fails. Without it, a failed build would happily carry on and delete your working container anyway, leaving you with nothing running.

When something breaks

Four commands cover almost every problem you will hit:

docker ps

What is running right now.

docker logs my-backend --tail 50

The last 50 lines your app printed. Start here — the error is usually sitting in plain sight.

docker stats

Live memory and CPU use per container. Use this when things feel slow.

docker exec -it my-backend sh

Open a shell inside the box, to look around as your app sees things.

What you built

You now have a real server. Your backend runs in a container, restarts itself when it crashes, survives a reboot, cannot eat all the memory, and updates with one command.

More importantly, you understand every layer of it. That is the part that shows up in interviews — not that you deployed something, but that you can explain what happens between a browser and your code.

Sensible next steps