Skip to content

Dockerfile Requirements

Specification for Airbase-compatible Dockerfiles

This reference defines the requirements and best practices for Dockerfiles used with Airbase.


Overview

All applications deployed to Airbase must follow these platform constraints:

  1. Run as non-root user (USER app with UID 999)
  2. Set proper file ownership (--chown=app:app)
  3. Listen on $PORT environment variable

Recommended best practices:

  1. Use Airbase-managed base images for a smoother deployment experience
  2. Use multi-stage builds to minimize image size

Recommendation

Use Airbase-managed base images for a smoother deployment experience on our platform.

FROM gdssingapore/airbase:<image-tag>

Benefits

  • Pre-configured: Includes non-root app user (UID 999) already set up
  • Security patches: Images are regularly updated with security fixes
  • Government-compliant: Meet Singapore Government security standards
  • Optimized: Pre-configured for Airbase infrastructure
  • Officially supported: Maintained by GovTech

Available Base Images

See Base Images Catalog for complete list.

Common base images:

Image Use Case
gdssingapore/airbase:node-22 Node.js 22 runtime
gdssingapore/airbase:node-22-builder Node.js 22 with build tools
gdssingapore/airbase:python-3.13 Python 3.13 runtime
gdssingapore/airbase:nginx-1.28 Nginx 1.28 for static sites

Examples

Node.js:

FROM gdssingapore/airbase:node-22-builder AS builder
# Build stage...

FROM gdssingapore/airbase:node-22
# Runtime stage...

Python:

FROM gdssingapore/airbase:python-3.13
# Application setup...

Static site (Nginx):

FROM gdssingapore/airbase:nginx-1.28
# Copy static files...

Required: Non-Root User

Requirement

All containers must run as the app user (non-root).

USER app

Why This Matters

Security: Running as root is a security risk. If the container is compromised, an attacker has root privileges.

Compliance: Government security standards require non-root containers.

Placement

The USER app directive must be the last directive before CMD or ENTRYPOINT.

Correct:

FROM gdssingapore/airbase:node-22
WORKDIR /app
COPY --chown=app:app . .
USER app  # ✅ Last directive before CMD
CMD ["node", "server.js"]

Incorrect:

FROM gdssingapore/airbase:node-22
USER app  # ❌ Too early - files copied after this will fail
COPY . .  #  Will fail - app user doesn't have permission
CMD ["node", "server.js"]

Pre-configured User

Airbase base images include a pre-configured app user:

  • UID: 999
  • GID: 999
  • Home directory: /home/app
  • Shell: /bin/bash

You don't need to create this user - it's already in the base image.

If using custom base images: Create an app user with UID 999:

RUN useradd -u 999 -m app
USER app

Required: File Ownership

Requirement

All files copied into the container must be owned by app:app.

COPY --chown=app:app <source> <destination>

Why This Matters

The application runs as the app user. If files are owned by root, the app won't be able to read or write them.

Apply to All COPY Commands

Every COPY instruction must include --chown=app:app.

Correct:

COPY --chown=app:app package.json ./
COPY --chown=app:app src ./src
COPY --chown=app:app public ./public

Incorrect:

COPY package.json ./  #  Missing --chown
COPY src ./src        #  Missing --chown
COPY public ./public  #  Missing --chown

Multi-Stage Builds

In multi-stage builds, apply --chown when copying from builder stage:

FROM gdssingapore/airbase:node-22-builder AS builder
WORKDIR /app
COPY package.json ./
RUN npm install  # Files owned by root in builder (OK)

FROM gdssingapore/airbase:node-22
WORKDIR /app
# ✅ Apply --chown when copying FROM builder
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --chown=app:app . .
USER app
CMD ["node", "server.js"]

Required: PORT Environment Variable

Requirement

Applications must listen on the port specified by the $PORT environment variable.

Why This Matters

Airbase injects $PORT at runtime (typically 3000). Your application must respect this value.

Implementation

Node.js (Express):

const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0', () => {
  console.log(`Server listening on port ${port}`);
});

Python (Flask):

import os
port = int(os.environ.get('PORT', 3000))
app.run(host='0.0.0.0', port=port)

Python (FastAPI):

import os
import uvicorn
port = int(os.environ.get('PORT', 3000))
uvicorn.run(app, host='0.0.0.0', port=port)

Bind to 0.0.0.0

Important: Listen on 0.0.0.0 (all interfaces), not 127.0.0.1 (localhost only).

Correct:

app.listen(port, '0.0.0.0')  // ✅ Accessible from outside container

Incorrect:

app.listen(port, '127.0.0.1')  // ❌ Only accessible within container
app.listen(port, 'localhost')  // ❌ Only accessible within container

Match airbase.json

The port in airbase.json should match the port your app listens on:

airbase.json:

{
  "framework": "container",
  "handle": "team/project",
  "port": 3000
}

Application:

const port = process.env.PORT || 3000;  // Matches airbase.json

Benefits

Multi-stage builds:

  • Reduce image size (exclude build tools from final image)
  • Improve security (fewer packages = smaller attack surface)
  • Faster deployments (smaller images download faster)

Pattern

# Stage 1: Builder
FROM gdssingapore/airbase:node-22-builder AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install

# Stage 2: Runtime
FROM gdssingapore/airbase:node-22
WORKDIR /app
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --chown=app:app . .
USER app
CMD ["node", "server.js"]

Image Size Comparison

Without multi-stage build: - Image size: ~1.2 GB (includes build tools, source files, dependencies)

With multi-stage build: - Image size: ~200 MB (runtime only) - 6x smaller!

When to Use

Use multi-stage builds for: - Node.js applications (with npm install or build step) - Python applications (with pip install) - Applications with compilation step - Static sites (build → serve with Nginx)

Skip for: - Very simple applications with no build step - Already pre-built applications


Complete Examples

Node.js (Express)

# Build stage
FROM gdssingapore/airbase:node-22-builder AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install

# Runtime stage
FROM gdssingapore/airbase:node-22
WORKDIR /app
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --chown=app:app package.json ./
COPY --chown=app:app server.js ./
USER app
CMD ["node", "server.js"]

Node.js (Next.js)

# Build stage
FROM gdssingapore/airbase:node-22-builder AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
RUN npm run build

# Runtime stage
FROM gdssingapore/airbase:node-22
WORKDIR /app
COPY --from=builder --chown=app:app /app/.next/standalone ./
COPY --from=builder --chown=app:app /app/.next/static ./.next/static
COPY --from=builder --chown=app:app /app/public ./public
USER app
CMD ["node", "server.js"]

Python (Flask)

FROM gdssingapore/airbase:python-3.13
ENV PYTHONUNBUFFERED=TRUE
WORKDIR /app
COPY --chown=app:app requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app . .
USER app
CMD ["python", "app.py"]

Python (FastAPI with uvicorn)

FROM gdssingapore/airbase:python-3.13
ENV PYTHONUNBUFFERED=TRUE
WORKDIR /app
COPY --chown=app:app requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app . .
USER app
CMD ["bash", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-3000}"]

Static Site (Vite/React)

# Build stage
FROM gdssingapore/airbase:node-22-builder AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
RUN npm run build

# Runtime stage (Nginx)
FROM gdssingapore/airbase:nginx-1.28
COPY --from=builder --chown=app:app /app/dist /usr/share/nginx/html
USER app
# Nginx starts automatically

Security Requirements

1. No Secrets in Dockerfile

Never include secrets in your Dockerfile.

Bad:

ENV DATABASE_URL=postgresql://user:password@host/db  #  Secret exposed
ENV API_KEY=sk-1234567890abcdef                       #  Secret exposed

Good:

Use .env files (not committed to git):

# .env file (gitignored)
DATABASE_URL=postgresql://user:password@host/db
API_KEY=sk-1234567890abcdef

Airbase CLI reads .env and injects variables at runtime.

2. No Root Processes

All processes must run as app user.

Bad:

CMD ["sudo", "node", "server.js"]  #  Running with sudo

Good:

USER app
CMD ["node", "server.js"]  #  Running as app user

3. No Unnecessary Packages

Only install packages your application needs.

Bad:

RUN apt-get update && apt-get install -y \
    vim \
    curl \
    git \
    build-essential \
    # ... 50 more packages

Good:

# Use builder image for build tools
FROM gdssingapore/airbase:node-22-builder AS builder
# Runtime image has only what's needed
FROM gdssingapore/airbase:node-22

4. Pin Dependencies

Always use lock files to ensure reproducible builds.

Node.js:

COPY package.json package-lock.json ./  #  Include lock file
RUN npm ci  # Use 'ci' for reproducible installs

Python:

COPY requirements.txt ./
RUN pip install -r requirements.txt  # Requirements should pin versions

Common Issues and Solutions

Issue 1: Permission Denied

Error:

Error: EACCES: permission denied, open '/app/data/file.txt'

Cause: Files not owned by app user.

Solution: Add --chown=app:app to all COPY commands.

Issue 2: Application Not Accessible

Error: Application deployed but URL returns connection error.

Cause: Application not listening on correct interface or port.

Solutions:

  1. Listen on 0.0.0.0, not 127.0.0.1

    app.listen(port, '0.0.0.0')  // ✅ Correct
    

  2. Read $PORT environment variable

    const port = process.env.PORT || 3000;
    

  3. Match port in airbase.json

    { "port": 3000 }
    

Issue 3: Build Fails - File Not Found

Error:

COPY failed: file not found in build context

Cause: File excluded by .dockerignore or path incorrect.

Solution:

  1. Check .dockerignore - ensure file isn't excluded
  2. Verify file path relative to Dockerfile location
  3. Use COPY . . to copy all files (if appropriate)

Issue 4: Image Too Large

Cause: Including build tools and dependencies in final image.

Solution: Use multi-stage builds:

FROM gdssingapore/airbase:node-22-builder AS builder
# Install dependencies, build app

FROM gdssingapore/airbase:node-22  # ✅ Smaller runtime image
COPY --from=builder --chown=app:app /app/dist ./dist

Issue 5: Python Package Installation Fails

Error:

error: externally-managed-environment

Cause: Python 3.13+ prevents global pip installs without virtual environment.

Solution: Airbase Python images are pre-configured to allow pip installs. Just use pip install:

RUN pip install -r requirements.txt  #  Works in Airbase images

Dockerfile Checklist

Use this checklist to verify your Dockerfile meets requirements:

  • Uses Airbase-managed base image (gdssingapore/airbase:*)
  • All COPY commands include --chown=app:app
  • USER app directive is present (before CMD/ENTRYPOINT)
  • Application listens on $PORT environment variable
  • Application binds to 0.0.0.0 (not 127.0.0.1)
  • Port in airbase.json matches application port
  • No secrets in Dockerfile (use .env instead)
  • Uses multi-stage build (if applicable)
  • Lock files included (package-lock.json, requirements.txt)
  • .dockerignore configured to exclude unnecessary files

Best Practices

1. Order Layers by Change Frequency

Put less-frequently-changed layers first for better caching:

FROM gdssingapore/airbase:node-22
WORKDIR /app
COPY --chown=app:app package.json ./       # Changes rarely
RUN npm install                            # Changes rarely
COPY --chown=app:app . .                   # Changes frequently
USER app
CMD ["node", "server.js"]

2. Use .dockerignore

Exclude unnecessary files from build context:

# .dockerignore
node_modules
.git
.env
*.md
.vscode
.DS_Store

3. Minimize Layers

Combine related RUN commands:

Good:

RUN apt-get update && \
    apt-get install -y package1 package2 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Bad:

RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
RUN apt-get clean

4. Clean Up in Same Layer

Remove temporary files in the same RUN command:

RUN npm install && \
    npm cache clean --force  #  Cleanup in same layer

5. Set WORKDIR Early

FROM gdssingapore/airbase:node-22
WORKDIR /app  # ✅ Set once, applies to all subsequent commands
COPY --chown=app:app . .

Testing Your Dockerfile

Local Build Test

# Build locally
docker build -t myapp:test .

# Run locally
docker run -p 3000:3000 -e PORT=3000 myapp:test

# Test
curl http://localhost:3000

Verify Non-Root User

# Check user
docker run myapp:test whoami
# Should output: app

Verify File Permissions

# Check file ownership
docker run myapp:test ls -la /app
# Files should be owned by app:app

See Also