Skip to content

Caddy or Nginx Alpine

In Stage 2: Runner of the multi-stage build (FROM caddy:alpine AS runner or FROM nginx:alpine AS runner), Caddy Alpine or Nginx Alpine serves as the lightweight, production web server environment.

While Stage 1 (builder) acts as the compiler, Stage 2 provides the runtime engine that serves the finished website.


  1. Purpose & Footprint: The "Throwaway Builder" Pattern

  2. Minimal Base Image: Uses ultra-lightweight Alpine Linux distributions (caddy:alpine or nginx:alpine) to keep the production container footprint tiny—often under 20MB total.

  3. Separation of Concerns: Astro generates pure static HTML, CSS, and JavaScript files, but static files cannot listen for network traffic or serve themselves. Stage 1 is heavy because it contains Node.js, node_modules, package managers, and source code. Stage 2 strips away Node.js and all build dependencies entirely, leaving only a lean web server.

  1. Artifact Transfer (COPY --from=builder)

Stage 2 acts as the recipient of the compiled assets built in Stage 1:

  • Asset Cleanup (Nginx): In Nginx-based configurations, default static placeholder files are removed first (RUN rm -rf /usr/share/nginx/html/*).
  • Artifact Extraction: The container copies only the compiled static folder out of the builder stage using:
    • For Caddy: COPY --from=builder /app/dist /usr/share/caddy
    • For Nginx: COPY --from=builder /app/dist /usr/share/nginx/html
  • All raw .astro files, Node tools, and unneeded code remain behind in the discarded Stage 1 container.

  1. Runtime Commands (EXPOSE & CMD)

To keep the container alive and responding to web traffic, Stage 2 configures runtime execution parameters:

  • Metadata Exposure (EXPOSE 80): Documents internally that the web server process inside the container is listening on port 80.
  • Foreground Service (CMD):
    • Caddy: CMD ["caddy", "file-server", "--root", "/usr/share/caddy", "--listen", ":80"] launches Caddy as a static file server listening on port 80 in the foreground.
    • Nginx: CMD ["nginx", "-g", "daemon off;"] runs Nginx in the foreground.
  • Keeping the process running in the foreground prevents Docker from instantly exiting once the build instructions finish.

  1. Integration into the Broader Homelab Architecture

In the project's production environment, Stage 2's internal web server operates within a multi-tiered reverse proxy architecture:

  1. Isolated Container: The portfolio container runs Caddy internally on port 80 and attaches to a shared Docker bridge network (frontend) without exposing host ports directly to the internet.
  2. Central Homelab Routing: Incoming web traffic enters via Cloudflare Tunnels to a central host Caddy instance.
  3. Docker DNS Proxy: The central Caddy router proxies requests (reverse_proxy astro_portfolio:80) over Docker DNS directly to port 80 of the Stage 2 container.

Artifact Transfer (COPY --from)

In the multi-stage Docker build architecture of the project, COPY --from=builder acts as the explicit bridge and handoff mechanism between Stage 1 (builder) and Stage 2 (runner).

While Stage 1 uses a heavy Node.js environment (node:20-alpine) to install dependencies and compile .astro templates, Stage 2 (caddy:alpine or nginx:alpine) requires only the final static assets to serve over HTTP. The COPY --from directive makes this separation possible by extracting compiled files across build stages.


  1. Breakdown of the Command Mechanics

In the project's Dockerfiles, artifact transfer is written as:

COPY --from=builder /app/dist /usr/share/caddy
# or for Nginx:
COPY --from=builder /app/dist /usr/share/nginx/html
  • COPY: The standard Docker instruction that copies files from a source to a destination path.
  • --from=builder: The key multi-stage flag that instructs Docker not to look on the local host machine, but rather inside the container filesystem of the earlier stage tagged AS builder.
  • /app/dist: The source path inside the builder stage where Astro outputs its finished, static production assets (.html, .css, .js, and RSS feeds) during npm run build or pnpm run build.
  • /usr/share/caddy (or /usr/share/nginx/html): The destination path inside the Stage 2 web server container, corresponding to Caddy or Nginx’s default root directory for serving files over port 80.

  1. Context & Architectural Role in Stage 2 (runner)

Within the larger context of Stage 2: Runner, COPY --from provides three major architectural advantages:

A. Selective Extraction ("Leaving Heavy Tools Behind")

Building an Astro site requires Node.js, package managers (npm/pnpm), lockfiles, and hundreds of megabytes of node_modules. However, once static files are emitted into /app/dist, none of those build tools are needed to run the website. COPY --from cherry-picks only the finished static bundle out of Stage 1.

B. Dramatic Footprint Reduction & Security Hardening

By copying only /app/dist into a bare web server image like caddy:alpine or nginx:alpine, the entire Stage 1 container—including Node.js, source code, and development dependencies—is thrown away.

  • Container Size: Reduces the final production image footprint down to an ultra-lightweight size (often under 20MB).
  • Security: Eliminates Node.js runtime vulnerabilities or unneeded shell tools from the live production container.

C. Handoff to a High-Performance Static Web Server

Because raw HTML/CSS files sitting in a folder cannot listen on network ports or serve themselves over HTTP, COPY --from delivers those assets directly into the web server's serving root. This allows Caddy or Nginx to immediately pick up the static assets and serve them to incoming web traffic over port 80.

EXPOSE 80

In Stage 2: Runner of the multi-stage build, EXPOSE 80 functions as an architectural declaration stating that the web server process inside the container (Caddy or Nginx) listens for HTTP traffic on port 80.

While it is a standard line in the Stage 2 Dockerfile, its actual behavior within Docker and the broader homelab infrastructure involves key technical distinctions:


  1. Metadata & Documentation vs. Active Port Publishing

A common misunderstanding in Docker containerization is assuming EXPOSE 80 opens or forwards ports to the outside world automatically.

  • What it DOES do: It acts as metadata and internal documentation between the developer writing the Dockerfile and orchestration tools, reverse proxies, or cloud providers. It announces that an internal service is listening on port 80.
  • What it DOES NOT do: It does not automatically publish or bind port 80 to the host computer's interface or the internet.
  • The "Sign vs. Doorman" Analogy: The sources describe EXPOSE 80 as putting a sign on a building stating "The front door is at number 80". However, you still need an active web server (Nginx or Caddy) acting as the doorman standing at that door, along with explicit network routing to let traffic inside.

  1. Alignment with the Stage 2 Server Command (CMD)

Inside Stage 2: Runner, EXPOSE 80 directly mirrors the runtime startup command configured for the web server:

  • For Caddy: CMD ["caddy", "file-server", "--root", "/usr/share/caddy", "--listen", ":80"] explicitly binds the static file server process to listen on :80, perfectly matching EXPOSE 80.
  • For Nginx: CMD ["nginx", "-g", "daemon off;"] starts Nginx, which defaults to listening on port 80 internally.

  1. Usage in Local Testing vs. Production Homelab Architecture

The way EXPOSE 80 is consumed differs significantly between local development and production deployment:

A. Local Machine Testing (-p Port Mapping)

When testing the container locally on a laptop, EXPOSE 80 must be manually bridged using the -p flag:

docker run --rm -p 8080:80 ghcr.io/ritikkumar27/portfolio:latest

Here, -p 8080:80 explicitly instructs the host machine to take traffic coming into http://localhost:8080 and forward it directly to the container's internal port 80 declared by EXPOSE 80.

B. Production Homelab Deployment (Zero Host-Port Exposure)

In the production homelab setup, host ports are intentionally not exposed directly (ports: is omitted from the portfolio's docker-compose.yml) to enhance security:

  1. Bridge Isolation: The portfolio container attaches directly to an external Docker bridge network named frontend.
  2. Docker DNS & Central Routing: A central host Caddy reverse proxy sits on the same frontend network.
  3. Internal Proxying: When a request hits the domain, central Caddy uses Docker's internal DNS to route traffic directly to the container's EXPOSE 80 port over the bridge network:

Because EXPOSE 80 establishes port 80 as the internal interface, central Caddy can route traffic directly to astro_portfolio:80 without requiring any open ports on the host server itself.

Caddy File Server CMD

In Stage 2: Runner of the multi-stage Docker build (FROM caddy:alpine AS runner), the instruction CMD ["caddy", "file-server", "--root", "/usr/share/caddy", "--listen", ":80"] defines the primary runtime process that keeps the container active and serves the compiled Astro portfolio to the web.


  1. Breakdown of Command Arguments

Every flag in this command configures a specific runtime behavior for Caddy:

  • "caddy": Launches the Caddy web server application.
  • "file-server": Instructs Caddy to operate specifically as a static file server rather than as a reverse proxy or dynamic application server. Its sole job is to fetch static files from disk and hand them to web browsers over HTTP.
  • "--root", "/usr/share/caddy": Defines the target directory where Caddy looks for web assets. This matches the exact destination path where Stage 1's compiled static output was transferred during the COPY --from=builder /app/dist /usr/share/caddy step.
  • "--listen", ":80": Forces Caddy to listen for incoming web requests on port 80 (standard HTTP) inside the container, directly aligning with the EXPOSE 80 declaration.

  1. Role in the Stage 2 Container Lifecycle

In the larger context of Stage 2: Runner, this command fulfils two critical operational functions:

  • Keeping the Container Alive: A Docker container stays running only as long as its foreground process (CMD) remains active. Because Astro strips away its internal development server during npm run build, the site consists purely of unexecutable HTML/CSS/JS files. Running Caddy in the foreground provides an active daemon process; without it, the container would exit immediately upon starting.
  • Zero-Node Production Environment: By running Caddy as a standalone static file server, Stage 2 serves the compiled assets directly from disk without requiring Node.js, node_modules, or build scripts in the live environment.

  1. Integration into the Homelab Reverse Proxy Architecture

Inside the broader production homelab setup, this CMD provides the internal endpoint for traffic routing:

  1. Docker Bridge Isolation: The container boots up running Caddy on port 80 and attaches to a shared frontend Docker bridge network without exposing host ports directly to the internet.
  2. Central Reverse Proxy Routing: A central host Caddy instance receives incoming traffic for the domain (e.g., http://ritikkumar.dev).
  3. Docker DNS Forwarding: Central Caddy delegates requests using internal Docker DNS (reverse_proxy astro_portfolio:80). The internal Caddy process executed by this CMD receives the forwarded traffic on port 80, grabs the static files from /usr/share/caddy, and sends the HTML response back.