Moje produkcyjne deploye trwają 15 minut. Nie patrzę na nie. Nie wstrzymuję oddechu. Zaparzam herbatę, a zanim się zaparzy, nowa wersja jest na produkcji.

Nie zawsze tak było.

Dwa lata temu deploy to był rytuał. Zarezerwuj godzinę w kalendarzu. Powiadom zespół. Odpal skrypt ręcznie. Wpatruj się w logi. Módl się. Napraw to, co się wysypało. Odpal ponownie. Módl się mocniej. Cały proces zajmował 2-3 godziny i zostawiał wszystkich wykończonych. Deployowaliśmy w piątki, bo najwyraźniej nienawidziliśmy siebie.

Jeśli brzmi znajomo, to znaczy, że odpalasz deploye na adrenalinie zamiast na systemach. Na dworze marzec 2026 — nie ma żadnego powodu, żeby tak żyć. Oto jak zamieniłem 3-godzinny festiwal lęku w 15 nudnych minut. ⚙️

Problem: jedenaście kroków w czyjejś głowie

Deploy — proces wypychania nowego kodu z Twojego komputera na serwer, gdzie użytkownicy go faktycznie widzą — oznaczał 11 różnych rzeczy naraz. Ściągnij kod. Zainstaluj zależności (zewnętrzne biblioteki, których potrzebuje Twoja aplikacja). Puść migracje (zaktualizuj strukturę bazy danych, żeby pasowała do nowego kodu). Zbuduj assety. Zrestartuj serwis. Wyczyść cache. Zaktualizuj reverse proxy. Sprawdź health endpoint.

Wszystko to żyło na stronie wiki, która była w 40% nieaktualna. Nowi członkowie zespołu bali się tego dotknąć. Starsi byli po prostu zmęczeni.

Rozwiązaniem nie był żaden wymyślny tool. Była nim dyscyplina.

Krok 1: Jeden skrypt, jedna komenda

Napisałem jeden skrypt bash — mały program, który wykonuje komendy po kolei, jak przepis, który komputer realizuje linijka po linijce. Nazwałem go deploy.sh. Robi wszystkie 11 rzeczy w kolejności. Jeśli jakikolwiek krok padnie, całość się zatrzymuje i mówi Ci, który krok się wysypał i dlaczego.

#!/bin/bash
set -euo pipefail

APP=$1
DEPLOY_LOG="/var/log/deploys/${APP}-$(date +%Y%m%d-%H%M%S).log"

echo "Deploying ${APP}..." | tee "$DEPLOY_LOG"

# Każdy krok: nazwa, komenda, rollback
deploy_step "Pull code"       "git -C /srv/${APP} pull origin main"
deploy_step "Install deps"    "cd /srv/${APP} && pip install -r requirements.txt"
deploy_step "Run migrations"   "cd /srv/${APP} && python manage.py migrate"
deploy_step "Build assets"    "cd /srv/${APP} && npm run build"
deploy_step "Restart service"  "systemctl restart ${APP}"
deploy_step "Health check"    "curl -sf http://localhost:8080/health"

echo "Deploy complete: ${APP}" | tee -a "$DEPLOY_LOG"

Funkcja deploy_step opakowuje każdą komendę w logowanie, mierzenie czasu i obsługę błędów. Jeśli health check padnie, automatycznie restartuje poprzednią wersję — robi rollback, czyli "cofnij i wróć do tego, co działało".

Czas konfiguracji: 45 minut. Te 45 minut oszczędziło setki godzin od tamtej pory. Kluczowa idea pochodzi wprost z metodologii Twelve-Factor App — jedno repozytorium, jeden build, jedna komenda deployowa.

Krok 2: CI/CD — robot, który deployuje za Ciebie

CI/CD to skrót od Continuous Integration / Continuous Deployment. Po ludzku: system, który automatycznie testuje Twój kod, kiedy pushsujesz zmiany (to część CI), a jeśli testy przechodzą, automatycznie deployuje go na serwer (to część CD). Ty pushsujesz kod, robot ogarnia resztę.

Dla małego zespołu nie potrzebujesz Jenkinsa (ciężki serwer automatyzacji), ArgoCD (narzędzie do deploymentu na Kubernetesie) ani klastra Kubernetes (system orkiestracji kontenerów — jeśli te słowa nic Ci nie mówią, świetnie, nie potrzebujesz tego). Potrzebujesz GitHub Actions i serwera z dostępem SSH. GitHub Actions wystartowało w 2019 roku i dojrzało do roli domyślnego wyboru CI/CD dla zespołów już pracujących na GitHubie — stabilne, dobrze udokumentowane i darmowe dla małych obciążeń.

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: pytest --tb=short

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: success()
    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          script: bash /srv/myapp/deploy.sh myapp

To cały pipeline — sekwencja zautomatyzowanych kroków, przez które kod podróżuje z Twojego laptopa na produkcję. Push na main (główna gałąź Twojego kodu). Testy lecą na serwerach GitHuba. Jeśli przechodzą, GitHub łączy się z Twoim serwerem po SSH (protokół bezpiecznego połączenia zdalnego) i odplala deploy.sh.

Bez kontenerów. Bez orkiestratorów. Bez plików YAML na 200 linii. Darmowy tier GitHub Actions daje 2000 minut miesięcznie — więcej niż wystarczająco dla małego zespołu deployującego 2-3 razy dziennie.

Krok 3: Health check, który naprawdę sprawdza zdrowie

Większość health checków to URL /health zwracający {"status": "ok"}. To mówi Ci, że serwer webowy działa. Nie mówi Ci nic o tym, czy aplikacja faktycznie działa. To jak pytanie kogoś "żyjesz?" zamiast "widzisz, słyszysz i ruszasz palcami?"

Moje health checki weryfikują trzy rzeczy:

@app.get("/health")
async def health():
    checks = {
        "database": await check_db_connection(),
        "cache": await check_redis_ping(),
        "disk": check_disk_space_above(20),  # procent
    }
    all_ok = all(checks.values())
    return JSONResponse(
        status_code=200 if all_ok else 503,
        content={"status": "healthy" if all_ok else "degraded", "checks": checks}
    )

Baza danych padła? Health check nie przechodzi. Redis (cache w pamięci — szybka warstwa tymczasowego przechowywania) nieosiągalny? Nie przechodzi. Dysk zapełniony w 95%? Nie przechodzi. Skrypt deployowy wywołuje ten endpoint po restarcie. Jeśli zwraca 503 (kod HTTP oznaczający "usługa niedostępna"), deploy automatycznie robi rollback.

Ten jeden endpoint złapał więcej problemów po deployu niż wszystkie nasze ręczne testy razem wzięte. ⚙️

Krok 4: Powiadomienia, nie dashboardy

Nie siedzę i nie wpatruję się w dashboardy monitoringu. Skrypt deployowy wysyła dokładnie dwie wiadomości:

  1. Deploy wystartował — "Deploying myapp o 14:32. Wyzwolony przez commit abc123."
  2. Deploy zakończony — "myapp deployed w 14m 22s" albo "myapp deploy FAILED na kroku: health check. Rollback wykonany."

Idą na kanał na Telegramie. Jeśli widzę powiadomienie o błędzie, badam sprawę. Jeśli widzę sukces, piję herbatę.

Bez Grafany (popularny tool do dashboardów). Bez PagerDuty (serwis alertów, który budzi Cię o 3 w nocy). Dla małego zespołu obsługującego 2-3 serwisy, wiadomość na Telegramie to odpowiedni poziom złożoności. Złożoność powinna odpowiadać skali problemu, nie skali Twoich ambicji. 🫶

Krok 5: Deploy w poniedziałek rano

Przestaliśmy deployować w piątki. Deployujemy w poniedziałki i środy rano.

Dlaczego? Bo jeśli coś się wysypie w poniedziałek, masz pięć dni na naprawę ze świeżą głową. Piątkowe deploye to debugowanie w weekend. Debugowanie w weekend to wypalenie. Wypalenie to błędy. Błędy to jeszcze więcej debugowania w weekend.

To jest pętla negatywnego sprzężenia zwrotnego — cykl, w którym każdy zły wynik powoduje następny. To nie jest strategia deploymentu.

Nudny harmonogram deployów: poniedziałek i środa rano. Nic po 15:00. Nic w piątki. Nic w porze obiadu. Jeśli nie jest gotowe na poniedziałkowe okno, czeka. Czekanie jest tanie. Wypalenie jest drogie.

Z czego rezygnujesz

Czas na szczerość. Ten setup ma swoje kompromisy.

Brak zero-downtime deploys. Kiedy leci systemctl restart, jest 2-5 sekund przerwy, gdy aplikacja jest niedostępna. Dla serwisu obsługującego miliony requestów to ma znaczenie. Dla większości małych zespołów nikt tego nie zauważa. Jeśli wyrośniesz z tego modelu, zainteresuj się blue-green deployments (uruchamianie dwóch kopii aplikacji i przełączanie ruchu między nimi) — ale nie zaczynaj od tego.

Jeden serwer, jeden punkt awarii. Jeśli serwer padnie, wszystko padnie. Redundancja kosztuje. Dla większości projektów poniżej 10 tys. dolarów miesięcznego przychodu, jeden dobrze utrzymany serwer z codziennymi backupami to właściwa odpowiedź. Problemy ze skalą to dobre problemy.

Brak konteneryzacji. Docker (narzędzie pakujące Twoją aplikację ze wszystkimi zależnościami w izolowaną jednostkę) jest świetny dla złożonych środowisk. Dla pythonowej aplikacji z bazą danych i Redisem na jednym serwerze, to zbędny narzut. Zawsze możesz go dodać później.

Badania DORA metrics od Google pokazują, że elitarne zespoły deployują na żądanie z lead time poniżej godziny. Ale pokazują też, że dochodzi się tam stopniowo — nie przez adopcję każdego toola na raz.

Rezultat

Przed: 3-godzinne deploye, 1-2 razy w tygodniu, zespół 3 osób zablokowany. Po: 15-minutowe deploye, 2-3 razy w tygodniu, zero osób patrzących.

Miesięczna oszczędność czasu: roughly 24 godziny zbiorowego czasu zespołu. Roczny koszt setupu CI/CD: 0 zł (darmowy tier GitHub Actions plus skrypt bash). Czas konfiguracji: jeden weekend.

Niech będzie nudno

Celem dobrego opsu nie jest sprawienie, żeby deploye były ekscytujące. Celem jest sprawienie, żeby były tak nudne, że zapominasz, że się dzieją.

Kiedy deployujesz bez adrenaliny, deployujesz częściej. Kiedy deployujesz częściej, każdy deploy jest mniejszy. Kiedy każdy deploy jest mniejszy, mniej rzeczy się psuje. Kiedy mniej rzeczy się psuje, lepiej śpisz.

Cała filozofia w jednym zdaniu. Jeden skrypt. Jeden pipeline. Jeden health check. Jeden kanał z powiadomieniami. Jeden spokojny poranek w tygodniu.

Niech będzie nudno. Niech będzie małe. Niech będzie automatyczne. Idź zaparz herbatę. 🫶