From 162d485c31233a19ff5e2a2c2fc28beb5c38e143 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Wed, 28 Jan 2026 14:07:02 -0700 Subject: [PATCH] Add Docker infrastructure with caddy-docker-proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-stage Dockerfile (node build → caddy:2-alpine production, node dev). Compose base + dev override for prod/dev switching via .env ENVIRONMENT flag. Makefile targets: build, up, down, restart, logs, dev, prod, clean, rebuild. Caddyfile serves static Astro output with caching and compression. --- .dockerignore | 10 +++++++ Caddyfile | 16 ++++++++++++ Dockerfile | 38 +++++++++++++++++++++++++++ Makefile | 59 ++++++++++++++++++++++++++++++++++++++++++ docker-compose.dev.yml | 21 +++++++++++++++ docker-compose.yml | 16 ++++++++++++ 6 files changed, 160 insertions(+) create mode 100644 .dockerignore create mode 100644 Caddyfile create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6c46e30 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.git +.gitignore +.env +*.md +!README.md +Makefile +docker-compose*.yml +.astro diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..fcac428 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,16 @@ +:80 { + root * /srv + file_server + encode gzip zstd + + # SPA-style fallback for clean URLs + try_files {path} {path}/ {path}/index.html =404 + + # Cache static assets aggressively + @static path /_astro/* /pagefind/* /favicon.svg + header @static Cache-Control "public, max-age=31536000, immutable" + + # Short cache for HTML (allows quick content updates) + @html path *.html / + header @html Cache-Control "public, max-age=3600" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0aeb661 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# --- Build stage --- +FROM node:22-slim AS build + +WORKDIR /app + +# Install dependencies first (layer caching) +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy source and build +COPY . . +ENV ASTRO_TELEMETRY_DISABLED=1 +RUN npm run build + +# --- Production stage --- +FROM caddy:2-alpine AS production + +COPY --from=build /app/dist /srv +COPY Caddyfile /etc/caddy/Caddyfile + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ + CMD wget -qO- http://127.0.0.1:80/ || exit 1 + +# --- Dev stage --- +FROM node:22-slim AS dev + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +ENV ASTRO_TELEMETRY_DISABLED=1 + +EXPOSE 4321 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d213008 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +include .env +export + +# Compose file selection based on ENVIRONMENT +ifeq ($(ENVIRONMENT),dev) + COMPOSE_FILES := -f docker-compose.yml -f docker-compose.dev.yml +else + COMPOSE_FILES := -f docker-compose.yml +endif + +DC := docker compose $(COMPOSE_FILES) + +.PHONY: help build up down restart logs status clean rebuild dev prod + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' + +build: ## Build container image + $(DC) build + +up: ## Start services (detached) + $(DC) up -d --build + @sleep 2 + $(DC) logs --tail=20 + +down: ## Stop services + $(DC) down + +restart: ## Restart services + $(DC) down + $(DC) up -d --build + @sleep 2 + $(DC) logs --tail=20 + +logs: ## Show service logs + $(DC) logs -f + +status: ## Show running containers + $(DC) ps + +clean: ## Remove containers, images, and volumes + $(DC) down --rmi local -v + +rebuild: ## Full rebuild (no cache) + $(DC) build --no-cache + $(DC) up -d + @sleep 2 + $(DC) logs --tail=20 + +dev: ## Switch to dev mode and start + @sed -i 's/^ENVIRONMENT=.*/ENVIRONMENT=dev/' .env + @echo "Switched to dev mode" + $(MAKE) restart + +prod: ## Switch to prod mode and start + @sed -i 's/^ENVIRONMENT=.*/ENVIRONMENT=prod/' .env + @echo "Switched to prod mode" + $(MAKE) restart diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..c7adec5 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,21 @@ +services: + docs: + build: + target: dev + volumes: + - ./src:/app/src + - ./public:/app/public + - ./astro.config.mjs:/app/astro.config.mjs + environment: + - VITE_HMR_HOST=${DOMAIN} + labels: + caddy: ${DOMAIN} + caddy.reverse_proxy: "{{upstreams 4321}}" + caddy.reverse_proxy.flush_interval: "-1" + caddy.reverse_proxy.transport: http + caddy.reverse_proxy.transport.read_timeout: "0" + caddy.reverse_proxy.transport.write_timeout: "0" + caddy.reverse_proxy.transport.keepalive: 5m + caddy.reverse_proxy.transport.keepalive_idle_conns: "10" + caddy.reverse_proxy.stream_timeout: 24h + caddy.reverse_proxy.stream_close_delay: 5s diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..86e1368 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + docs: + build: + context: . + target: production + dockerfile: Dockerfile + restart: unless-stopped + networks: + - caddy + labels: + caddy: ${DOMAIN} + caddy.reverse_proxy: "{{upstreams 80}}" + +networks: + caddy: + external: true