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 buildbez dodatkowych kroków - Obsługuje system plików (
embed.FS) kompatybilny z interfejsemfs.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
.envnadal 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.FSzhttp.FileServerdo 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.