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:
- Run as non-root user (
USER appwith UID 999) - Set proper file ownership (
--chown=app:app) - Listen on
$PORTenvironment variable
Recommended best practices:
- Use Airbase-managed base images for a smoother deployment experience
- Use multi-stage builds to minimize image size
Recommended: Airbase Base Images¶
Recommendation¶
Use Airbase-managed base images for a smoother deployment experience on our platform.
Benefits¶
- Pre-configured: Includes non-root
appuser (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:
Static site (Nginx):
Required: Non-Root User¶
Requirement¶
All containers must run as the app user (non-root).
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:
Required: File Ownership¶
Requirement¶
All files copied into the container must be owned by app:app.
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):
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:
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:
Application:
Recommended: Multi-Stage Builds¶
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:
Good:
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:
Common Issues and Solutions¶
Issue 1: Permission Denied¶
Error:
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:
-
Listen on 0.0.0.0, not 127.0.0.1
-
Read $PORT environment variable
-
Match port in airbase.json
Issue 3: Build Fails - File Not Found¶
Error:
Cause: File excluded by .dockerignore or path incorrect.
Solution:
- Check
.dockerignore- ensure file isn't excluded - Verify file path relative to Dockerfile location
- 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:
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:
Dockerfile Checklist¶
Use this checklist to verify your Dockerfile meets requirements:
- Uses Airbase-managed base image (
gdssingapore/airbase:*) - All
COPYcommands include--chown=app:app -
USER appdirective is present (before CMD/ENTRYPOINT) - Application listens on
$PORTenvironment variable - Application binds to
0.0.0.0(not127.0.0.1) - Port in
airbase.jsonmatches application port - No secrets in Dockerfile (use
.envinstead) - Uses multi-stage build (if applicable)
- Lock files included (package-lock.json, requirements.txt)
-
.dockerignoreconfigured 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:
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:
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¶
Verify File Permissions¶
See Also¶
- Reference: Base Images Catalog - Available base images
- Reference: airbase.json Configuration - Project configuration
- How-To: Build and Deploy Applications - Build workflow
- Tutorial: Getting Started - First deployment
- Examples: Node.js Example - Complete Node.js example
- Examples: Python Example - Complete Python example