Example Dockerfiles

Complete, copy-paste-ready Dockerfiles for common languages and frameworks. Every example follows the Burrito Dockerfile requirements: uses EXPOSE to declare its port and responds to GET / with a 200.

Static Site (Nginx)

For plain HTML/CSS/JS sites, or any frontend framework that produces static files:

FROM nginx:alpine
COPY . /usr/share/nginx/html
EXPOSE 80
  • Nginx serves on port 80 by default
  • Put your index.html in the repo root, or adjust the COPY path
  • For SPAs (React, Vue, Svelte), build locally and push the dist/ output, or add a build stage

Node.js (Express)

FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
  • Your app should read PORT from the environment or default to 3000: app.listen(process.env.PORT || 3000)
  • npm ci installs exact versions from the lock file and skips dev dependencies with --production
  • Replace server.js with your entry point

Node.js (TypeScript)

Multi-stage build — compile TypeScript in the build stage, run plain JavaScript in production:

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
  • Adjust src and dist paths to match your tsconfig.json output directory
  • Dev dependencies (TypeScript, types) stay in the build stage only
  • The runtime image contains only production node_modules and compiled JS

Python (Flask)

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
  • Include gunicorn in your requirements.txt
  • app:app means the app object in app.py — adjust to match your module and variable name
  • --no-cache-dir keeps the image smaller

Python (Django)

FROM python:3.12-slim
WORKDIR /app

RUN apt-get update && \
    apt-get install -y --no-install-recommends libpq-dev && \
    rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]
  • libpq-dev is needed if you use psycopg2 for PostgreSQL — remove it if you use SQLite
  • collectstatic gathers static files at build time so they're ready to serve
  • Replace myproject.wsgi:application with your project's WSGI path
  • Include gunicorn in your requirements.txt

Go

Multi-stage build — compile a static binary, run in a minimal Alpine image:

FROM golang:1.23-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM alpine:3.20
COPY --from=build /app/server /server
EXPOSE 8080
CMD ["/server"]
  • CGO_ENABLED=0 produces a static binary with no C dependencies
  • go mod download is cached until go.mod or go.sum changes
  • Your app should listen on the port matching the EXPOSE directive

Java (Spring Boot + Maven)

FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY pom.xml .
RUN apk add --no-cache maven && \
    mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B

FROM eclipse-temurin:21-jre-alpine
COPY --from=build /app/target/*.jar /app.jar
EXPOSE 8080
CMD ["java", "-jar", "/app.jar"]
  • mvn dependency:go-offline downloads all dependencies so they're cached independently of source changes
  • -DskipTests skips tests during the Docker build — run tests in CI instead
  • Spring Boot defaults to port 8080 — the EXPOSE tells Burrito to use it
  • The runtime stage uses JRE only (no compiler), keeping the image smaller

Java (Spring Boot + Gradle)

FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
COPY gradlew .
RUN chmod +x gradlew && \
    ./gradlew dependencies --no-daemon
COPY src ./src
RUN ./gradlew bootJar --no-daemon

FROM eclipse-temurin:21-jre-alpine
COPY --from=build /app/build/libs/*.jar /app.jar
EXPOSE 8080
CMD ["java", "-jar", "/app.jar"]
  • --no-daemon prevents Gradle from starting a background daemon inside Docker
  • bootJar produces the executable fat JAR
  • Copy gradle/, gradlew, and build files first to cache dependency resolution

Ruby (Rails + Puma)

FROM ruby:3.3-slim AS build
WORKDIR /app

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential libpq-dev nodejs && \
    rm -rf /var/lib/apt/lists/*

COPY Gemfile Gemfile.lock ./
RUN bundle install --without development test

COPY . .
RUN RAILS_ENV=production SECRET_KEY_BASE=placeholder bundle exec rake assets:precompile

FROM ruby:3.3-slim
WORKDIR /app

RUN apt-get update && \
    apt-get install -y --no-install-recommends libpq5 && \
    rm -rf /var/lib/apt/lists/*

COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /app /app

ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-p", "3000"]
  • build-essential is needed to compile native gem extensions (removed from runtime stage)
  • The runtime stage only includes libpq5 (runtime lib, not the dev headers)
  • SECRET_KEY_BASE=placeholder satisfies Rails during asset precompilation
  • Set your real SECRET_KEY_BASE as an environment variable at runtime

PHP (Laravel + Apache)

FROM php:8.3-apache

RUN docker-php-ext-install pdo pdo_mysql opcache && \
    a2enmod rewrite

ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' \
    /etc/apache2/sites-available/*.conf /etc/apache2/apache2.conf

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader

COPY . .
RUN composer dump-autoload --optimize && \
    chown -R www-data:www-data storage bootstrap/cache

EXPOSE 80
  • docker-php-ext-install adds PHP extensions — adjust for your needs (pdo_pgsql, gd, redis, etc.)
  • Laravel's public/ directory must be the document root, hence the APACHE_DOCUMENT_ROOT override
  • Apache listens on port 80 by default
  • composer install --no-dev skips development dependencies

Rust

Multi-stage build with a dependency caching trick using a dummy main.rs:

FROM rust:1.82-slim AS build
WORKDIR /app

COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo 'fn main() {}' > src/main.rs && \
    cargo build --release && \
    rm -rf src

COPY src ./src
RUN touch src/main.rs && cargo build --release

FROM debian:bookworm-slim
COPY --from=build /app/target/release/myapp /myapp
EXPOSE 8080
CMD ["/myapp"]
  • The dummy main.rs trick lets Cargo download and compile all dependencies first — this layer is cached until Cargo.toml or Cargo.lock changes
  • touch src/main.rs forces Cargo to recompile your actual source after copying it in
  • Replace myapp with your binary name (the [[bin]] name in Cargo.toml, or the package name)
  • Your app should bind to 0.0.0.0 on the port matching the EXPOSE directive

.NET (ASP.NET Core)

FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /out

FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine
WORKDIR /app
COPY --from=build /out .
ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000
CMD ["dotnet", "MyApp.dll"]
  • ASPNETCORE_URLS=http://+:5000 tells Kestrel to listen on port 5000
  • Replace MyApp.dll with your project's output DLL name
  • dotnet restore is cached until *.csproj changes
  • The SDK image (with compiler) is only in the build stage; the runtime image is much smaller

Need a Different Stack?

The universal pattern works for any language:

# 1. Start from an official base image
FROM <language>:<version>
WORKDIR /app

# 2. Copy dependency files and install
COPY <dependency-files> ./
RUN <install-command>

# 3. Copy source code
COPY . .

# 4. Expose your app's port and set the start command
EXPOSE <port>
CMD ["<start-command>"]

The key rules: use EXPOSE to declare your port (defaults to 80 if omitted), respond to GET / with a 200, and put a Dockerfile in the repo root.

See Dockerfile Requirements for details on system packages, layer caching, and .dockerignore.