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.
# 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 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:latest
Learn more on self-hosting with Docker.
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: 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