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.htmlin the repo root, or adjust theCOPYpath - 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
PORTfrom the environment or default to 3000:app.listen(process.env.PORT || 3000) npm ciinstalls exact versions from the lock file and skips dev dependencies with--production- Replace
server.jswith 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
srcanddistpaths to match yourtsconfig.jsonoutput directory - Dev dependencies (TypeScript, types) stay in the build stage only
- The runtime image contains only production
node_modulesand 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
gunicornin yourrequirements.txt app:appmeans theappobject inapp.py— adjust to match your module and variable name--no-cache-dirkeeps 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-devis needed if you usepsycopg2for PostgreSQL — remove it if you use SQLitecollectstaticgathers static files at build time so they're ready to serve- Replace
myproject.wsgi:applicationwith your project's WSGI path - Include
gunicornin yourrequirements.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=0produces a static binary with no C dependenciesgo mod downloadis cached untilgo.modorgo.sumchanges- Your app should listen on the port matching the
EXPOSEdirective
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-offlinedownloads all dependencies so they're cached independently of source changes-DskipTestsskips tests during the Docker build — run tests in CI instead- Spring Boot defaults to port 8080 — the
EXPOSEtells 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-daemonprevents Gradle from starting a background daemon inside DockerbootJarproduces 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-essentialis 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=placeholdersatisfies Rails during asset precompilation- Set your real
SECRET_KEY_BASEas 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-installadds PHP extensions — adjust for your needs (pdo_pgsql,gd,redis, etc.)- Laravel's
public/directory must be the document root, hence theAPACHE_DOCUMENT_ROOToverride - Apache listens on port 80 by default
composer install --no-devskips 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.rstrick lets Cargo download and compile all dependencies first — this layer is cached untilCargo.tomlorCargo.lockchanges touch src/main.rsforces Cargo to recompile your actual source after copying it in- Replace
myappwith your binary name (the[[bin]]name inCargo.toml, or the package name) - Your app should bind to
0.0.0.0on the port matching theEXPOSEdirective
.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://+:5000tells Kestrel to listen on port 5000- Replace
MyApp.dllwith your project's output DLL name dotnet restoreis cached until*.csprojchanges- 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.