Docker
Jak używam Dockera do lokalnego programowania
Konfiguracja Dockera na lokalnym komputerze — od zera, krok po kroku.
film wkrótce na YouTubeLokalnie korzystam właściwie wyłącznie z Dockera — i to nieprzypadkowo. Jeśli piszecie w Go, C#, C, PHP, Ruby czy czymkolwiek innym, to kiedyś trzeba było to po prostu zainstalować na swoim komputerze. A potem przychodzi problem: potrzebujecie Node'a w jednej wersji do jednego projektu i w zupełnie innej do drugiego, więc trzeba się między tymi wersjami przełączać, trzymać jakieś menadżery wersji i tak dalej. Cały ten problem rozwiązuje Docker.
Prawdziwa historia z produkcji
To nie jest tylko wygoda do zabawy lokalnie — w bardziej profesjonalnym programowaniu Docker też się bardzo przydaje, i opowiem wam, jak to wyglądało u mnie na żywym przykładzie.
Mieliśmy aplikację napisaną w Ruby on Rails, która zaczęła się rozrastać. Ruby jest naprawdę fajnym językiem pod względem składni — moim zdaniem jednym z najpiękniejszych — ale jeśli chodzi o wydajność, to już nie jest jakiś szał. Do prostych rzeczy nie ma nic lepszego, ale gdy pojawia się coś bardziej obciążającego, np. procesowanie obrazków czy dodawanie znaku wodnego, to przy większej skali zaczyna to zjadać sporo pamięci i czasu.
Rozwiązaniem było przeniesienie takiej funkcjonalności do osobnej aplikacji w Node.js. Stworzyliśmy nową aplikację Node'a obok istniejącej, trzymając wszystko w jednym repozytorium — takie monorepo, gdzie mamy kilka różnych aplikacji: jedna do wysyłki maili, druga do obrazków, trzecia do komunikacji z AI, czwarta do czegoś jeszcze innego. Super w tym jest to, że możemy w każdej chwili np. wywalić aplikację do obrazków i przepisać ją całkowicie w innym języku, i nie stanowi to żadnego problemu — bo każda z nich żyje we własnym, odizolowanym kontenerze.
Budowa konfiguracji od zera
Pokażę wam, jak od zera ustawić taki projekt. Docker instaluje się bez problemu na Macu, Ubuntu czy Windowsie, więc nie będę się nad tym rozwodził — zakładam, że już go macie zainstalowanego.
Zaczynamy od nowego katalogu i otwieramy go w edytorze (ja korzystam z Visual Studio Code). W pustym projekcie potrzebujemy dwóch rzeczy: pliku docker-compose.yml i osobnych katalogów na każdy z naszych serwisów — w moim przykładzie node-hello, ruby-hello i go-hello. W realnym, produkcyjnym projekcie równie dobrze mogłyby się tu znaleźć np. image-processor czy generate-pdf — cała logika zostaje taka sama, bez względu na to, ile serwisów dorzucicie.
docker-compose.yml
Zaczynamy od sekcji services, w której wypisujemy wszystkie serwisy, jakie nas interesują:
services:
node-hello:
container_name: node-hello
build:
context: ./node-hello
command: 'tail -f /dev/null'
volumes:
- ./node-hello:/appKilka rzeczy wartych wyjaśnienia:
- container_name — opcjonalny, ale fajnie go mieć, bo konfiguracja się lepiej czyta.
- build.context — mówi Dockerowi, gdzie znaleźć katalog z
Dockerfile, z którego ma zbudować obraz. - command: 'tail -f /dev/null' — to moim zdaniem najważniejsza linijka tutaj. Czasem nie chcemy, żeby kontener od razu uruchamiał naszą aplikację — chcemy, żeby po prostu „żył” w tle, gotowy na to, żebyśmy sami wygodnie do niego wchodzili.
tail -foznacza „wyświetlaj w kółko zawartość pliku”, a/dev/nullto plik, który zawsze jest pusty — więc kontener w kółko „wyświetla nic”, dzięki czemu może działać bez końca, nie robiąc nic konkretnego, dopóki my mu czegoś nie każemy. - volumes — to jest kluczowe, żeby kontener widział zmiany w naszym kodzie. Mówimy Dockerowi: to, co mam lokalnie w
node-hello, zamontuj wewnątrz kontenera jako/app. Dzięki temu edytuję pliki u siebie, a kontener od razu widzi te zmiany, bez przebudowywania obrazu.

Dockerfile
Każdy serwis ma też swój Dockerfile — w moim przypadku bardzo prosty:
FROM node:24.1.0-alpine
RUN mkdir /app
WORKDIR /app
COPY . /appBierzemy gotowy obraz node z internetu (takich obrazów jest mnóstwo — gołe języki, gotowe frameworki typu Postgres, Rails czy Django, dosłownie wszystko), tworzymy w nim katalog /app, ustawiamy go jako katalog roboczy i kopiujemy do niego zawartość naszego lokalnego folderu.

Uruchamianie i wchodzenie do kontenera
Mając to wszystko gotowe, budujemy i uruchamiamy kontenery jedną komendą z głównego katalogu:
docker compose up -d --buildDocker ściąga obraz z internetu, buduje go na podstawie naszego Dockerfile, i uruchamia. Możemy sprawdzić, co w tej chwili działa, poleceniem docker ps.
Następny krok — wejście do środka kontenera:
docker exec -it node-hello sh-it to tryb interaktywny z terminalem, node-hello to nazwa kontenera, a na końcu podajemy powłokę, jakiej chcemy użyć — czasem jest dostępny bash, a jeśli nie, zawsze powinien być sh, taka bardziej prymitywna, ale zawsze obecna powłoka.

I jesteśmy w środku — Node jest tam już zainstalowany, więc możemy od razu pisać i uruchamiać kod:
docker exec node-hello node index.jsgdzie index.js to po prostu:
console.log("siemanko")To samo dla Ruby i Go
W dzisiejszych czasach raczej nie piszę już takich powtarzalnych rzeczy ręcznie — po prostu poprosiłem swojego asystenta AI, żeby dopisał identyczną konfigurację dla ruby-hello i go-hello, na wzór tego, co już miałem dla Node'a. Finalne pliki wyglądają tak:
FROM ruby:3.4.4-alpine
RUN mkdir /app
WORKDIR /app
COPY . /appputs "siemanko"FROM golang:1.24.4-alpine
RUN mkdir /app
WORKDIR /app
COPY . /apppackage main
import "fmt"
func main() {
fmt.Println("siemanko")
}Uruchamiam wszystko na raz tą samą komendą docker compose up -d --build, a potem wchodzę do każdego kontenera osobno, dokładnie tak samo jak przy Node'ie:
docker exec ruby-hello ruby main.rb
docker exec go-hello go run main.goWarto zwrócić uwagę na jedną rzecz, która moim zdaniem jest w tym wszystkim najfajniejsza: wchodząc do kontenera ruby-hello i próbując sprawdzić wersję Node'a, dostaniemy informację, że go tam po prostu nie ma — bo to jest kontener Ruby'ego, nic więcej. Każde środowisko jest w pełni odizolowane od pozostałych.
Podsumowanie
Dzięki takiemu podejściu mogę bardzo łatwo pisać aplikacje w dowolnym języku i używać ich lokalnie, bez zaśmiecania własnego systemu niczym. Można też bez problemu wrzucić do takiego kontenera np. całą aplikację frontendową w Reakcie — to nie stanowi żadnej różnicy. Takie kontenery da się w każdej chwili zabić, usunąć i zbudować od nowa, więc zdecydowanie jest to mój preferowany sposób pracy nad kodem lokalnie — i, co ważne, dokładnie to samo podejście sprawdza się też na produkcji.
