Jeden plik, cała aplikacja

W świecie zdominowanym przez wielowarstwowe stosy technologiczne, zarządzanie zależnościami i skomplikowane procesy wdrożeniowe, istnieje podejście radykalnie prostsze. Single binary deployment to strategia, w której cała aplikacja — kod, szablony, zasoby statyczne, konfiguracja domyślna — jest skompilowana do jednego pliku wykonywalnego.

Go jest jednym z niewielu języków, które czynią tę strategię naturalną i praktyczną. W naszej infrastrukturze każdy serwis to pojedynczy plik binarny uruchomiony w minimalnym kontenerze Docker. Brak interpreterów, brak runtime'ów, brak katalogów z zależnościami.

Dyrektywa go:embed

Kluczowym elementem jest dyrektywa //go:embed, wprowadzona w Go 1.16. Pozwala wbudować pliki i katalogi bezpośrednio w binarny plik wykonywalny na etapie kompilacji:

package main

import "embed"

//go:embed templates/*.html
var templates embed.FS

//go:embed static/*
var staticFiles embed.FS

Po kompilacji plik binarny zawiera wszystkie szablony HTML, arkusze CSS, skrypty JavaScript, obrazki i inne zasoby. Nie trzeba ich kopiować na serwer produkcyjny ani martwić się o ścieżki i uprawnienia.

W naszych serwisach embed obejmuje:

  • Szablony HTML — wszystkie strony i komponenty
  • CSS i JavaScript — style i skrypty frontend
  • Grafiki SVG — ikony i logotypy
  • Pliki konfiguracyjne — domyślne ustawienia

Efekt jest natychmiastowy: kopiujesz jeden plik na serwer, uruchamiasz go i aplikacja działa. Żadnych brakujących plików, żadnych problemów ze ścieżkami.

Multi-stage Docker build

Docker pozwala oddzielić środowisko budowania od środowiska uruchomieniowego. W naszych projektach stosujemy dwuetapowy build:

# Etap 1: budowanie
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

# Etap 2: uruchomienie
FROM alpine:3.20
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

Pierwszy etap używa pełnego obrazu Go z kompilatorem i narzędziami (około 300 MB). Drugi etap to czysty Alpine Linux (5 MB) plus nasz plik binarny. Wynikowy obraz waży 10-15 MB — to ułamek rozmiaru typowej aplikacji Node.js czy Python.

Flaga CGO_ENABLED=0 wyłącza kompilator C, dzięki czemu binarny plik jest w pełni statycznie linkowany. Oznacza to, że nie potrzebuje żadnych bibliotek współdzielonych w systemie docelowym — zadziała nawet na pustym obrazie scratch.

Zalety jednego pliku binarnego

Atomowe wdrożenia — deployment to podmiana jednego pliku. Nie ma stanu pośredniego, w którym część plików jest nowa, a część stara. Aplikacja albo działa w nowej wersji, albo w starej.

Natychmiastowe rollbacki — jeśli nowa wersja ma problem, wystarczy uruchomić poprzedni plik binarny. Nie trzeba przywracać dziesiątek plików ani odtwarzać katalogu node_modules. Przechowujemy ostatnie 3 wersje binarki — rollback trwa sekundy.

Brak zależności runtime — na serwerze produkcyjnym nie ma interpretera Python, runtime'u Node.js ani JVM. Mniej oprogramowania = mniejsza powierzchnia ataku. W naszych kontenerach jest dosłownie system operacyjny i jeden plik.

Szybki cold start — aplikacja Go startuje w milisekundach. Nie ma rozgrzewania JIT, ładowania modułów ani interpretacji kodu. To ważne w kontekście konteneryzacji, gdzie kontenery mogą być uruchamiane i zatrzymywane dynamicznie.

Powtarzalność — ten sam plik binarny zachowuje się identycznie na każdym serwerze. Nie ma ryzyka, że wersja interpretera, brakujący pakiet systemowy czy inna konfiguracja środowiska spowoduje nieoczekiwane zachowanie.

Go:embed vs alternatywy

Przed go:embed stosowano różne podejścia do osadzania zasobów: generatory kodu (go-bindata, pkger), narzędzia konwertujące pliki na tablice bajtów w Go, czy po prostu kopiowanie plików na serwer.

go:embed ma kluczowe przewagi:

  • Jest częścią standardowej biblioteki — brak zewnętrznych zależności
  • Działa z go build bez dodatkowych kroków
  • Obsługuje system plików (embed.FS) kompatybilny z interfejsem fs.FS
  • Kompilator weryfikuje istnienie plików — brakujący plik to błąd kompilacji, nie runtime'owy panic

Kiedy single binary nie wystarczy

Uczciwie trzeba przyznać ograniczenia tego podejścia:

  • Duże zasoby medialne — wbudowywanie gigabajtów zdjęć czy filmów w binarny plik nie ma sensu. Statyczne media lepiej serwować przez CDN lub dedykowany serwer plików.
  • Dynamiczna konfiguracja — zmienne środowiskowe i pliki .env nadal są potrzebne dla danych wrażliwych (klucze API, hasła bazy danych). Embed jest dla zasobów, nie sekretów.
  • Częste zmiany treści — jeśli treści strony zmieniają się codziennie, rekompilacja przy każdej zmianie może być uciążliwa. W takim przypadku lepszy jest CMS lub baza danych.
  • Bardzo duże aplikacje — projekty z setkami megabajtów zasobów frontendowych (SPA z wieloma bibliotekami) mogą generować niepraktycznie duże pliki binarne.

Praktyczne wskazówki

Kilka lekcji z naszego doświadczenia:

  • Organizuj embedded pliki w dedykowanych katalogach (templates/, static/, content/) — ułatwia zarządzanie i selektywne embed
  • Używaj embed.FS z http.FileServer do serwowania plików statycznych — to kilka linii kodu
  • Dodaj nagłówki cache (Cache-Control, ETag) do plików statycznych — choć są w binarce, przeglądarka nadal powinna je buforować
  • W trybie deweloperskim ładuj pliki z dysku (nie z embed) — umożliwia hot-reload bez rekompilacji

Podsumowanie

Single binary deployment z Go to podejście, które eliminuje całą klasę problemów wdrożeniowych. Dyrektywa go:embed czyni je naturalnym — zasoby są częścią aplikacji, nie zewnętrznymi plikami do zarządzania. W połączeniu z multi-stage Docker build otrzymujemy minimalne, szybkie i niezawodne kontenery.

Jeśli planujesz nowy projekt webowy i cenisz prostotę wdrożeń, skontaktuj się z nami — chętnie pokażemy, jak to wygląda w praktyce.