Build Scripts
Makefiles, Ant build files, Gradle tasks, and shell scripts for container workflows. You'll hit this when your team needs a single command to build, test, and deploy containers.
#!/bin/bash
# build.sh - hard to discover, no help
docker build -t myapp:latest .
docker run --rm -p 3000:3000 myapp:latest
# Different scripts for different tasks
# No dependency tracking
# No tab completion#!/bin/bash
# build.sh - hard to discover, no help
docker build -t myapp:latest .
docker run --rm -p 3000:3000 myapp:latest
# Different scripts for different tasks
# No dependency tracking
# No tab completion# Makefile - self-documenting
.PHONY: build run test clean
build: ## Build the container image
docker build -t myapp:latest .
run: build ## Run the app locally
docker run --rm -p 3000:3000 myapp:latest
test: build ## Run tests in container
docker run --rm myapp:latest npm test
clean: ## Remove images and volumes
docker compose down -v
docker rmi myapp:latest
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'# Makefile - self-documenting
.PHONY: build run test clean
build: ## Build the container image
docker build -t myapp:latest .
run: build ## Run the app locally
docker run --rm -p 3000:3000 myapp:latest
test: build ## Run tests in container
docker run --rm myapp:latest npm test
clean: ## Remove images and volumes
docker compose down -v
docker rmi myapp:latest
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'Scattered shell scripts are hard to discover and have no built-in dependency tracking. New team members don't know which script to run. There's no way to list available commands or ensure prerequisites are met before running a task.
Makefiles provide discoverable, self-documenting commands with dependency tracking. make run automatically builds first if needed. The help target documents all available commands. Make is pre-installed on most Unix systems and supports tab completion.
<!-- build.xml - manual process -->
<project name="myapp" default="build">
<target name="build">
<javac srcdir="src"
destdir="build/classes"
includeantruntime="false"/>
<jar destfile="build/myapp.jar"
basedir="build/classes"/>
</target>
<!-- Developer must manually
docker build after ant build -->
</project><!-- build.xml - manual process -->
<project name="myapp" default="build">
<target name="build">
<javac srcdir="src"
destdir="build/classes"
includeantruntime="false"/>
<jar destfile="build/myapp.jar"
basedir="build/classes"/>
</target>
<!-- Developer must manually
docker build after ant build -->
</project><!-- build.xml - integrated pipeline -->
<project name="myapp" default="docker-build">
<target name="compile">
<javac srcdir="src"
destdir="build/classes"
includeantruntime="false"/>
</target>
<target name="jar" depends="compile">
<jar destfile="build/myapp.jar"
basedir="build/classes"/>
</target>
<target name="docker-build" depends="jar">
<exec executable="docker" failonerror="true">
<arg line="build -t myapp:latest ."/>
</exec>
</target>
<target name="docker-push" depends="docker-build">
<exec executable="docker" failonerror="true">
<arg line="push registry.io/myapp:latest"/>
</exec>
</target>
</project><!-- build.xml - integrated pipeline -->
<project name="myapp" default="docker-build">
<target name="compile">
<javac srcdir="src"
destdir="build/classes"
includeantruntime="false"/>
</target>
<target name="jar" depends="compile">
<jar destfile="build/myapp.jar"
basedir="build/classes"/>
</target>
<target name="docker-build" depends="jar">
<exec executable="docker" failonerror="true">
<arg line="build -t myapp:latest ."/>
</exec>
</target>
<target name="docker-push" depends="docker-build">
<exec executable="docker" failonerror="true">
<arg line="push registry.io/myapp:latest"/>
</exec>
</target>
</project>Separating the Ant build from the Docker build means developers must remember to run both in the right order. Forgetting to rebuild the JAR before docker build results in deploying stale code. There's no single command for the full pipeline.
Integrating Docker commands into Ant's dependency chain ensures the JAR is always built before the image. depends enforces the order: compile, then JAR, then Docker build. failonerror="true" stops the pipeline if any step fails.
# README.md says:
# 1. Install Node 20
# 2. Install PostgreSQL 16
# 3. Create database "myapp"
# 4. Copy .env.example to .env
# 5. Run npm install
# 6. Run npm run migrate
# 7. Run npm run dev
# Good luck!# README.md says:
# 1. Install Node 20
# 2. Install PostgreSQL 16
# 3. Create database "myapp"
# 4. Copy .env.example to .env
# 5. Run npm install
# 6. Run npm run migrate
# 7. Run npm run dev
# Good luck!# Makefile
.PHONY: dev setup test
setup: ## First-time setup
cp -n .env.example .env 2>/dev/null || true
docker compose build
dev: ## Start development environment
docker compose up
test: ## Run tests
docker compose run --rm app npm test
# docker-compose.yml handles:
# - Node.js version
# - PostgreSQL setup
# - Database creation
# - Hot reloading# Makefile
.PHONY: dev setup test
setup: ## First-time setup
cp -n .env.example .env 2>/dev/null || true
docker compose build
dev: ## Start development environment
docker compose up
test: ## Run tests
docker compose run --rm app npm test
# docker-compose.yml handles:
# - Node.js version
# - PostgreSQL setup
# - Database creation
# - Hot reloadingManual setup instructions are error-prone, platform-specific, and quickly become outdated. Different developers end up with different versions of Node, PostgreSQL, and other tools. 'Works on my machine' becomes the default state.
Docker Compose encapsulates the entire development environment. New developers run make setup && make dev instead of following a multi-step guide. Everyone gets the same versions, same database, same configuration. The setup is reproducible and version-controlled.
# Dockerfile for Java app
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./gradlew build
FROM eclipse-temurin:21-jre
COPY --from=build /app/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
# Needs Docker daemon
# Rebuilds entire fat JAR layer
# No layer optimization# Dockerfile for Java app
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./gradlew build
FROM eclipse-temurin:21-jre
COPY --from=build /app/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
# Needs Docker daemon
# Rebuilds entire fat JAR layer
# No layer optimization// build.gradle.kts
plugins {
id("com.google.cloud.tools.jib") version "3.4.4"
}
jib {
from { image = "eclipse-temurin:21-jre" }
to { image = "registry.io/myapp" }
container {
jvmFlags = listOf("-Xms256m", "-Xmx512m")
ports = listOf("8080")
mainClass = "com.example.App"
}
}
// Build: ./gradlew jib
// No Docker daemon required
// Optimized layer caching// build.gradle.kts
plugins {
id("com.google.cloud.tools.jib") version "3.4.4"
}
jib {
from { image = "eclipse-temurin:21-jre" }
to { image = "registry.io/myapp" }
container {
jvmFlags = listOf("-Xms256m", "-Xmx512m")
ports = listOf("8080")
mainClass = "com.example.App"
}
}
// Build: ./gradlew jib
// No Docker daemon required
// Optimized layer cachingA traditional Dockerfile packages everything into a fat JAR in a single layer. Any code change rebuilds the entire layer, including unchanged dependencies (which are often 100+ MB). It also requires a running Docker daemon in CI.
Jib builds optimized container images directly from your build tool without a Dockerfile or Docker daemon. It separates dependencies, resources, and classes into distinct layers, so code changes only rebuild the thin classes layer. Builds are faster and reproducible.