# Multi-stage Dockerfile for Actual-sync # Optimized for minimal image size and production deployment # Stage 1: Builder # Use native platform for building to avoid QEMU emulation issues FROM --platform=$BUILDPLATFORM node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS builder # Install build dependencies RUN apk add --no-cache \ python3 \ make \ g++ \ sqlite WORKDIR /app # Copy package files COPY package*.json ./ # Install production dependencies only. devDependencies (jest, puppeteer — which # downloads Chromium) are not needed: tests run in the dedicated CI job, and the # production stage copies this node_modules verbatim, so it must be dev-free. RUN npm ci --omit=dev # Copy source code COPY . . # Stage 2: Production image FROM node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 # Build arguments for dynamic versioning ARG VERSION=unknown ENV VERSION=${VERSION} # Install runtime dependencies, tini, and the tools needed for PUID/PGID support # (su-exec to drop privileges, shadow for usermod/groupmod) RUN apk add --no-cache sqlite tini su-exec shadow # Create non-root user RUN addgroup -g 1001 -S actualuser && \ adduser -u 1001 -S actualuser -G actualuser WORKDIR /app # Copy package files and node_modules from builder COPY package*.json ./ COPY --from=builder /app/node_modules ./node_modules # Copy application files from builder COPY --from=builder /app/src ./src COPY --from=builder /app/config ./config COPY --from=builder /app/scripts ./scripts COPY --from=builder /app/index.js ./ # Bundle the config defaults (example + schema) at a path the user's config # volume mount does NOT shadow, so first-run seeding and schema validation # still work when /app/config is a fresh bind mount. (#96) COPY --from=builder /app/config ./config-defaults # Entrypoint handles PUID/PGID alignment and privilege drop COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh # Create the writable volume dirs and own only those. App code and node_modules # are world-readable as copied, so the runtime user can read them — a recursive # chown over /app (thousands of node_modules files) is needlessly slow. The # entrypoint also re-chowns data/logs to the runtime PUID/PGID at startup. RUN mkdir -p /app/data /app/logs && \ chown -R actualuser:actualuser /app/data /app/logs # OCI metadata labels LABEL org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.title="actual-sync" \ org.opencontainers.image.description="Automated bank synchronization service for Actual Budget with multi-server support" \ org.opencontainers.image.vendor="Actual-sync" \ org.opencontainers.image.licenses="MIT" # NOTE: the container starts as root so the entrypoint can align PUID/PGID and # fix volume ownership; it then drops to the non-root actualuser via su-exec. # Expose health check port EXPOSE 3000 # Health check using the built-in endpoint HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));" # tini for signal handling; entrypoint applies PUID/PGID then drops privileges ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/entrypoint.sh"] # Start the application CMD ["node", "index.js"]