Home Lab Docker Server and Deploy Workflow
So you want a personal Docker host. Somewhere to run side projects, self-hosted services, or AI experiments without paying for cloud infrastructure. You have two realistic options: an old PC or laptop running Linux, or WSL2 on your Windows machine. This post covers both — and more importantly, how to deploy to either of them using the exact same workflow.
Companion post: If you run into boot problems during Linux installation on older hardware, see The Legacy BIOS + GPT Trap.
Choosing Your Host
Option A: Dedicated Linux Machine
Any machine from the last 10–15 years with at least 4 GB RAM and an SSD is workable. The more RAM, the more services you can run simultaneously.
Good candidates: an old laptop, a retired office desktop, or a mini PC (Intel NUC, Beelink, etc.).
The advantage is a dedicated, always-on host that doesn't compete with your development machine for resources. The disadvantage is setup time — particularly on machines old enough to have Legacy BIOS quirks.
Recommended OS: Debian 13 — stable, minimal, excellent Docker support, and the installer handles older hardware well.
Option B: WSL2 on Windows
If you don't have a spare machine, WSL2 is a legitimate alternative. Docker Engine runs natively inside WSL2, SSH access works, and the deploy workflow described in this post is identical — same commands, same tools, same result.
The trade-offs: your Docker host competes with Windows for RAM, and it goes down whenever your development machine goes down. For personal projects and learning, this is usually acceptable.
Setting Up the Linux Host (Debian 13)
Install the OS: Download the Debian 13 netinstall ISO from debian.org, create a bootable USB with Rufus, and install with the text installer. At partitioning choose guided — entire disk, all files in one partition. At software selection: SSH server + Standard system utilities only — no desktop environment.
First boot — install sudo:
su -
apt install sudo -y
usermod -aG sudo <your-username>
exit
Log out and back in. sudo now works.
Set a static IP. Find your interface name with ip a, then edit /etc/network/interfaces:
auto enp0s25
iface enp0s25 inet static
address 192.168.x.x
netmask 255.255.255.0
gateway 192.168.x.1
dns-nameservers 8.8.8.8 1.1.1.1
Apply with sudo systemctl restart networking. Replace enp0s25 with your actual interface name. Your gateway IP is visible on Windows via ipconfig.
Firewall:
sudo apt install ufw -y
sudo ufw allow OpenSSH
sudo ufw allow 9443/tcp # Portainer
sudo ufw allow 80/tcp # Nginx Proxy Manager
sudo ufw allow 81/tcp # NPM admin
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
Timezone: sudo timedatectl set-timezone Europe/Budapest
Setting Up WSL2
Enable WSL2 from an Administrator PowerShell prompt:
wsl --install
Restart when prompted. Ubuntu installs by default — that's fine.
Static IP (optional): WSL2 gets a dynamic IP by default. For a stable address, add this to %USERPROFILE%\.wslconfig:
[wsl2]
networkingMode=mirrored
This makes WSL2 share your Windows IP.
Enable SSH access inside WSL2:
sudo apt update && sudo apt install openssh-server -y
sudo service ssh start
To start SSH automatically, add sudo service ssh start to your ~/.bashrc.
Installing Docker
The official Docker installation method works identically on Debian and Ubuntu/WSL2.
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
For Ubuntu/WSL2, replace debian with ubuntu in the repository URL and gpg path.
Post-install — run Docker without sudo:
sudo usermod -aG docker $USER
Log out and back in, then verify: docker run hello-world
On a dedicated Linux machine, enable Docker to start automatically:
sudo systemctl enable docker
sudo systemctl enable containerd
Portainer and Nginx Proxy Manager
Portainer CE
docker volume create portainer_data
docker run -d \
-p 8000:8000 \
-p 9443:9443 \
--name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
Access at https://<host-ip>:9443. Accept the self-signed certificate warning — it's expected. Create an admin user and select Get Started → local.
Nginx Proxy Manager
Create ~/stacks/npm/docker-compose.yml:
services:
app:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
Then: docker compose up -d
Access the admin UI at http://<host-ip>:81. Default credentials: admin@example.com / changeme — change them on first login.
The Deploy Workflow
This is the core of the post. The workflow below works whether your host is a dedicated Linux machine or WSL2 — the commands are identical.
Developer machine Host (Linux or WSL2)
──────────────────────────── ──────────────────────────────
1. docker build
2. docker save | gzip → scp →
3. docker load
4. Portainer stack deploy
5. Nginx Proxy Manager (optional)
Step 1 — Build the image:
docker build -t <project>:1.0.0 -t <project>:latest .
Step 2 — Export:
docker save <project>:latest | gzip > <project>.tar.gz
Step 3 — Upload (one-time directory setup):
ssh user@<host-ip> "mkdir -p ~/images"
scp <project>.tar.gz user@<host-ip>:~/images/
Step 4 — Load on the host:
ssh user@<host-ip> "docker load < ~/images/<project>.tar.gz"
Step 5 — Deploy via Portainer: On first deploy, go to Portainer → Stacks → Add stack, paste your docker-compose.yml, set environment variables, and click Deploy. For updates: select the stack and click Update the stack. Portainer picks up the newly loaded latest image automatically.
Optional — expose via Nginx Proxy Manager: Add a Proxy Host with the service name as forward hostname and the container's internal port. Add the host IP to your hosts file on the development machine to reach it by local domain name.
Deploy Script
Put this deploy.sh in the root of each project repository:
#!/bin/bash
set -e
PROJECT="<project-name>"
VERSION="${1:-latest}"
SERVER="user@<host-ip>"
REMOTE_DIR="~/images"
echo "Building $PROJECT:$VERSION..."
docker build -t "$PROJECT:$VERSION" -t "$PROJECT:latest" .
echo "Exporting image..."
docker save "$PROJECT:latest" | gzip > "$PROJECT.tar.gz"
echo "Uploading to $SERVER..."
scp "$PROJECT.tar.gz" "$SERVER:$REMOTE_DIR/"
echo "Loading on server..."
ssh "$SERVER" "docker load < $REMOTE_DIR/$PROJECT.tar.gz"
echo "Cleaning up local tar..."
rm "$PROJECT.tar.gz"
echo "Done. Update the stack in Portainer: https://<host-ip>:9443"
Usage:
chmod +x deploy.sh
./deploy.sh # deploys latest
./deploy.sh 1.2.0 # deploys with version tag
Secrets and Volumes
Secrets
For any project using API keys or credentials, create secret files on the host before the first deploy:
ssh user@<host-ip>
mkdir -p ~/secrets/<project-name>
echo "your-actual-api-key" > ~/secrets/<project-name>/api_key.txt
chmod 600 ~/secrets/<project-name>/*.txt
Reference them in docker-compose.yml by file path. Never commit the secrets directory — keep a secrets.example/ folder in the repo with placeholder filenames only.
Volumes
Named volumes (databases, persistent data) — Docker manages these, visible in Portainer:
volumes:
db_data:
name: myproject_db_data
Bind mounts (models, uploads, logs) — create the directory on the host first:
ssh user@<host-ip> "mkdir -p /opt/<project-name>/models"
Then reference in compose:
volumes:
- type: bind
source: /opt/<project-name>/models
target: /models
read_only: true
Checklists
First Deploy
~/images/directory exists on host- Required directories under
/opt/<project>/created - Secrets files created under
~/secrets/<project>/ - Image built and uploaded
- Image loaded:
docker load - Portainer stack created and deployed
- Service reachable on its internal port
- NPM proxy host configured (if needed)
- Smoke test done
Update
- Image built and uploaded
- Image loaded:
docker load - Portainer → stack → Update the stack
- Smoke test done
Useful Commands on the Host
docker ps # running containers
docker ps -a # all containers
docker images # image list
docker logs <container-name> -f # follow container logs
docker image prune # remove unused images
docker system df # disk usage summary
Otthoni Docker szerver és deploy workflow
Egy személyes Docker hoszt — hely ahol mellékprojekteket, self-hosted szolgáltatásokat vagy AI kísérleteket futtathatsz anélkül, hogy felhőinfrastruktúráért fizetnél. Két reális opció van: egy régi gép Linuxszal, vagy WSL2 Windows alatt. Ez a post mindkettőt lefedi — és ami fontosabb, egy egységes deploy workflow-t mutat be, ami mindkét esetben ugyanúgy működik.
Kapcsolódó post: Ha régi hardverre telepítés közben bootolási problémákba ütközöl, lásd: A Legacy BIOS + GPT csapda.
A hoszt kiválasztása
A opció: Dedikált Linux gép
Bármely 10–15 évnél nem régebbi gép, amelyen van legalább 4 GB RAM és SSD, megfelel. Minél több RAM, annál több szolgáltatást lehet egyszerre futtatni.
Jó jelöltek: egy fiókban porosodó régi laptop, nyugdíjazott irodai asztali gép, vagy mini PC (Intel NUC, Beelink, stb.).
Az előnye: dedikált, mindig bekapcsolt hoszt, amely nem verseng a fejlesztői géppel az erőforrásokért. A hátránya a telepítési idő — különösen régi, Legacy BIOS-os gépeken.
Ajánlott OS: Debian 13 — stabil, minimális, kiváló Docker-támogatás, és a telepítő jól kezeli a régebbi hardvert.
B opció: WSL2 Windows alatt
Ha nincs tartalék gép, a WSL2 valódi alternatíva. A Docker Engine natívan fut WSL2 alatt, az SSH-hozzáférés működik, és a post-ban leírt deploy workflow azonos — ugyanazok a parancsok, ugyanazok az eszközök, ugyanaz az eredmény.
A kompromisszumok: a Docker hoszt verseng a Windowsszal a RAM-ért, és leáll, ha a fejlesztői gép leáll. Személyes projektekhez és tanuláshoz ez általában elfogadható.
A Linux hoszt beállítása (Debian 13)
OS telepítése: Töltsd le a Debian 13 netinstall ISO-t a debian.org oldalról, készíts bootolható USB-t Rufus segítségével, és telepíts a szöveges telepítővel. Particionálásnál válaszd a guided — teljes lemez, minden fájl egy partíción opciót. Szoftverválasztásnál: csak SSH server + Standard system utilities — semmilyen asztali környezet.
Első indítás — sudo telepítése:
su -
apt install sudo -y
usermod -aG sudo <felhasználónév>
exit
Jelentkezz ki és vissza. A sudo mostantól működik.
Statikus IP beállítása. Az interfész nevét az ip a paranccsal kérdezd le, majd szerkeszd az /etc/network/interfaces fájlt:
auto enp0s25
iface enp0s25 inet static
address 192.168.x.x
netmask 255.255.255.0
gateway 192.168.x.1
dns-nameservers 8.8.8.8 1.1.1.1
Alkalmazd: sudo systemctl restart networking. Az enp0s25 helyére a saját interfészneved kerül. A gateway IP Windows alatt az ipconfig kimenetéből olvasható le.
Tűzfal:
sudo apt install ufw -y
sudo ufw allow OpenSSH
sudo ufw allow 9443/tcp # Portainer
sudo ufw allow 80/tcp # Nginx Proxy Manager
sudo ufw allow 81/tcp # NPM admin
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
Időzóna: sudo timedatectl set-timezone Europe/Budapest
WSL2 beállítása
WSL2 engedélyezése rendszergazdai PowerShell ablakból:
wsl --install
Indítsd újra a gépet. Alapértelmezetten Ubuntu települ — ez megfelelő.
Statikus IP (opcionális): A WSL2 alapból dinamikus IP-t kap. Stabil cím érdekében add hozzá a %USERPROFILE%\.wslconfig fájlhoz:
[wsl2]
networkingMode=mirrored
Így a WSL2 a Windows IP-jét osztja meg.
SSH-hozzáférés engedélyezése WSL2-n belül:
sudo apt update && sudo apt install openssh-server -y
sudo service ssh start
Az automatikus indításhoz add a sudo service ssh start sort a ~/.bashrc fájlhoz.
Docker telepítése
A hivatalos Docker telepítési módszer Debianon és Ubuntu/WSL2-n azonos.
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
Ubuntu/WSL2 esetén a repository URL-ben és a gpg útvonalon a debian helyére ubuntu kerül.
Telepítés utáni lépés — Docker futtatása sudo nélkül:
sudo usermod -aG docker $USER
Jelentkezz ki és vissza, majd ellenőrizd: docker run hello-world
Dedikált Linux gépen engedélyezd az automatikus indítást:
sudo systemctl enable docker
sudo systemctl enable containerd
Portainer és Nginx Proxy Manager
Portainer CE
docker volume create portainer_data
docker run -d \
-p 8000:8000 \
-p 9443:9443 \
--name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
Elérhető: https://<host-ip>:9443. Az önaláírt tanúsítványra vonatkozó figyelmeztetést el kell fogadni — ez várható. Hozz létre admin felhasználót, és válaszd a Get Started → local opciót.
Nginx Proxy Manager
Hozd létre a ~/stacks/npm/docker-compose.yml fájlt:
services:
app:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
Majd: docker compose up -d
Az admin felület: http://<host-ip>:81. Alapértelmezett belépési adatok: admin@example.com / changeme — azonnal változtasd meg az első bejelentkezéskor.
A deploy workflow
Ez a post lényege. Az alábbi workflow működik, akár dedikált Linux gép, akár WSL2 a hoszt — a parancsok azonosak.
Fejlesztői gép Hoszt (Linux vagy WSL2)
──────────────────────────── ──────────────────────────────
1. docker build
2. docker save | gzip → scp →
3. docker load
4. Portainer stack deploy
5. Nginx Proxy Manager (opcionális)
1. lépés — Image build:
docker build -t <projekt>:1.0.0 -t <projekt>:latest .
2. lépés — Export:
docker save <projekt>:latest | gzip > <projekt>.tar.gz
3. lépés — Feltöltés (könyvtár egyszeri létrehozása):
ssh user@<host-ip> "mkdir -p ~/images"
scp <projekt>.tar.gz user@<host-ip>:~/images/
4. lépés — Betöltés a hoszton:
ssh user@<host-ip> "docker load < ~/images/<projekt>.tar.gz"
5. lépés — Deploy Portainerrel: Első deploy esetén: Portainer → Stacks → Add stack, illeszd be a docker-compose.yml tartalmát, állítsd be a környezeti változókat, és kattints a Deploy gombra. Frissítésnél: válaszd ki a stacket, és kattints az Update the stack gombra. A Portainer automatikusan felveszi az újonnan betöltött latest image-t.
Opcionálisan — Nginx Proxy Manageren keresztül: Adj hozzá egy Proxy Hostot a service névvel mint forward hostname és a konténer belső portjával. Add hozzá a hoszt IP-jét a fejlesztői gép hosts fájljához, hogy lokális domain névvel érhesd el.
Deploy szkript
Helyezd el ezt a deploy.sh fájlt minden projekt repository gyökérkönyvtárában:
#!/bin/bash
set -e
PROJECT="<projekt-neve>"
VERSION="${1:-latest}"
SERVER="user@<host-ip>"
REMOTE_DIR="~/images"
echo "Building $PROJECT:$VERSION..."
docker build -t "$PROJECT:$VERSION" -t "$PROJECT:latest" .
echo "Exporting image..."
docker save "$PROJECT:latest" | gzip > "$PROJECT.tar.gz"
echo "Uploading to $SERVER..."
scp "$PROJECT.tar.gz" "$SERVER:$REMOTE_DIR/"
echo "Loading on server..."
ssh "$SERVER" "docker load < $REMOTE_DIR/$PROJECT.tar.gz"
echo "Cleaning up local tar..."
rm "$PROJECT.tar.gz"
echo "Done. Update the stack in Portainer: https://<host-ip>:9443"
Használat:
chmod +x deploy.sh
./deploy.sh # latest deploy
./deploy.sh 1.2.0 # verziócímkével
Titkok és volume-ok
Titkok
Minden API kulcsot vagy bejelentkezési adatot igénylő projektnél az első deploy előtt hozd létre a titkos fájlokat a hoszton:
ssh user@<host-ip>
mkdir -p ~/secrets/<projekt-neve>
echo "valódi-api-kulcs" > ~/secrets/<projekt-neve>/api_key.txt
chmod 600 ~/secrets/<projekt-neve>/*.txt
A docker-compose.yml-ben fájlútvonallal hivatkozz rájuk. A secrets könyvtárat soha ne commitold — a repo-ban csak egy secrets.example/ mappa legyen, placeholder fájlnevekkel.
Volume-ok
Named volume-ok (adatbázisok, perzisztens adatok) — a Docker kezeli ezeket, a Portainerben láthatóak:
volumes:
db_data:
name: myproject_db_data
Bind mountok (modellek, feltöltések, logok) — előbb hozd létre a könyvtárat a hoszton:
ssh user@<host-ip> "mkdir -p /opt/<projekt-neve>/models"
Majd hivatkozz rá a compose fájlban:
volumes:
- type: bind
source: /opt/<projekt-neve>/models
target: /models
read_only: true
Ellenőrzőlisták
Első deploy
- A
~/images/könyvtár létezik a hoszton - A szükséges könyvtárak létrehozva
/opt/<projekt>/alatt - Titkos fájlok létrehozva
~/secrets/<projekt>/alatt - Image build és feltöltve
- Image betöltve:
docker load - Portainer stack létrehozva és deployolva
- A szolgáltatás elérhető a belső portján
- NPM proxy host beállítva (ha szükséges)
- Smoke teszt elvégezve
Frissítés
- Image build és feltöltve
- Image betöltve:
docker load - Portainer → stack → Update the stack
- Smoke teszt elvégezve
Hasznos parancsok a hoszton
docker ps # futó konténerek
docker ps -a # összes konténer
docker images # image lista
docker logs <konténer-neve> -f # konténer logok követése
docker image prune # nem használt image-ek törlése
docker system df # lemezhasználat összefoglaló