ServerlessBase Blog
  • Introduction to Docker Build Arguments and Environment Variables

    Learn how to use ARG and ENV instructions in Dockerfiles to build flexible, configurable container images with build-time and runtime variables.

    Introduction to Docker Build Arguments and Environment Variables

    You've probably seen Dockerfiles with variables sprinkled throughout. Maybe you've copied a Dockerfile from a tutorial and tried to change the port number, only to realize the variable was hardcoded elsewhere. Or you've built the same image multiple times with different settings and wished there was a cleaner way to handle configuration.

    Docker provides two instructions for handling variables in images: ARG for build-time configuration and ENV for runtime environment variables. Understanding the difference between them and when to use each will make your Dockerfiles more maintainable and your container images more flexible.

    Build-Time Variables with ARG

    The ARG instruction defines a variable that users can pass at build time. Think of ARG as a parameter you set when you run docker build. The variable exists only during the build process and isn't available inside the running container.

    # syntax=docker/dockerfile:1
     
    ARG VERSION=1.0
    ARG BUILD_DATE
    ARG VCS_REF
     
    FROM alpine:${VERSION}
     
    LABEL version="${VERSION}"
    LABEL build-date="${BUILD_DATE}"
    LABEL vcs-ref="${VCS_REF}"
     
    RUN apk add --no-cache python3

    When you build this image, you can override the default values:

    docker build \
      --build-arg VERSION=2.0 \
      --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
      --build-arg VCS_REF=$(git rev-parse --short HEAD) \
      -t myapp:2.0 .

    The ARG instruction has a few important characteristics:

    • Default values: You can provide a default value that's used if the user doesn't specify the argument
    • Scope: Variables are available in the FROM instruction and all subsequent instructions
    • Lifetime: They disappear after the build completes
    • Visibility: They're not visible inside the running container

    ARG vs ENV: The Key Differences

    The most common confusion is between ARG and ENV. Here's the fundamental difference:

    AspectARGENV
    When availableOnly during buildAvailable at runtime
    Container visibilityNot visibleVisible inside container
    PurposeBuild-time configurationRuntime configuration
    PersistenceLost after buildPersists in container
    # ARG - only during build
    ARG APP_VERSION=1.0
     
    # ENV - available at runtime
    ENV APP_VERSION=1.0

    If you try to use an ARG inside a RUN command, it won't work because the variable isn't available at that point:

    # This won't work - ARG is not available in RUN
    ARG APP_VERSION=1.0
    RUN echo "Version: ${APP_VERSION}"  # Empty string

    You need to declare the ARG before the FROM instruction to use it in FROM, and you need to declare it before the RUN instruction to use it there.

    Runtime Variables with ENV

    The ENV instruction sets environment variables that will be available in the container. These variables persist after the container starts and can be accessed by processes running inside the container.

    # syntax=docker/dockerfile:1
     
    FROM node:18-alpine
     
    # Set environment variables
    ENV NODE_ENV=production
    ENV PORT=3000
    ENV LOG_LEVEL=info
     
    WORKDIR /app
     
    COPY package*.json ./
    RUN npm ci --only=production
     
    COPY . .
     
    EXPOSE ${PORT}
     
    CMD ["node", "server.js"]

    When this container runs, the environment variables are available to all processes:

    docker run myapp:latest
    # Inside container: $NODE_ENV = "production"
    # Inside container: $PORT = "3000"
    # Inside container: $LOG_LEVEL = "info"

    You can also set multiple variables in a single ENV instruction:

    ENV NODE_ENV=production \
        PORT=3000 \
        LOG_LEVEL=info

    ENV in Different Contexts

    ENV variables can be set in different places in your Dockerfile:

    # 1. At the top level - available everywhere
    ENV APP_NAME=myapp
     
    FROM alpine
    RUN echo $APP_NAME
     
    # 2. Inside a stage - only available in that stage
    FROM alpine AS builder
    ENV APP_NAME=builder-app
    RUN echo $APP_NAME
     
    FROM alpine
    RUN echo $APP_NAME  # Empty - not available in this stage

    Combining ARG and ENV

    The most powerful pattern is to use ARG for build-time configuration and ENV to pass those values to the runtime. This gives you the best of both worlds: flexibility during build and configuration at runtime.

    # syntax=docker/dockerfile:1
     
    # Build-time argument with default
    ARG NODE_ENV=development
    ARG PORT=3000
     
    FROM node:18-alpine
     
    # Pass ARG to ENV for runtime availability
    ENV NODE_ENV=${NODE_ENV}
    ENV PORT=${PORT}
     
    WORKDIR /app
     
    COPY package*.json ./
    RUN npm install
     
    COPY . .
     
    EXPOSE ${PORT}
     
    CMD ["node", "server.js"]

    Now you can build different variants of your image:

    # Development build
    docker build \
      --build-arg NODE_ENV=development \
      --build-arg PORT=3000 \
      -t myapp:dev .
     
    # Production build
    docker build \
      --build-arg NODE_ENV=production \
      --build-arg PORT=80 \
      -t myapp:prod .

    Both images will have the correct environment variables set at runtime, but you can override them when running the container:

    docker run -e PORT=8080 myapp:prod

    Practical Example: Multi-Stage Build with Configuration

    Here's a complete example showing how to use ARG and ENV together in a multi-stage build:

    # syntax=docker/dockerfile:1
     
    # Build arguments
    ARG NODE_VERSION=18
    ARG APP_ENV=production
    ARG PORT=3000
     
    # Stage 1: Build the application
    FROM node:${NODE_VERSION}-alpine AS builder
     
    WORKDIR /app
     
    # Copy package files
    COPY package*.json ./
     
    # Install dependencies
    RUN npm ci
     
    # Copy application code
    COPY . .
     
    # Build arguments for the build process
    ARG BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
    ARG VCS_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
     
    # Set environment variables for the build
    ENV NODE_ENV=${APP_ENV}
    ENV BUILD_TIME=${BUILD_TIME}
    ENV VCS_REF=${VCS_REF}
     
    # Build the application
    RUN npm run build
     
    # Stage 2: Production runtime
    FROM node:${NODE_VERSION}-alpine AS runtime
     
    WORKDIR /app
     
    # Copy built application from builder stage
    COPY --from=builder /app/dist ./dist
    COPY --from=builder /app/node_modules ./node_modules
    COPY --from=builder /app/package.json ./
     
    # Runtime environment variables
    ENV NODE_ENV=production
    ENV PORT=${PORT}
    ENV NODE_OPTIONS="--max-old-space-size=512"
     
    # Health check
    HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
      CMD node -e "require('http').get('http://localhost:${PORT}/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
     
    EXPOSE ${PORT}
     
    CMD ["node", "dist/index.js"]

    This Dockerfile demonstrates several best practices:

    1. Build arguments control the Node.js version and environment
    2. Build-time ENV captures metadata like build time and git commit
    3. Runtime ENV sets the actual configuration for the application
    4. Multi-stage build keeps the final image small by only copying what's needed

    Best Practices

    1. Use ARG for Version Pinning

    Pin versions of dependencies and tools using ARG to make your builds reproducible:

    ARG PYTHON_VERSION=3.11
    ARG NGINX_VERSION=1.25
     
    FROM python:${PYTHON_VERSION}-slim
     
    RUN apt-get update && apt-get install -y nginx=${NGINX_VERSION}

    2. Validate ARG Values

    You can validate ARG values in your Dockerfile to catch errors early:

    ARG APP_ENV
    RUN if [ "$APP_ENV" != "development" ] && [ "$APP_ENV" != "production" ]; then \
          echo "Invalid APP_ENV: $APP_ENV"; \
          exit 1; \
        fi

    3. Use .dockerignore

    Don't commit .env files or sensitive configuration to your repository. Use .dockerignore to exclude them:

    .env
    .env.local
    *.key
    *.pem

    4. Document Your Build Arguments

    Add comments to your Dockerfile explaining what arguments are available:

    # Build arguments
    # - APP_ENV: development or production (default: production)
    # - PORT: Application port (default: 3000)
    ARG APP_ENV=production
    ARG PORT=3000

    5. Use BuildKit for Better ARG Support

    Enable BuildKit for improved ARG handling and caching:

    DOCKER_BUILDKIT=1 docker build -t myapp .

    Or add to your Dockerfile:

    # syntax=docker/dockerfile:1

    Common Pitfalls

    Pitfall 1: Using ARG in RUN Commands

    As mentioned earlier, ARG is not available in RUN commands unless declared before them:

    # Wrong - ARG not available here
    ARG VERSION=1.0
    RUN echo $VERSION  # Empty
     
    # Correct - ARG declared before RUN
    ARG VERSION=1.0
    RUN echo $VERSION  # Works

    Pitfall 2: Overriding ENV with ENV

    If you set an ENV variable and then try to override it with another ENV, the second one wins:

    ENV PORT=3000
    ENV PORT=8080  # This overrides the previous line

    Pitfall 3: Hardcoding Values

    Avoid hardcoding values that should be configurable:

    # Wrong - not flexible
    FROM alpine
    RUN apk add python3.11
     
    # Better - use ARG
    ARG PYTHON_VERSION=3.11
    FROM alpine
    RUN apk add python${PYTHON_VERSION}

    Conclusion

    Understanding the difference between ARG and ENV is crucial for writing effective Dockerfiles. Use ARG for build-time configuration that changes between builds, and use ENV for runtime configuration that needs to be available when the container runs.

    The combination of both gives you the flexibility to build different variants of your image while keeping the runtime configuration consistent. This pattern is especially useful in CI/CD pipelines where you might build development, staging, and production images from the same Dockerfile.

    When you're ready to deploy your applications, platforms like ServerlessBase can help you manage these container images and handle the complex orchestration of environments, making it easier to focus on writing good Dockerfiles rather than managing deployment infrastructure.

    Next Steps

    Now that you understand ARG and ENV, you can explore:

    • Docker Compose: How to pass build arguments and environment variables to services
    • Multi-stage builds: Using ARG to control different stages of your build
    • Docker secrets: Securely managing sensitive data at runtime
    • CI/CD integration: Automating build arguments in your deployment pipeline

    Leave comment