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:
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:
- It is free. You already own the computer. A small cloud server costs roughly $10–20 a month, every month, forever.
- You learn how servers actually work. Clicking "Deploy" on a hosting platform teaches you very little. Doing this teaches you a lot.
- Nothing is hidden. When something breaks, you can see every layer and fix it yourself.
And the honest downsides, because they matter:
- If your PC is off, your app is down. No power, no internet, no app.
- You are the one on call. Nobody else is watching it at 3am.
- Home internet is slower to reach than a data centre.
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:
FROM— start from a box that already has Node.js 20 installed.WORKDIR— work in a folder called/appinside the box.COPY package*.json— copy just the dependency list in first.RUN npm ci— install the dependencies.COPY . .— copy the rest of your code in.EXPOSE— note that the app listens on port 3000.CMD— the command that starts your app.
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:
-d— run in the background so you get your terminal back.--name— give the container a name you can refer to later.--restart unless-stopped— start automatically if it crashes, or if your PC reboots. This is what makes it a server rather than a script.--memory 512m— cap how much memory this app can use.-p 3000:3000— connect port 3000 outside the box to port 3000 inside it.
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:
To open that path you need two things:
- 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".
- 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.orgthat 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:
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
- Add a second app and give it a different port and its own memory cap.
- Learn Docker Compose, which describes several containers in one file.
- Put Nginx in front so you can serve several apps from one port, with HTTPS.
- Add a database container, with a volume so the data survives restarts.