Skip to content

Rootless / non-root Docker images and split tusd/nginx out of the all-in-one container for Kubernetes PSA compliance #451

Description

@timothee-bn

Is your feature request related to a problem? Please describe.

The current production image (docker/dockerfile/Dockerfile) is an all-in-one container that bundles the Next.js app, PostgreSQL, tusd, and nginx in a single image. This causes two separate problems:

  1. Not compatible with hardened Kubernetes environments. The image is designed to run as root by default, with nginx binding to port 80 and PostgreSQL's initdb/postgres process needing a resolvable UID/GID via /etc/passwd. Clusters that enforce Pod Security Admission in restricted mode (runAsNonRoot: true, arbitrary runAsUser, no privilege escalation) reject the image outright, or require the Helm chart's securityContext to be relaxed, which isn't acceptable in many organizations' security policies.
  2. Unnecessarily large attack surface. Bundling PostgreSQL, tusd, and nginx binaries/packages inside the app image means the app image ships ~300 extra packages (and their CVEs) that have nothing to do with the Next.js application itself, are already available as maintained upstream images, and make the app image slower to rebuild/rescan on every dependency bump.

Describe the solution you'd like

Replace the monolithic image with a slim, single-purpose app image, and let tusd/nginx/PostgreSQL be regular sidecar/external services wired together via Docker Compose (and, in Kubernetes, via the Helm chart):

  • Dockerfile containing only the Node.js/Next.js dashboard app, single-stage, minimal apt footprint, running as a fixed non-root UID:GID (e.g. 1001:1001) by default via USER 1001:1001. No custom entrypoint script at all — the app already runs its own migrations/init on boot (init() in src/utils/init), so there's nothing left for an entrypoint to orchestrate. This also removes the /etc/passwd self-registration hack that only existed to satisfy PostgreSQL's getpwuid() requirement — moot once PostgreSQL isn't embedded.
  • nginx.conf proxying to the app and tusd by service name instead of 127.0.0.1, run via the standard upstream nginx image.
  • compose.yaml (the reference/example compose file) wiring together portabase (the app), tusd (tusproject/tusd, official image), postgres, and nginx — all as independent services/images.
  • No embedded/fallback PostgreSQL and no entrypoint logic to install or manage one locally: if a user doesn't provide DATABASE_URL, that's a configuration error, not something the container should paper over by spinning up its own local PostgreSQL. PostgreSQL is just another compose service like any other.
  • Update the Helm chart to:
    • Set securityContext/podSecurityContext defaults compatible with Pod Security Admission restricted (runAsNonRoot: true, runAsUser/runAsGroup: 1001, allowPrivilegeEscalation: false, readOnlyRootFilesystem where feasible, capabilities.drop: [ALL]) for the portabase and tusd containers.
    • Deploy tusd as its own Deployment/container (using the upstream image) instead of a process inside the app pod, matching the new compose topology. nginx has no Kubernetes equivalent in the chart — routing is handled by an Ingress resource instead.

Describe alternatives you've considered

There isn't really an alternative approach here so much as an industry convergence: this is the standard shape for hardened container images (see Docker Hardened Images and most other minimalist/hardened image initiatives) — small, single-purpose, non-root by default. Following it also means users don't have to rebuild and audit their own non-hardened images just to run Portabase in a restricted environment; they can rely on the upstream image directly.

The only real alternative would be to keep the current all-in-one Dockerfile as-is and try to make it run non-root in place. Given the number of packages and processes it bundles (PostgreSQL, tusd, nginx, the app), that's significantly more complex to get right and only adds maintenance burden rather than removing it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions