This document defines the standardized patterns and best practices for all Dockerfiles in the Discogsography project.
All Dockerfiles must follow these standards to ensure consistency, security, and maintainability across the project.
The standard template uses a two-stage build (builder + final). Services that require a build-time asset pipeline add a third stage before the builder stage.
The dashboard uses a dedicated Node stage to run the Tailwind v4 CLI and produce a minified stylesheet at image build time, eliminating any CDN dependency at runtime:
# ββ CSS build stage ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
FROM node:24-slim AS css-builder
WORKDIR /build
# Copy only the files the CLI needs
COPY dashboard/tailwind.input.css ./
COPY dashboard/static/index.html ./static/index.html
# Install Tailwind v4 CLI + forms plugin, emit minified stylesheet
# hadolint ignore=DL3016
RUN npm install @tailwindcss/cli@^4 @tailwindcss/forms --save-dev && \
./node_modules/.bin/tailwindcss \
--input tailwind.input.css \
--output tailwind.css \
--minifyThe generated tailwind.css is copied into the final stage:
COPY --from=css-builder --chown=discogsography:discogsography /build/tailwind.css /app/dashboard/static/tailwind.css# syntax=docker/dockerfile:1
# Build arguments
ARG PYTHON_VERSION=3.13
ARG UID=1000
ARG GID=1000
# === BUILDER STAGE ===
FROM python:${PYTHON_VERSION}-slim AS builder
# Install uv
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /bin/uv
# Set environment for build
ENV UV_SYSTEM_PYTHON=1 \
UV_CACHE_DIR=/tmp/.cache/uv \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
# Copy dependency files first for better caching
COPY pyproject.toml uv.lock README.md ./
COPY common/pyproject.toml ./common/
COPY <service>/pyproject.toml ./<service>/
# Install dependencies
RUN --mount=type=cache,target=/tmp/.cache/uv \
uv sync --frozen --no-dev --extra <service>
# Copy source files
COPY common/ ./common/
COPY <service>/ ./<service>/
# === FINAL STAGE ===
FROM python:${PYTHON_VERSION}-slim
# Build arguments for labels
ARG BUILD_DATE
ARG BUILD_VERSION
ARG VCS_REF
ARG UID=1000
ARG GID=1000
# OCI Image Spec Annotations
# [Labels section - see below]
# Install security updates and service-specific packages
# [Package installation section - see below]
# Create user and directories
# [User creation section - see below]
WORKDIR /app
# Copy from builder
COPY --from=builder --chown=discogsography:discogsography /app /app
# Install uv for runtime
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /bin/uv
# Create startup script
# [Startup script section - see below]
# Health check
# [Health check section - see below]
USER discogsography:discogsography
# Environment variables
# [Environment section - see below]
# Expose ports (if applicable)
# [Port exposure section - see below]
# Declare volumes
VOLUME ["/logs"]
# Security comment
# Security: This container should be run with:
# docker run --cap-drop=ALL --security-opt=no-new-privileges:true ...
CMD ["/app/start.sh"]Always define at the top:
ARG PYTHON_VERSION=3.13
ARG UID=1000
ARG GID=1000Standardized format with service-specific variations:
LABEL org.opencontainers.image.title="Discogsography <Service>" \
org.opencontainers.image.description="<Service description>" \
org.opencontainers.image.authors="Robert Wlodarczyk <robert@simplicityguy.com>" \
org.opencontainers.image.url="https://github.com/SimplicityGuy/discogsography" \
org.opencontainers.image.documentation="https://github.com/SimplicityGuy/discogsography/blob/main/README.md" \
org.opencontainers.image.source="https://github.com/SimplicityGuy/discogsography" \
org.opencontainers.image.vendor="SimplicityGuy" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.version="${BUILD_VERSION:-0.1.0}" \
org.opencontainers.image.revision="${VCS_REF}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.base.name="docker.io/library/python:${PYTHON_VERSION}-slim" \
com.discogsography.service="<service>" \
com.discogsography.dependencies="<comma-separated-list>" \
com.discogsography.python.version="${PYTHON_VERSION}"Additional labels for database services:
com.discogsography.database="postgresql"(tableinator)com.discogsography.database="neo4j"(graphinator)
Base template:
# Install security updates and curl for healthcheck
# hadolint ignore=DL3008
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*Service-specific additions:
- tableinator: Add
libpq5for PostgreSQL client libraries
Standard format for all services:
# Create user and directories with configurable UID/GID
RUN groupadd -r -g ${GID} discogsography && \
useradd -r -l -u ${UID} -g discogsography -m -s /bin/bash discogsography && \
mkdir -p /tmp /app /logs && \
chown -R discogsography:discogsography /tmp /app /logsAdditional directories:
- extractor: Add
/discogs-datadirectory
Standard format:
# Create startup script
# hadolint ignore=SC2016
RUN printf '#!/bin/sh\nset -e\nsleep "${STARTUP_DELAY:-0}"\nexec /app/.venv/bin/python -m <service>.<service> "$@"\n' > /app/start.sh && \
chmod +x /app/start.shHTTP-based (default):
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:<port>/health || exit 1Process-based (graphinator only):
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD pgrep -f "python.*graphinator" > /dev/null || exit 1Base environment (all services):
ENV HOME=/home/discogsography \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
UV_SYSTEM_PYTHON=1 \
UV_NO_CACHE=1 \
PATH="/app/.venv/bin:$PATH" \
RABBITMQ_HOST="" \
RABBITMQ_USERNAME="" \
RABBITMQ_PASSWORD=""Service-specific additions:
- dashboard: All database connections
- extractor:
DISCOGS_ROOT="/discogs-data"andPERIODIC_CHECK_DAYS="15" - graphinator: Neo4j connections
- tableinator: PostgreSQL connections
Only expose ports for services with HTTP endpoints or health checks:
- api:
EXPOSE 8004 8005 - dashboard:
EXPOSE 8003 - explore:
EXPOSE 8006 8007 - insights:
EXPOSE 8008 8009 - extractor:
EXPOSE 8000 - graphinator:
EXPOSE 8001 - tableinator:
EXPOSE 8002 - brainztableinator:
EXPOSE 8010 - brainzgraphinator:
EXPOSE 8011
Standard volume:
VOLUME ["/logs"]Additional volumes:
- extractor: Add
"/discogs-data"
- One-shot init container β no health check port, no
restart: unless-stopped - Docker Compose
restart: "no"so it exits after completing - Neo4j and PostgreSQL connection environment variables
- Read-only filesystem with
/tmptmpfs mount cap_drop: ALL(no Linux capabilities needed)
- Three-stage build:
css-builder(Node) βbuilder(Python) β final css-builderstage runs Tailwind v4 CLI to producedashboard/static/tailwind.css- Expose port 8003
- All database connections in environment
- Expose ports 8004 (service) and 8005 (health)
- All database connections in environment
- Rust-based container using multi-stage build
- Create
/discogs-datadirectory - Add
/discogs-datavolume - Special environment variables for Discogs configuration
- Process-based health check (no HTTP endpoint)
- Neo4j connection environment variables
- Install libpq5 for PostgreSQL
- PostgreSQL connection environment variables
- Expose ports 8008 (service) and 8009 (health)
- Fetches data from API internal endpoints over HTTP
- Uses Redis for caching
API_BASE_URLandREDIS_HOSTenvironment variables
- MusicBrainz data enrichment into Neo4j
- Neo4j and RabbitMQ connection environment variables
- Health check on port 8011
- MusicBrainz data into PostgreSQL
musicbrainzschema - Install libpq5 for PostgreSQL
- PostgreSQL and RabbitMQ connection environment variables
- Health check on port 8010
- Exposes knowledge graph to AI assistants via API (no direct DB access)
API_BASE_URLenvironment variable
Before committing any Dockerfile:
- Structure: Follows the standard template order
- Comments: Includes all standard comments and hadolint ignores
- Labels: All OCI labels present with correct values
- Security: Security comment present at bottom
- Health Check: Appropriate health check for service type
- Environment: All required environment variables defined
- Volumes: /logs volume declared (plus service-specific)
- User: Runs as discogsography user
- Caching: Uses BuildKit cache mounts
- Linting: Passes hadolint validation
- Non-root execution: All containers run as UID/GID 1000
- Minimal packages: Only install what's needed
- Security updates: Always run
apt-get upgrade - Clean up: Remove apt lists after installation
- Capability dropping: Document in security comment
- Read-only root: Can be enabled with tmpfs mounts
When updating Dockerfiles:
- Update this document if adding new patterns
- Apply changes consistently across all services
- Test builds for all services
- Update docker-compose.yml if needed
- Verify health checks still function