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
Section titled “Prerequisites”Before we get started you must have Docker installed:
docker --version# Docker version 28.3.2, build 578ccf6If you don’t have it, get it from docker.com.
Building the image
Section titled “Building the image”yarn docker:buildRunning the container
Section titled “Running the container”yarn docker:run # start a container named privatefolioyarn docker:remove # remove the containerBuilding the image manually
Section titled “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 privatefolioAccessing the app
Section titled “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
Section titled “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.
# 1) Backend Builder: use Alpine + Yarn (needs Node) to install prod depsFROM oven/bun:1-alpine AS backend-builder
# Install Node.js & Yarn so you can use yarn installRUN apk add --no-cache nodejs npm && npm install -g yarn
WORKDIR /appCOPY 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 YarnRUN yarn install --production --frozen-lockfile
# Copy the entire monorepo source codeCOPY . .
# 2) Frontend Builder: use Alpine + Yarn (needs Node) to bundle the frontendFROM oven/bun:1-alpine AS frontend-builder
# Install Node.js & Yarn so you can use yarn installRUN apk add --no-cache nodejs npm && npm install -g yarn
WORKDIR /appCOPY 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 YarnRUN yarn install --frozen-lockfile
# Copy the entire monorepo source codeCOPY . .
# Define build argumentsARG APP_VERSION_ARG=unknownARG GIT_HASH_ARG=unknownARG GIT_DATE_ARG=unknown
RUN yarn workspace privatefolio-backend buildRUN 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 bundleFROM 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_modulesCOPY --from=backend-builder /app/packages/backend ./packages/backendCOPY --from=backend-builder /app/packages/commons ./packages/commonsCOPY --from=backend-builder /app/packages/commons-node ./packages/commons-nodeCOPY --from=frontend-builder /app/packages/frontend/build ./packages/frontend/build
RUN mkdir -p /app/data/{databases,logs,files}
WORKDIR /app/packages/backend
# Define build argumentsARG PORT_ARG=5555ARG APP_VERSION_ARG=unknownARG GIT_HASH_ARG=unknownARG GIT_DATE_ARG=unknown
# Set environment variables using build argumentsENV 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 portEXPOSE $PORT
# Set the default command to run when starting the containerCMD ["bun", "run", "src/start.ts"]Configuration
Section titled “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 privatefolioData Persistence
Section titled “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 infoTo view logs from the container:
docker logs privatefolioTo follow the logs in real-time:
docker logs -f privatefolioOfficial deployment
Section titled “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:latestSee all versions at ghcr.io/privatefolio/privatefolio.
Available image tags:
latest: Latest tagged releasev*.*.*: 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:latestLearn more on self-hosting with Docker.
Workflow file
Section titled “Workflow file”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: 22.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