Skip to content

Docker deployment

Deploying Privatefolio via Docker requires us to build a Docker image that packages both @privatefolio/backend and @privatefolio/frontend alongside their dependencies.

Are you looking to self-host Privatefolio using the official Docker image? Go to Self-hosting.

Prerequisites

Before we get started you must have Docker installed:

docker --version
# Docker version 28.3.2, build 578ccf6

If you don't have it, get it from docker.com.

Building the image

yarn docker:build

Running the container

yarn docker:run # start a container named privatefolio
yarn docker:remove # remove the container

Building the image manually

To build the image without using the npm scripts:

docker build -t privatefolio -f packages/backend/Dockerfile .

To run the container:

docker run -d -p ${PORT:-5555}:${PORT:-5555} -v privatefolio-data:/app/data --name privatefolio privatefolio

Accessing the app

The app will be available on the port you set in docker run (defaulting to 5555):

Visit http://localhost:5555 in your browser.

Dockerfile

The Docker image uses a multi-stage build process. It builds the frontend bundle and installs backend production dependencies. The final image utilizes Bun to serve the compiled JavaScript build of the backend and the pre-built frontend bundle.

packages/backend/Dockerfile
# 1) Backend Builder: use Alpine + Yarn (needs Node) to install prod deps
FROM oven/bun:1-alpine AS backend-builder
 
# Install Node.js & Yarn so you can use yarn install
RUN apk add --no-cache nodejs npm \
 && npm install -g yarn
 
WORKDIR /app
COPY package.json yarn.lock lerna.json ./
COPY packages/backend/package.json ./packages/backend/
COPY packages/commons/package.json ./packages/commons/
COPY packages/commons-node/package.json ./packages/commons-node/
 
# Install dependencies using Yarn
RUN yarn install --production --frozen-lockfile
 
# Copy the entire monorepo source code
COPY . .
 
# 2) Frontend Builder: use Alpine + Yarn (needs Node) to bundle the frontend
FROM oven/bun:1-alpine AS frontend-builder
 
# Install Node.js & Yarn so you can use yarn install
RUN apk add --no-cache nodejs npm \
 && npm install -g yarn
 
WORKDIR /app
COPY package.json yarn.lock lerna.json ./
COPY packages/backend/package.json ./packages/backend/
COPY packages/frontend/package.json ./packages/frontend/
COPY packages/commons/package.json ./packages/commons/
COPY packages/commons-node/package.json ./packages/commons-node/
 
# Install dependencies using Yarn
RUN yarn install --frozen-lockfile
 
# Copy the entire monorepo source code
COPY . .
 
# Define build arguments
ARG APP_VERSION_ARG=unknown
ARG GIT_HASH_ARG=unknown
ARG GIT_DATE_ARG=unknown
 
RUN yarn workspace privatefolio-backend build
RUN NODE_ENV=production VITE_APP_VERSION=$APP_VERSION_ARG VITE_GIT_HASH=$GIT_HASH_ARG VITE_GIT_DATE=$GIT_DATE_ARG yarn workspace privatefolio-frontend build:custom
 
# 3) Runtime: only Bun, prod-deps, source code & frontend bundle
FROM oven/bun:1-alpine AS runtime
 
WORKDIR /app
 
# TODO6 image size jumped from 190MB to 240MB because of commons and commons-node (dev deps?)
 
LABEL org.opencontainers.image.title="Privatefolio"
LABEL org.opencontainers.image.description="The AI Wealth Manager - A free* and open-source toolkit for financial empowerment"
LABEL org.opencontainers.image.source="https://github.com/privatefolio/privatefolio"
LABEL org.opencontainers.image.licenses="AGPL-3.0"
 
COPY --from=backend-builder /app/node_modules ./node_modules
COPY --from=backend-builder /app/packages/backend ./packages/backend
COPY --from=backend-builder /app/packages/commons ./packages/commons
COPY --from=backend-builder /app/packages/commons-node ./packages/commons-node
COPY --from=frontend-builder /app/packages/frontend/build ./packages/frontend/build
 
RUN mkdir -p /app/data/{databases,logs,files}
 
WORKDIR /app/packages/backend
 
# Define build arguments
ARG PORT_ARG=5555
ARG APP_VERSION_ARG=unknown
ARG GIT_HASH_ARG=unknown
ARG GIT_DATE_ARG=unknown
 
# Set environment variables using build arguments
ENV PORT=$PORT_ARG \
    NODE_ENV=production \
    DATA_LOCATION=/app/data \
    APP_VERSION=$APP_VERSION_ARG \
    GIT_HASH=$GIT_HASH_ARG \
    GIT_DATE=$GIT_DATE_ARG
 
# Expose the port
EXPOSE $PORT
 
# Set the default command to run when starting the container
CMD ["bun", "run", "src/start.ts"]

Configuration

You can customize the PORT variable at runtime, by passing it to the docker run command with the -e flag:

docker run -d -p 5000:5000 -v privatefolio-data:/app/data -e PORT=5000 --name privatefolio privatefolio

Data Persistence

All data is stored in the /app/data directory inside the container, which is mounted to a persistent volume called privatefolio-data. This ensures that your data is persisted even if the container is stopped or removed.

To backup your data, you can use the Docker volume commands:

docker volume inspect privatefolio-data # View volume info

Logs

To view logs from the container:

docker logs privatefolio

To follow the logs in real-time:

docker logs -f privatefolio

Official deployment

We are currently deploying to GitHub Container Registry (GHCR) through a GitHub Actions workflow. This deployment system provides continuous deployment for all tagged releases and branches, with Discord notifications for deployment status updates.

docker pull ghcr.io/privatefolio/privatefolio:latest

See all versions at ghcr.io/privatefolio/privatefolio.

Available image tags:

  • latest: Latest tagged release
  • v*.*.*: Tagged releases (e.g., v2.0.0-beta.40)
  • ******: Git commit (e.g. 1da1a8f)

To run the official image:

docker run -d -p ${PORT:-5555}:${PORT:-5555} -v privatefolio-data:/app/data --name privatefolio ghcr.io/privatefolio/privatefolio:latest

Learn more on self-hosting with Docker.

Workflow file

.github/workflows/publish-docker-image.yml
name: Publish docker image
 
on:
  push:
    tags: ["*"]
 
  workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
 
permissions:
  contents: read
  packages: write
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://ghcr.io/privatefolio/privatefolio
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x
 
      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          # Create tags for:
          # - latest if it's a push to main
          # - git tag if it's a tagged release
          # - short SHA for any build
          tags: |
            type=ref,event=branch
            type=ref,event=tag
            type=sha,prefix=,format=short
 
      - name: Build image
        run: yarn docker:build
 
      - name: Push all tags
        shell: bash
        run: |
          # stash the (newline- or space-separated) tags into one var
          tags="${{ steps.meta.outputs.tags }}"
          echo "Will push these tags:"
          echo "$tags"
 
          for tag in $tags; do
            echo "Tagging local image as $tag"
            docker tag privatefolio:latest "$tag"
            echo "Pushing $tag"
            docker push "$tag"
          done
 
      - name: Notify Discord on success
        if: success()
        uses: sarisia/actions-status-discord@v1
        with:
          webhook: ${{ secrets.DISCORD_DEPLOYMENTS_WEBHOOK }}
          status: "Success"
          title: "Publish docker image"
          description: |
            **Branch**: `${{ github.ref_name }}`
            **Commit**: `${{ github.sha }}`
          color: 0x00FF00
 
      - name: Notify Discord on failure
        if: failure()
        uses: sarisia/actions-status-discord@v1
        with:
          webhook: ${{ secrets.DISCORD_DEPLOYMENTS_WEBHOOK }}
          status: "Failure"
          title: "Publish docker image"
          description: |
            **Branch**: `${{ github.ref_name }}`
            **Commit**: `${{ github.sha }}`
          color: 0xFF0000