Docker dla początkujących: praktyczny przewodnik od zera
Ten artykuł jest dla osoby, która nie chce kolejnej definicji konteneryzacji, tylko chce faktycznie odpalić pierwszy kontener, zbudować własny obraz i zrozumieć, co się dzieje po drodze.
Przejdziemy przez Dockera na przykładach: najpierw gotowy obraz, potem własna mała aplikacja Node.js, następnie Dockerfile, porty, zmienne środowiskowe, wolumeny, sieci, debugowanie i sprzątanie po sobie.
Co zbudujesz w tym poradniku
Uruchomisz pierwszy kontener i sprawdzisz jego logi.
Zrozumiesz różnicę między obrazem a kontenerem.
Napiszesz minimalną aplikację HTTP w Node.js.
Dodasz Dockerfile i .dockerignore.
Zbudujesz własny obraz i uruchomisz go lokalnie.
Podłączysz port, zmienne środowiskowe, wolumen i osobną sieć Dockera.
Zobaczysz, kiedy przejść z pojedynczego
docker rundo Docker Compose.
Wymagania i instalacja Dockera
W oryginalnym poradniku zaczynaliśmy od bardzo praktycznej rzeczy: gdzie wejść i co zainstalować. To nadal jest ważne, bo bez działającego Docker Engine reszta komend będzie tylko teorią.
macOS
Wejdź na Docker Desktop for Mac.
Pobierz wersję dla swojego procesora: Apple Silicon albo Intel.
Przenieś Docker.app do Applications i uruchom aplikację.
Poczekaj, aż w menu bar zobaczysz, że Docker Desktop działa. Dopiero wtedy terminalowe komendy
docker ...zaczną odpowiadać.
docker version
docker run hello-worlddocker version
docker run hello-worldWindows
Wejdź na Docker Desktop for Windows.
Zainstaluj Docker Desktop z backendem WSL 2. Jeśli instalator poprosi o włączenie WSL, zrób restart i dokończ konfigurację.
Kod projektu trzymaj najlepiej w filesystemie WSL, np.
~/projects/app, a nie wC:\..., bo będzie szybciej i bliżej Linuxowego środowiska produkcyjnego.Uruchom Docker Desktop, otwórz terminal WSL i sprawdź komendy.
docker version
docker run hello-worlddocker version
docker run hello-worldLinux / Ubuntu
Na serwerach i Linuxowych stacjach roboczych zwykle instalujesz Docker Engine, bez Docker Desktop. Przykład dla Ubuntu:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/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/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$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-pluginsudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/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/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$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-pluginPo instalacji sprawdź usługę:
sudo systemctl status docker
sudo docker run hello-worldsudo systemctl status docker
sudo docker run hello-worldJeśli chcesz uruchamiać Dockera bez sudo, dodaj użytkownika do grupy docker i zaloguj się ponownie:
sudo usermod -aG docker $USER
newgrp docker
docker run hello-worldsudo usermod -aG docker $USER
newgrp docker
docker run hello-worldUwaga: grupa docker daje bardzo wysokie uprawnienia na maszynie. Na własnym laptopie to normalny kompromis wygody, ale na współdzielonym serwerze trzeba to robić świadomie.
Jak sprawdzić, czy instalacja jest gotowa
Minimalny smoke test po instalacji wygląda tak:
docker version
docker info
docker run --rm hello-worlddocker version
docker info
docker run --rm hello-worldJeśli docker version pokazuje część Client, ale nie pokazuje Server, CLI jest zainstalowane, ale daemon Dockera nie działa albo użytkownik nie ma do niego dostępu.
Najpierw mentalny model: image, container, registry
W Dockerze najłatwiej pogubić się przez nazwy. W praktyce wystarczą trzy pojęcia:
Image / obraz - gotowy szablon systemu plików i komendy startowej. Obraz budujesz albo pobierasz z registry.
Container / kontener - uruchomiona instancja obrazu. Kontener jest procesem z własnym izolowanym środowiskiem.
Registry - miejsce przechowywania obrazów, np. Docker Hub, GitHub Container Registry albo prywatne registry w firmie.
Dobra analogia: obraz to przepis i przygotowana paczka, kontener to faktycznie uruchomiony proces z tej paczki.
Krok 1: uruchom pierwszy kontener
Zacznijmy od najprostszego testu. hello-world pobiera mały obraz i uruchamia kontener, który wypisuje komunikat, a potem kończy działanie.
docker run hello-worlddocker run hello-worldTeraz coś praktyczniejszego: uruchom Nginx i wystaw go na lokalny port 8080.
docker run --name web-demo --rm -p 8080:80 nginx:alpinedocker run --name web-demo --rm -p 8080:80 nginx:alpineOtwórz w przeglądarce http://localhost:8080. Powinieneś zobaczyć stronę startową Nginx.
Co oznaczają flagi?
--name web-demonadaje kontenerowi czytelną nazwę.--rmusuwa kontener automatycznie po zatrzymaniu.-p 8080:80mapuje port 80 z kontenera na port 8080 na hoście.nginx:alpineto nazwa obrazu i tag.alpineoznacza lekką odmianę obrazu.
Krok 2: podstawowe komendy, których będziesz używać codziennie
W drugim terminalu sprawdź działający kontener:
docker psdocker psZobacz logi:
docker logs web-demodocker logs web-demoWejdź do środka kontenera shellem:
docker exec -it web-demo shdocker exec -it web-demo shWyjdź z shella komendą exit, a następnie zatrzymaj kontener:
docker stop web-demodocker stop web-demoPo docker stop kontener zniknie, bo uruchomiliśmy go z --rm. Gdyby nie było --rm, zobaczyłbyś zatrzymane kontenery tak:
docker ps -adocker ps -aKrok 3: przygotuj małą aplikację do konteneryzacji
Zrobimy minimalną aplikację HTTP w Node.js bez frameworka. Chodzi o to, żeby zobaczyć cały przepływ Dockera bez ukrywania szczegółów za Next.js czy NestJS.
mkdir docker-node-demo
cd docker-node-demo
npm init -ymkdir docker-node-demo
cd docker-node-demo
npm init -yUtwórz plik server.js:
const http = require("node:http");
const port = Number(process.env.PORT || 3000);
const message = process.env.MESSAGE || "Hello from Docker";
const server = http.createServer((req, res) => {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ message, path: req.url, hostname: process.env.HOSTNAME }));
});
server.listen(port, "0.0.0.0", () => {
console.log(`Server listening on 0.0.0.0:${port}`);
});const http = require("node:http");
const port = Number(process.env.PORT || 3000);
const message = process.env.MESSAGE || "Hello from Docker";
const server = http.createServer((req, res) => {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ message, path: req.url, hostname: process.env.HOSTNAME }));
});
server.listen(port, "0.0.0.0", () => {
console.log(`Server listening on 0.0.0.0:${port}`);
});Zmień scripts w package.json, żeby aplikację dało się wystartować przez npm start:
{
"name": "docker-node-demo",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
}
}{
"name": "docker-node-demo",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
}
}Sprawdź lokalnie bez Dockera:
npm start
# w drugim terminalu:
curl http://localhost:3000npm start
# w drugim terminalu:
curl http://localhost:3000Krok 4: napisz Dockerfile
Dockerfile to instrukcja budowy obrazu. Zacznijmy od prostej, czytelnej wersji.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]Co robią poszczególne linie?
FROM node:22-alpinewybiera bazowy obraz z Node.js.WORKDIR /appustawia katalog roboczy wewnątrz kontenera.COPY package*.json ./kopiuje manifesty zależności przed kodem, żeby Docker mógł zapisywać w pamięci podręcznej warstwę instalacji.RUN npm ci --omit=devinstaluje zależności produkcyjne w powtarzalny sposób.COPY server.js ./kopiuje kod aplikacji.EXPOSE 3000dokumentuje port aplikacji. SamoEXPOSEnie publikuje portu na hosta — od tego jest-p.CMD ["npm", "start"]definiuje domyślną komendę startową kontenera.
Krok 5: dodaj .dockerignore
Bez .dockerignore Docker może wysłać do build context rzeczy, których nie chcesz w obrazie: node_modules, .git, pamięć podręczna, pliki lokalne albo sekrety.
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
.dockerignore
.env
.env.*
coverage
dist
.nextnode_modules
npm-debug.log
.git
.gitignore
Dockerfile
.dockerignore
.env
.env.*
coverage
dist
.nextTo ważne nie tylko dla rozmiaru obrazu. To też kwestia bezpieczeństwa: sekret, który trafi do obrazu, może potem zostać w historii warstw.
Krok 6: zbuduj obraz
Będąc w katalogu z Dockerfile, zbuduj obraz i nadaj mu tag:
docker build -t docker-node-demo:1.0 .docker build -t docker-node-demo:1.0 .Sprawdź, czy obraz istnieje lokalnie:
docker images docker-node-demodocker images docker-node-demoJeśli build się wywali, najczęstsze powody to: brak pliku wskazanego w COPY, zbyt agresywny .dockerignore, brak lockfile albo komenda instalacji niedopasowana do package managera.
Krok 7: uruchom własny obraz
Uruchom aplikację z własnego obrazu:
docker run --name node-demo --rm -p 3000:3000 docker-node-demo:1.0docker run --name node-demo --rm -p 3000:3000 docker-node-demo:1.0W drugim terminalu sprawdź odpowiedź:
curl http://localhost:3000
curl http://localhost:3000/healthcurl http://localhost:3000
curl http://localhost:3000/healthJeśli działa, zatrzymaj kontener:
docker stop node-demodocker stop node-demoKrok 8: zmienne środowiskowe
Konfiguracji nie wpisuj na sztywno do obrazu. Podawaj ją przy uruchomieniu kontenera.
docker run --name node-demo --rm \
-p 3000:3000 \
-e MESSAGE='Configured at runtime' \
docker-node-demo:1.0docker run --name node-demo --rm \
-p 3000:3000 \
-e MESSAGE='Configured at runtime' \
docker-node-demo:1.0Teraz curl http://localhost:3000 powinien zwrócić JSON z innym message. To jest podstawowy wzorzec: obraz jest ten sam, konfiguracja zależy od środowiska.
Sekretów nie wkładaj do Dockerfile przez ENV, nie commituj .env i nie buduj obrazu z sekretami w środku. Sekrety powinny trafić do środowiska uruchomieniowego: zmienne środowiskowe, menedżer sekretów, CI/CD albo platforma wdrożeniowa.
Krok 9: bind mount w trybie pracy lokalnej
Do nauki i pracy programistycznej przydaje się bind mount: katalog z hosta jest widoczny wewnątrz kontenera. Dla naszej prostej aplikacji możesz podmienić server.js bez budowania obrazu od nowa.
docker run --name node-dev --rm \
-p 3000:3000 \
-v "$PWD/server.js:/app/server.js:ro" \
docker-node-demo:1.0docker run --name node-dev --rm \
-p 3000:3000 \
-v "$PWD/server.js:/app/server.js:ro" \
docker-node-demo:1.0:ro oznacza read-only. Kontener widzi plik, ale nie może go nadpisać. Dla kodu źródłowego to często bezpieczniejszy default.
Krok 10: named volume dla danych
Kontener jest ulotny. Jeśli usuniesz kontener, tracisz dane zapisane tylko w jego filesystemie. Dlatego bazy danych uruchamia się z wolumenem.
docker volume create postgres-data
docker run --name postgres-demo --rm \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=app \
-p 5432:5432 \
-v postgres-data:/var/lib/postgresql/data \
postgres:17-alpinedocker volume create postgres-data
docker run --name postgres-demo --rm \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=app \
-p 5432:5432 \
-v postgres-data:/var/lib/postgresql/data \
postgres:17-alpineW tym przykładzie kontener można zatrzymać i uruchomić ponownie, a dane zostaną w wolumenie postgres-data. Usuń wolumen dopiero wtedy, gdy świadomie chcesz usunąć dane:
docker volume ls
docker volume rm postgres-datadocker volume ls
docker volume rm postgres-dataKrok 11: sieć Dockera zamiast localhost
Gdy dwa kontenery mają ze sobą rozmawiać, nie zakładaj localhost. localhost wewnątrz kontenera oznacza ten konkretny kontener, nie hosta i nie inny kontener.
Utwórz sieć i uruchom Postgresa w tej sieci:
docker network create demo-net
docker run -d --name postgres-demo \
--network demo-net \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=app \
postgres:17-alpinedocker network create demo-net
docker run -d --name postgres-demo \
--network demo-net \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=app \
postgres:17-alpineTeraz inne kontenery w tej samej sieci mogą użyć hosta postgres-demo. To jest dokładnie ten sam mechanizm, który później w Docker Compose działa po nazwie serwisu.
docker run --rm --network demo-net postgres:17-alpine \
pg_isready -h postgres-demo -U postgresdocker run --rm --network demo-net postgres:17-alpine \
pg_isready -h postgres-demo -U postgresPosprzątaj:
docker stop postgres-demo
docker network rm demo-netdocker stop postgres-demo
docker network rm demo-netKrok 12: debugowanie kontenera
Kiedy coś nie działa, nie zgaduj. Docker daje kilka prostych komend diagnostycznych.
# działające kontenery
docker ps
# wszystkie kontenery, także zatrzymane
docker ps -a
# logi
docker logs <container-name>
# szczegóły konfiguracji kontenera
docker inspect <container-name>
# wejście do środka
docker exec -it <container-name> sh
# zużycie CPU/RAM
docker stats# działające kontenery
docker ps
# wszystkie kontenery, także zatrzymane
docker ps -a
# logi
docker logs <container-name>
# szczegóły konfiguracji kontenera
docker inspect <container-name>
# wejście do środka
docker exec -it <container-name> sh
# zużycie CPU/RAM
docker statsNajczęstszy błąd początkujących z aplikacjami webowymi: aplikacja słucha tylko na 127.0.0.1 wewnątrz kontenera. W kontenerze serwer powinien zwykle słuchać na 0.0.0.0, inaczej port może być poprawnie zmapowany, ale aplikacja i tak nie odpowie z hosta.
Krok 13: sprzątanie obrazów, kontenerów i pamięć podręczna
Docker potrafi zająć dużo miejsca, jeśli budujesz dużo obrazów. Zacznij od bezpiecznego przeglądu:
docker system df
docker images
docker ps -a
docker volume lsdocker system df
docker images
docker ps -a
docker volume lsUsuń zatrzymany kontener:
docker rm <container-id-or-name>docker rm <container-id-or-name>Usuń obraz:
docker rmi docker-node-demo:1.0docker rmi docker-node-demo:1.0Usuń nieużywane rzeczy, ale uważaj na wolumeny, bo mogą zawierać dane baz danych:
docker system prune
# bardziej agresywnie, razem z nieużywanymi obrazami:
docker system prune -a
# wolumeny tylko świadomie:
docker volume prunedocker system prune
# bardziej agresywnie, razem z nieużywanymi obrazami:
docker system prune -a
# wolumeny tylko świadomie:
docker volume pruneKiedy Docker Compose ma więcej sensu niż docker run
Pojedynczy docker run jest świetny do nauki i szybkiego testu. W realnym projekcie szybko pojawiają się jednak: aplikacja, baza danych, Redis, mailhog, proces roboczy, kolejka, emulator usługi plików, kolektor telemetrii. Wtedy jedna długa komenda przestaje być dokumentacją, a zaczyna być źródłem błędów.
W takim momencie przejdź do Compose. Zamiast ręcznie odpalać kilka komend, opisujesz usługi w compose.yaml i startujesz wszystko jedną komendą.
Następny krok: Docker Compose dla początkujących.
Docker w pracy z AI i nowoczesnym pracą programistyczną
Docker jest dziś szczególnie przydatny przy pracy z AI i agentami kodującymi. Agent nie powinien zgadywać, jaką wersję Node.js, Pythona, Playwrighta, pnpm, libvips czy Postgresa ma mieć lokalnie. Powinien dostać środowisko opisane jako kod.
Devcontainer daje zespołowi i agentom AI ten sam punkt startu.
Konteneryzowane zależności ograniczają sytuacje typu
działa u mnie.CI może uruchamiać testy integracyjne na takim samym zestawie usług jak lokalna praca programistyczna.
Nowy programista albo agent może wejść w projekt bez ręcznego instalowania połowy internetu.
Najczęstsze błędy początkujących
Mylenie obrazu z kontenerem. Obraz budujesz, kontener uruchamiasz.
Oczekiwanie, że
EXPOSEsamo wystawi port na hosta. Do tego potrzebujesz-p host:container.Trzymanie danych w filesystemie kontenera zamiast w wolumenie.
Kopiowanie całego repo do obrazu bez
.dockerignore.Wkładanie sekretów do Dockerfile albo budowanie obrazu z
.envw środku.Uruchamianie aplikacji webowej na
127.0.0.1zamiast0.0.0.0wewnątrz kontenera.Debugowanie przez zgadywanie zamiast
docker logs,docker inspect,docker execidocker ps -a.
Minimalna checklista do własnego projektu
Masz Dockerfile, który buduje się od zera na czystej maszynie.
Masz
.dockerignorei nie kopiujesz sekretów ani lokalnych artefaktów.Aplikacja słucha na
0.0.0.0, nie tylko nalocalhost.Konfiguracja trafia przez zmienne środowiskowe w czasie uruchomienia, nie przez hardcode w obrazie.
Dane trwałe są w wolumenach albo zewnętrznych usługach.
README zawiera konkretne komendy: build, run, logs, sprzątanie.
Jeśli masz więcej niż jedną usługę, masz plan przejścia na Docker Compose.
Docker nie jest magią ani miniaturowym VPS-em. To sposób na spakowanie procesu aplikacji z jego środowiskiem tak, żeby dało się go powtarzalnie uruchomić lokalnie, w CI i na serwerze.
Jeśli po tym poradniku umiesz zbudować obraz, uruchomić kontener, wystawić port, przekazać zmienne środowiskowe, podpiąć wolumen, stworzyć sieć i przeczytać logi, masz solidne podstawy. Dopiero wtedy Docker Compose zaczyna być naturalnym kolejnym krokiem, a nie kolejną warstwą chaosu.