How to Deploy Ghostfolio on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host Ghostfolio, the open-source wealth tracker, with its official Docker Compose stack (app, PostgreSQL and Redis), then close signups after you create the admin.
- A VPS with 1 vCPU / 2 GB RAM
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain you can point at the server
- Docker Engine + Compose installed (see the base guide below)
What Ghostfolio is
Ghostfolio is an open-source wealth management app for investors. It tracks stocks, ETFs and cryptocurrencies across brokers and exchanges in one dashboard, and reports portfolio performance, allocations and risk. It is a mobile-first web app you can install to your home screen, and it is licensed AGPL-3.0.
Under the hood it is a TypeScript project: a NestJS backend with PostgreSQL (through Prisma) as the database and Redis as a cache, and an Angular frontend. The official Docker Compose file runs all three as containers.
Two things to know before you install:
- Ghostfolio tracks investments, not spending. It records buys, sells, dividends, fees and similar activities. For budgets, pair it with Actual Budget or Firefly III. Firefly III vs Ghostfolio explains the split.
- Market data comes from outside providers. A self-hosted instance works with the free providers Ghostfolio supports. Ghostfolio's own professional data provider needs a Premium subscription's API key.
Server sizing
Upstream publishes no RAM figure. The catalog uses 1 GB as its estimate for the three containers. On our test box (GCP e2-standard-2, Ubuntu 26.04, Docker 29.8.1), the idle stack measured about 465 MB of RAM and about 2 GB of disk for the images and database.
- 2 GB RAM is the comfortable size. It leaves headroom for PostgreSQL to grow and for a reverse proxy on the same box.
- Disk: 20 GB is plenty. The database holds your transactions and cached market data, which is small next to the images.
Official images exist for linux/amd64, linux/arm64 and linux/arm/v7, so
an ARM VPS works too.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed, along
with a non-root user and a ufw firewall. If not, start with
Docker & Compose on Ubuntu.
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
Paid link — we earn a commission if you shop through it.
Install Ghostfolio (Docker Compose)
Upstream's route is to clone the repository, copy .env.example to .env, fill
it in, and start docker/docker-compose.yml:
cd ~
git clone --depth 1 https://github.com/ghostfolio/ghostfolio.git
cd ~/ghostfolio
cp .env.example .env
The example file has four placeholders: REDIS_PASSWORD, POSTGRES_PASSWORD,
ACCESS_TOKEN_SALT and JWT_SECRET_KEY. Generate all four. Two optional
settings from the README also belong here once you add a domain:
ROOT_URL, the public URL. Ghostfolio uses it to build callback URLs and external links.TRUST_PROXY=1, which tells the Express server that it sits behind one reverse proxy. Rate limiting then sees the real client IP.
Replace folio.example.com with your hostname:
cd ~/ghostfolio
DOMAIN=folio.example.com
sed -i "s|<INSERT_REDIS_PASSWORD>|$(openssl rand -hex 24)|; s|<INSERT_POSTGRES_PASSWORD>|$(openssl rand -hex 24)|; s|^ACCESS_TOKEN_SALT=.*|ACCESS_TOKEN_SALT=$(openssl rand -hex 32)|; s|^JWT_SECRET_KEY=.*|JWT_SECRET_KEY=$(openssl rand -hex 32)|" .env
grep -q '^ROOT_URL=' .env || printf 'ROOT_URL=https://%s\nTRUST_PROXY=1\n' "$DOMAIN" >> .env
grep -c 'INSERT_' .env || true
The last command should print 0, which means no placeholder is left.
Generate the Postgres password before the first start. DATABASE_URL is built
from it, and PostgreSQL keeps whatever password it was initialised with.
The compose file publishes port 3333 on every interface. Bind it to loopback
so the reverse proxy is the only way in:
cd ~/ghostfolio
sed -i 's|- 3333:3333|- 127.0.0.1:3333:3333|' docker/docker-compose.yml
grep -n '3333:3333' docker/docker-compose.yml
Start the stack. The app waits for PostgreSQL and Redis to report healthy, applies database migrations, and then answers on its health endpoint:
cd ~/ghostfolio
docker compose -f docker/docker-compose.yml up -d
timeout 600 bash -c 'until curl -fsS http://127.0.0.1:3333/api/v1/health; do sleep 5; done'
echo
docker compose -f docker/docker-compose.yml ps
HTTPS + domain
Point an A record for folio.example.com at the server, wait for it to
resolve, then put Caddy in front of the loopback port. The setup is in
Automatic HTTPS with Caddy:
folio.example.com {
reverse_proxy 127.0.0.1:3333
}
If Caddy runs as a container, 127.0.0.1 is its own loopback. Attach it to
the ghostfolio compose network and proxy to ghostfolio:3333 instead.
First login, then close signups
Open https://folio.example.com and choose Get Started. Upstream's setup
note says the first user created gets the ADMIN role.
By default, Ghostfolio logs you in with a Security Token rather than a
password (ENABLE_FEATURE_AUTH_TOKEN defaults to true). The token is shown
when the account is created. Store it in your password manager, because it is
your login. You can generate a new one later from the account access panel.
User signup is on by default. Anyone who can reach the URL can create an account on your server. Once your admin account exists, open the Admin Control panel and switch off User Signup. Check the result in a private window: the Get Started flow should no longer create accounts.
For a team, the README documents experimental OpenID Connect login
(ENABLE_FEATURE_AUTH_OIDC plus the OIDC_* variables). It pairs with a
self-hosted identity provider such as Authentik.
Securing it
- Keep port 3333 on loopback. The sed step above does this, so traffic reaches the app only through Caddy and its certificate.
- Never commit or share
.env. It holds the database and Redis passwords and the two signing secrets. RotatingJWT_SECRET_KEYlogs everyone out. - The
cap_drop: ALLandno-new-privilegessettings in upstream's compose file are deliberate hardening. Leave them in place.
Backups
Everything that matters is in PostgreSQL and in .env. Dump the database with
the tools inside the Postgres container, which reads its own credentials from
the environment:
cd ~/ghostfolio
mkdir -p ~/backups/ghostfolio
docker compose -f docker/docker-compose.yml exec -T postgres \
sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' | gzip > ~/backups/ghostfolio/ghostfolio-$(date +%F).sql.gz
cp .env ~/backups/ghostfolio/env-$(date +%F)
ls -lh ~/backups/ghostfolio
Copy that folder off the box. Ghostfolio also has an in-app export of your activities to a JSON file, and the matching import. That makes a useful second, provider-independent copy of your transactions, but it does not include users or settings.
Upgrades
Upstream's procedure for the latest tag is to pull the image and start it
again. The container applies any schema migrations on startup:
cd ~/ghostfolio
docker compose -f docker/docker-compose.yml pull
docker compose -f docker/docker-compose.yml up -d
Take a pg_dump first, because migrations are one-way. If you would rather
choose when to move, replace latest in docker/docker-compose.yml with a
specific version tag and raise it on your own schedule, which is the other
route upstream describes.
Troubleshooting
docker compose up fails with "REDIS_PASSWORD variable is not set". The
Redis service refuses to start without a password. Run the sed step above and
check that grep -c INSERT_ .env prints 0.
The app never becomes healthy. Check docker compose -f docker/docker-compose.yml logs ghostfolio. The usual cause is a DATABASE_URL
that no longer matches the password PostgreSQL was initialised with, which
happens when .env is edited after the first start. Put the original password
back, or recreate the database volume if nothing is in it yet.
Prices or holdings show no market data. Your data provider has no data for
that symbol, or the request timed out. REQUEST_TIMEOUT defaults to 2000 ms,
which is short on a slow link. The Premium-only provider is not available on a
self-hosted instance without its API key.
Strangers are creating accounts. User Signup is still on. Turn it off in the Admin Control panel and delete the accounts you don't recognise.
Verification + next steps
You're done when you can load https://folio.example.com over a valid
certificate and log in with your Security Token. Your account should show
admin access, a private window should show that signup is closed, and you
should have an off-box pg_dump that you have restored at least once.
From there: import your transaction history, add your accounts, and set a benchmark to compare against. If you run budgeting next to it, Deploy Actual Budget on a VPS fits on the same box.