Docker + Bun + Caddy: mikroslužba za hodinu

Potřebujete rychle spustit malou mikroslužbu bez složité infrastruktury, ruční správy certifikátů a konfigurace Nginxu? V tomto článku si ukážeme, jak pomocí Bun, Caddy a Dockeru jako univerzálního prostředí vytvořit a nasadit plnohodnotnou mikroslužbu během jedné hodiny.
Nálepky:
Určitě znáte ten moment: chcete si rychle otestovat nový nápad. Spustit malou API službu, která například vrací detaily o SSL certifikátu, nebo resolvuje DSN záznamy dané domény. Jenže místo psaní produktivního kódu skončíte u nekonečné konfigurace Dockeru, Certbotu, reverzní proxy v Nginx a PM2, aby bylo službu vůbec možné spustit a bezpečně provozovat.
Jde to bez zbytečných zádrhelů — s čistým, přehledným a plně funkčním setupem. Zůstane nám Docker jako nositel aplikace, ale odpadne potřeba Nginx, ručního generování certifikátů pomocí Certbotu a PM2 pro persistenci Nodejs serveru.
Ukážeme si, jak pomocí Bun (moderní JavaScript runtime), Caddy serveru pro automatické HTTPS a reverzní proxy a Dockeru jako univerzálního prostředí spustit mikroslužbu během jediné hodiny. Celé řešení je navíc snadno přenositelné a běží bez závislostí na dalších službách.
Protože se jedná o mikroslužbu s minimální zátěží (řádově desítky requestů za hodinu), dovolíme si malý kompromis proti „best practices“ – provozovat dvě služby v jednom Docker kontejneru. V tomto případě to znamená jednodušší nasazení, menší režii a maximální přehlednost. Pro větší projekty je vhodné oddělit reverzní proxy a aplikační server do samostatných kontejnerů.
Náš projekt bude mít následující strukturu:
.
├── .docker
│ ├── Dockerfile # Build docker kontejneru
│ └── Caddyfile # Konfigurace Caddy serveru
├── index.ts # Javascript aplikace
├── docker-bake.hcl # Předpis pro multi-platform build
├── package.json # Závislosti a konfigurace
└── bun.lock # Lock file
Code language: plaintext (plaintext)
Začneme aplikaci
Pro backend jsem zvolil Bun, protože představuje moderní (např. nativní podpora Typescript) a výrazně rychlejší alternativu k tradičnímu Node.js. Bun kombinuje Javascript runtime, balíčkovací systém a buildovací nástroje. Má nižší paměťové nároky a skvěle se hodí nejen pro malé mikroslužby. Vytvořme si tedy nejprve index.ts
. Naše aplikace bude jednoduchá, vlastně nás jen pozdraví:
import { serve } from "bun";
const PORT = Bun.env.PORT || 3000;
serve({
port: PORT,
routes: {
'/': () => {
return new Response("Hello, from Bun!");
},
'/api': () => {
return Response.json({"message": "Hello, from API"})
}
}
}
);
Code language: TypeScript (typescript)
Využili jsme serve, které je přímo součastí knihoven Bun a umožní nám spustit HTTP server na vybraném portu. Kdybychom potřebovali komplikovanější aplikaci, nebo například ověřování identity uživatele pomocí JWT tokenů, je vhodnější zvolit Express.js nebo ElysiaJS. Bun serve totiž zatím stále neumí middleware. Naši aplikaci si můžeme vyzkoušet pomocí příkazu:
bun run index.ts
Code language: Bash (bash)
Předpokládejme, že tím máme hotovo — http://localhost:3000
nás pozdravil — aplikace dělá přesně to, co jsme chtěli.
Build aplikace
Pro sestavení našeho serveru použijeme oficiální Docker image oven/bun:alpine
. Nejprve do kontejneru zkopírujeme package.json
a bun.lock
. Soubor bun.lock
nám zajistí, že při instalaci nedojde k žádné změně námi použitých balíčků.
FROM oven/bun:alpine AS build
WORKDIR /app
COPY package.json package.json
COPY bun.lock bun.lock
Code language: Dockerfile (dockerfile)
Povšimněte si také prvního řádku FROM oven/bun:alpine AS build
— ten build
je totiž název našeho kontejneru, budeme ho ještě potřebovat. Dále spustíme instalaci závislostí:
ENV NODE_ENV=production
RUN bun install --frozen-lockfile
Code language: Dockerfile (dockerfile)
a samozřejmě přidat samotnou aplikaci:
COPY index.ts index.ts
Code language: Dockerfile (dockerfile)
Poslední krok Dockerfile se postará o build aplikace. Pomocí příkazu bun build
se TypeScriptový soubor index.ts
přeloží do samostatného spustitelného binárního souboru pojmenovaného myserver
. Volby --compile
, --minify-whitespace
a --minify-syntax
zajistí, že výsledný soubor bude zkompilovaný a optimalizovaný, a příkaz chmod +x myserver
nastaví soubor jako spustitelný. Díky tomu lze server jednoduše spustit bez potřeby dalšího runtime.
RUN bun build \
--compile \
--minify-whitespace \
--minify-syntax \
--outfile myserver \
index.ts && \
chmod +x myserver
Code language: Dockerfile (dockerfile)
Nyní se můžeme podívat na nastavení Caddy serveru.
Caddy server
Caddy je moderní webový server a reverzní proxy, který si získal popularitu díky své jednoduchosti a automatickému HTTPS. Na rozdíl od klasického Nginxu nebo Apache nevyžaduje složité konfigurace ani ruční správu certifikátů — Caddy je generuje i obnovuje sám pomocí Let’s Encrypt. Je napsaný v jazyce Go, má velmi nízké nároky na provoz a konfiguraci definuje přehledně v jediném souboru Caddyfile
.
Další velkou výhodou je, že se Caddy dá snadno rozšířit pomocí modulů. My jedno takové rozšíření použijeme, jmenuje se caddy-supervisor a postará se nám o persistenci naší Javascript aplikace – tím se zbavíme PM2.
Modul „supervisor“ není součástí základní distribuce a je nutné jej přidat pomocí nástroje xcaddy nebo sestavit vlastní binárku. Do našeho Dockerfile
si přidáme následující dva řádky. Povšimněte si opět prvního řádku caddy
, bude našeho kontejneru:
FROM caddy:builder AS caddy
RUN xcaddy build --with github.com/baldinof/caddy-supervisor
Code language: Dockerfile (dockerfile)
Uvnitř tohoto kontejneru budeme chtít spustit naši aplikaci na portu 3000, zatímco ven bude dostupná na klasických portech 443 a 80. Základní konfigurace Caddyfile může vypadat velmi jednoduše – stačí tři řádky:
:443 {
reverse_proxy localhost:3000
}
Code language: plaintext (plaintext)
Tímto získáme zabezpečený server dostupný na https://localhost se self-signed certifikátem, který bude reverzní proxy směrovat na localhost:3000
. Ale kde je náš supervisor?
Správná poznámka — na portu 3000 zatím nic neběží! Proto bude konfigurace o něco složitější. Aby Caddy spustil náš server na pozadí, přidáme globální nastavení, která se v konfiguraci uzavírají do složených závorek {}
.
{
supervisor {
myserver {
restart_policy always
redirect_stdout stdout
redirect_stderr stderr
}
}
}
Code language: plaintext (plaintext)
Dále je vhodné mít přístup k logům, aby bylo možné sledovat chování služby a odchytávat případné problémy. Úroveň podrobnosti logování si můžete zvolit podle potřeby – pro produkci je vhodné volit úspornější režim, zatímco pro testování nebo ladění se hodí nastavit level = debug
, aby byly zaznamenány všechny detaily. Takto získáte rychlou zpětnou vazbu o příchozích requestech, chybách a aktivitě Bun serveru i reverzní proxy.
{
log {
output stdout
format console
level DEBUG
}
}
Code language: plaintext (plaintext)
Pro snadnější nasazení kontejneru do produkce i pohodlné testování na lokálním počítači si upravíme konfiguraci Caddy tak, aby místo pevně daného portu :443
používala proměnnou prostředí ${DOMAIN:localhost}
. To znamená, že pokud je nastavena proměnná $DOMAIN
, Caddy ji použije; pokud není, fallback je localhost
.
Současně jsme také povolili dotazy z libovolného hosta pomocí hlavičky Access-Control-Allow-Origin
a definovali metody a hlavičky pro CORS, aby API bylo snadno dostupné z různých frontendů. Konečně, reverse_proxy localhost:3000
směruje požadavky na náš Bun server běžící uvnitř kontejneru.
{$DOMAIN:localhost} {
encode gzip
header Access-Control-Allow-Origin "*"
header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE"
header Access-Control-Allow-Headers "Content-Type, Authorization"
reverse_proxy localhost:3000
}
Code language: plaintext (plaintext)
Finální sestavení
✓ Máme hotovou aplikaci. ✓ Máme připraven spustitelný soubor. ✓ Máme nastavený Caddy server včetně modulu caddy-supervisor. Pojďme to dát vše dohromady.
Finální fáze našeho Dockerfile bude vypadat následovně, začneme opět nad oven/bun:alpine
:
FROM oven/bun:alpine AS bun-app
ENV NODE_ENV=production
Code language: Dockerfile (dockerfile)
Zkopírujeme si Caddy server z caddy
kontejneru a přidáme konfigurační soubor Caddyfile
:
COPY --from=caddy /usr/bin/caddy /usr/bin/caddy
COPY .docker/Caddyfile /etc/caddy/Caddyfile
Code language: Dockerfile (dockerfile)
Do kontejneru přidáme binární soubor myserver
, který jsme vytvořili ve fázi build
:
COPY --from=build /app/myserver /usr/local/bin/myserver
Code language: Dockerfile (dockerfile)
poté spustíme Caddy server a vystavíme porty 80
a 443
:
EXPOSE 80 443
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]
Code language: Dockerfile (dockerfile)
Build Docker kontejneru
Jakmile máme připravený kompletní Dockerfile, můžeme sestavit náš kontejner. Pokud používáte klasický Docker, stačí spustit příkaz:
docker build -t bun-app -f .docker/Dockerfile .
Code language: Bash (bash)
Tím vytvoříme image pojmenovaný bun-app
. Pokud používáte modernější a rychlejší nástroj Buildx, který podporuje i multi-platformní buildy, můžete využít připravený soubor docker-bake.hcl
:
variable "TAG" {
default = "latest"
}
target "bun-app" {
context = "."
dockerfile = ".docker/Dockerfile"
target = "bun-app"
platforms = ["linux/amd64", "linux/arm64"]
pull = true
tags = [
"docker.io/username/bun-app:${TAG}",
"bun-app:${TAG}"
]
}
Code language: plaintext (plaintext)
Build kontejneru pomocí nástroje buildx spustíte pomocí příkazu:
docker buildx bake bun-app
Pokud jste s buildx nikdy nepracovali, musíte si nejprve vytvořit novou instanci builderu, například takto:
docker buildx create \
--name multiarch \
--driver docker-container \
--use
Code language: Bash (bash)
Pokud chceme svůj hotový kontejner poslat do Docker repository, stačí přidat parametr --push
:
docker buildx bake bun-app --push
Code language: Bash (bash)
Další možnost je přidat místo --push
parametr --load
, který načte nově vytvořený kontejneru do vašeho lokálního Dockeru. Existuje zde však jedno omezení, exporter docker zatím neumí s manifest listy pracovat – umí uložit jen jednu architekturu — musíte mu tedy říct, že chcete vytvořit kontejner jen pro svoji platformu:
docker buildx bake bun-app
--set bun-app.platform=linux/arm64 \
--load
Code language: Bash (bash)
Finálním krokem je spuštění na produkci. Předpokládejme, že máme hotový kontejner – buď jsme ho nahráli do Docker Hubu (nebo jiného registry), nebo jsme ho načítali lokálně pomocí parametru --load
při buildu. V obou případech je spuštění maximálně jednoduché:
Lokální spuštění
docker run --rm \
--name bun-app \
-p "80:80" \
-p "443:443" \
-e DOMAIN="https://localhost" \
bun-app:latest
Code language: Bash (bash)
Spuštění na serveru
Na serveru můžete spustit svůj kontejner například pomocí docker compose
, případně stačí přidat parametr -d
, který zařídí nastartování kontejneru na pozadí:
docker run --name bun-app \
-d \
-p 80:80 \
-p 443:443 \
-e DOMAIN=api.example.com \
bun-app:latest
Code language: Bash (bash)
Caddy se postará o automatické HTTPS, reverzní proxy, a díky modulu supervisor také o to, že váš Bun server se po pádu znovu spustí. Takto získáte produkčně nasaditelnou mikroslužbu s plně automatizovaným SSL a minimální konfigurací — a to vše v jediném Docker kontejneru. Kompletní zdrojové kódy naleznete v Docker & Bun & Caddy na mém GitHub.
Konečně tady vyšlo něco jiného než PR Články