Skip to main content

Dockerfile Guide

Learn how to create optimized Dockerfiles for your applications on the Strongly platform.

Basic Requirements

Every application must include a Dockerfile in the project root. The platform builds your container image inside an isolated Kubernetes build Job and pushes it to Amazon ECR (Elastic Container Registry).

Minimal Dockerfile

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Bind to the PORT environment variable

App containers run as a non-root user, and the platform sets the PORT environment variable to the container's internal listen port at deploy time. Have your application bind to process.env.PORT (or the equivalent in your language) rather than a hard-coded port. The port declared in your manifest becomes the external Service port and is mapped to the internal port automatically.

Build System

Every build runs in its own isolated build environment on the platform:

  • Isolation: Each build gets dedicated CPU, memory, and disk, separate from other builds and apps
  • Parallelism: Multiple builds can run at the same time
  • Timeout: Builds are limited to 20 minutes by default

Built images are stored in the platform's private container registry, and each new build gets an automatically incremented version number (for example 1.0.3) that you can see on the app's Versions tab.

No cross-build cache

Each build starts fresh, so Docker layer caching does not carry over between builds. Keep your Dockerfile lean (see best practices below) to keep builds fast.

Custom Base Images

Environments configured on the platform can define a custom base image or a custom Dockerfile. When your app is deployed with such an environment, the environment's Dockerfile override is applied to the build.

Dockerfile Best Practices

1. Use Official Base Images

Use official images from Docker Hub for reliability and security:

# Good: Official images
FROM node:18-alpine
FROM python:3.11-slim
FROM nginx:1.25-alpine

# Avoid: Unverified or outdated images
FROM random/node
FROM ubuntu:latest

2. Use Specific Tags

Always use specific version tags, never latest:

# Good: Specific versions
FROM node:18.17-alpine3.18
FROM python:3.11.5-slim-bookworm

# Bad: Latest tag
FROM node:latest
FROM python

3. Minimize Layer Count

Combine RUN commands to reduce image size:

# Good: Combined commands
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git \
python3 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Bad: Multiple RUN commands
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get install -y python3

4. Leverage Build Cache

Order commands from least to most frequently changing:

# Good: Dependencies first, code last
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

# Bad: Code first invalidates cache
WORKDIR /app
COPY . .
RUN npm ci --only=production

5. Use .dockerignore

Create a .dockerignore file to exclude unnecessary files:

node_modules
npm-debug.log
.git
.env
.DS_Store
*.md
coverage
.vscode

Application-Specific Examples

Node.js Application

FROM node:18-alpine AS builder

WORKDIR /app

# Copy dependency files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application code
COPY . .

# Build if needed (TypeScript, etc.)
RUN npm run build

# Production stage
FROM node:18-alpine

WORKDIR /app

# Copy from builder
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001

USER nodejs

EXPOSE 3000

CMD ["node", "dist/server.js"]

React Application

FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy source code
COPY . .

# Build arguments from manifest
ARG REACT_APP_API_URL
ARG REACT_APP_VERSION
ENV REACT_APP_API_URL=${REACT_APP_API_URL}
ENV REACT_APP_VERSION=${REACT_APP_VERSION}

# Build React app
RUN npm run build

# Production stage with nginx
FROM nginx:1.25-alpine

# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf

# Copy built app
COPY --from=builder /app/build /usr/share/nginx/html

# Copy config template for runtime injection
COPY --from=builder /app/build/config.js /usr/share/nginx/html/config.js

EXPOSE 3000

CMD ["nginx", "-g", "daemon off;"]

nginx.conf for React:

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

server {
listen 3000;
root /usr/share/nginx/html;
index index.html;

# Enable gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

# SPA routing - fallback to index.html
location / {
try_files $uri $uri/ /index.html;
}

# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}

Python Flask Application

FROM python:3.11-slim

WORKDIR /app

# Install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
postgresql-client && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Copy requirements
COPY requirements.txt .

# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Create non-root user
RUN useradd -m -u 1001 appuser && \
chown -R appuser:appuser /app

USER appuser

EXPOSE 5000

# Use gunicorn for production
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]

Static Site

FROM nginx:1.25-alpine

# Copy static files
COPY . /usr/share/nginx/html

# Copy custom nginx config if needed
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Multi-Stage Builds

Use multi-stage builds to minimize final image size:

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
CMD ["node", "dist/server.js"]

Benefits:

  • Smaller final image (no build tools)
  • Faster deployment
  • More secure (fewer packages)

Using Build Arguments

Reference manifest build-time variables:

# Declare build arguments
ARG NODE_ENV
ARG REACT_APP_VERSION
ARG REACT_APP_API_URL

# Platform-injected build args (always available)
ARG STRONGLY_APP_ID
ARG STRONGLY_HOST
ARG STRONGLY_URL
ARG STRONGLY_CPU
ARG STRONGLY_MEMORY
ARG STRONGLY_DISK

# Set as environment variables
ENV NODE_ENV=${NODE_ENV}
ENV REACT_APP_VERSION=${REACT_APP_VERSION}
ENV REACT_APP_API_URL=${REACT_APP_API_URL}

# Use in build commands
RUN echo "Building version ${REACT_APP_VERSION}"
RUN npm run build

Corresponding manifest:

env:
- name: NODE_ENV
value: "production"
buildtime: true
- name: REACT_APP_VERSION
value: "1.0.0"
buildtime: true
- name: REACT_APP_API_URL
value: "https://api.example.com"
buildtime: true

Security Best Practices

Run as Non-Root User

# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup

# Change ownership
RUN chown -R appuser:appgroup /app

# Switch to non-root
USER appuser

Minimize Attack Surface

# Use minimal base images
FROM node:18-alpine # Instead of node:18

# Remove unnecessary packages
RUN apt-get remove -y build-essential && \
apt-get autoremove -y && \
apt-get clean

Performance Optimization

Optimize Layer Caching

# Copy package files first (changes less frequently)
COPY package*.json ./
RUN npm ci

# Copy code last (changes most frequently)
COPY . .

Use .dockerignore

Exclude unnecessary files to speed up builds:

node_modules
.git
.env
*.log
coverage
.vscode
.idea

Minimize Image Size

# Use alpine base images
FROM node:18-alpine # ~50MB vs node:18 (~900MB)

# Clean up after installing
RUN apt-get update && \
apt-get install -y package && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Remove dev dependencies
RUN npm ci --only=production

Health Check Implementation

Implement health check endpoint for Kubernetes probes:

Express.js

app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString()
});
});

Flask

@app.route('/health')
def health():
return {
'status': 'ok',
'timestamp': datetime.now().isoformat()
}, 200

Nginx (Static Sites)

Add to nginx.conf:

location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}

Common Issues

Build Timeout

Problem: Build exceeds 20-minute limit

Solutions:

  • Use multi-stage builds
  • Minimize dependencies
  • Use .dockerignore to shrink the build context
  • Note: each build starts fresh with no layer cache carried over, so reducing total work per build is what matters

Large Image Size

Problem: Image is too large, slow to deploy

Solutions:

  • Use alpine base images
  • Remove build dependencies in same RUN command
  • Use multi-stage builds
  • Clean package manager cache

Permission Errors

Problem: App can't write files

Solutions:

  • Run as non-root user
  • Set correct file permissions
  • Use proper USER directive
RUN chown -R appuser:appgroup /app
USER appuser

Missing Dependencies

Problem: App crashes due to missing system packages

Solutions:

  • Install required system packages
  • Test locally with Docker
  • Check application logs
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libpq-dev \
gcc && \
apt-get clean

Testing Locally

Test your Dockerfile before deploying:

# Build image
docker build -t my-app:test .

# Run container
docker run -p 3000:3000 my-app:test

# Check health endpoint
curl http://localhost:3000/health

# Inspect image size
docker images my-app:test

Next Steps