What you are about to run
Ghostfolio keeps track of stocks, ETFs and cryptocurrencies you record, across as many accounts as you like, in a web app that also works from a phone’s home screen. Self-hosting it means three pieces working together:
- The app (
ghostfolio/ghostfolio), listening on port 3333. It stores nothing itself. - PostgreSQL 15, the only piece with a volume. Every account, transaction and price you collect lives here.
- Redis, a password-protected cache. Lose it and nothing of yours is lost.
One .env file feeds all three. It holds ACCESS_TOKEN_SALT (a random salt for access tokens) and, separately, JWT_SECRET_KEY (a random key for JSON Web Tokens), the database connection (DATABASE_URL, built from POSTGRES_USER, POSTGRES_PASSWORD and POSTGRES_DB) and the cache settings (REDIS_HOST, REDIS_PORT, REDIS_PASSWORD). The project publishes no hardware requirements, so we will not invent any.
The server that fits a private tracker
Your holdings are about as personal as data gets, which is the case for a machine nobody else uses. A dedicated server is a single physical computer that runs your work and no one else’s, with root access and a port nobody meters. Which size you choose is your call; here is how we would think about it.
- A small cartridge server suits Ghostfolio as its one job.
- An everyday single-processor server is the choice if Ghostfolio will share the box with other things you host.
- Console access, meaning the screen plus the power button, comes with rack servers, with blades, and with two cartridge models, m510 and m710x. It rescues you if you shut yourself out with a bad firewall rule. Other cartridges offer power control only.
- Skip the X-Gene ARM cartridge, which cannot run anything newer than Ubuntu 18.04, too old for the packages used here.
A ready server is yours within 30 minutes of approval, and the ready servers page shows what is waiting; a custom build made to your spec needs hours, not minutes. You get one IPv4 address, and IPv6 at no charge. Running the software is your side of the deal. You manage the server; we keep the hardware and the network running.
Getting it running, step by step
Start from Ubuntu 24.04 or newer; the caddy package used later is missing from 22.04. The README’s own route is a git clone, then .env.example copied to .env and completed, then the stack started. Because the image comes ready-made from Docker Hub, you only need the compose file and the .env, so the commands below fetch just those. Two changes are ours, not the project’s: the passwords are random hex, so nothing in the database address needs escaping, and Ghostfolio listens on 127.0.0.1 only.
# As root on Ubuntu 24.04 or later. Packages, then the firewall (IPv6 on, SSH in, enable)
apt update && apt install -y docker.io docker-compose-v2 ufw openssl curl
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
# Our folder layout; .env must sit one level above the compose file
mkdir -p /srv/ghostfolio/docker && cd /srv/ghostfolio
curl -fsSL -o docker/docker-compose.yml \
https://raw.githubusercontent.com/ghostfolio/ghostfolio/main/docker/docker-compose.yml
sed -i 's/- 3333:3333/- 127.0.0.1:3333:3333/' docker/docker-compose.yml
grep -q -- '- 127.0.0.1:3333:3333' docker/docker-compose.yml && echo loopback-ok
# No loopback-ok? Fix the ports: line by hand, then carry on
# Private from the first byte: create the file at 600, then fill it
install -m 600 /dev/null /srv/ghostfolio/.env
printf '%s\n' \
'COMPOSE_PROJECT_NAME=ghostfolio' \
'REDIS_HOST=redis' 'REDIS_PORT=6379' \
"REDIS_PASSWORD=$(openssl rand -hex 24)" \
'POSTGRES_DB=ghostfolio-db' 'POSTGRES_USER=user' \
"POSTGRES_PASSWORD=$(openssl rand -hex 24)" \
"ACCESS_TOKEN_SALT=$(openssl rand -hex 32)" \
"JWT_SECRET_KEY=$(openssl rand -hex 32)" \
'DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?connect_timeout=300' \
>> /srv/ghostfolio/.env
docker compose -f /srv/ghostfolio/docker/docker-compose.yml up -d
docker compose -f /srv/ghostfolio/docker/docker-compose.yml ps
Why the 127.0.0.1 change matters: the official line 3333:3333 publishes the app on every address the server has, IPv4 and IPv6. Docker sends IPv4 traffic for it around ufw entirely, and IPv6 too if Docker manages IPv6 itself; if not, IPv6 reaches it through docker-proxy, which ufw does see. Pinning it to loopback keeps the app private, fully so from Docker Engine 28 onward (moby#45610), so check your version with docker version.
The shell generates the passwords, so they never land in the process list or your history file. Each compose command names its file in full, so you can run it from anywhere.
Make the admin account yours first
Ghostfolio’s README says it plainly: the first user created via Get Started gets the ADMIN role. Two files in its source fill in the rest: createUser in user.service.ts hands out ADMIN whenever no admin exists yet, and property.service.ts keeps sign-up open until an admin closes it. So do it privately, from your own computer, through your own SSH login:
- Open a tunnel:
ssh -N -L 8333:127.0.0.1:3333 you@your-serverand leave it running. The first time, SSH asks you to confirm the server’s host key: check the fingerprint and answer yes. - In your browser, visit
http://localhost:8333and choose Get Started. - Tick the box saying you understand a lost security token means a lost account, then press Continue. That click is what creates your account, and the dialog’s title now carries the ADMIN label.
- Your security token shows up next, and the app warns you will see it only once. Press Copy to clipboard right away and save it in your password manager. Create Account unlocks after the copy; it simply closes the window and signs you in.
- Go to Admin Control, and on the Overview tab switch User Signup off.
With sign-up off, signupUser in user.controller.ts turns new registrations away with a 403. Keep your .env safe too: createAccessToken in user.service.ts stores each token as an HMAC whose key is ACCESS_TOKEN_SALT, and auth.service.ts checks sign-ins the same way, so a different salt leaves your saved token useless. We took the toggle’s place from admin-overview.html and the dialog’s words from user-account-registration-dialog.html. You will not be asked to accept any Terms of Service there: the dialog adds that only with ENABLE_FEATURE_SUBSCRIPTION switched on, and it is off unless you set it (register-page.component.ts, configuration.service.ts).
Where the prices come from
Ghostfolio looks up market data through data providers. Its self-hosting FAQ lists COINGECKO, MANUAL, YAHOO and GHOSTFOLIO as officially supported and treats all others as experimental.
- For CoinGecko, the README lists two optional settings,
API_KEY_COINGECKO_DEMOandAPI_KEY_COINGECKO_PRO. Add yours to.envand recreate the app. - The GHOSTFOLIO provider comes with a Ghostfolio Premium subscription as an API key, which goes in under Admin Control, Settings, Set API key (a location we found in
admin-settings.component.html). - Anything no provider covers can be a custom asset, or MANUAL data with a scraper configuration, as the FAQ describes.
- Currency conversion needs historical exchange rates you gather yourself: in Market Data, choose the currencies filter, pick a pair such as USDEUR, then use Gather Historical Market Data on it.
Your own address, with HTTPS
Ghostfolio ships no proxy setup, so this part is our own choice: Caddy, installed on the server itself, fetching the certificate for you. Do it in this order. First point an A record at your server, plus AAAA if your server has IPv6, and wait until it resolves. Then let Caddy get the certificate. Only after that tell Ghostfolio its new address.
# Run only after the DNS record answers with this server's address
getent ahosts money.example.net
apt install -y caddy
ufw allow 80,443/tcp
cat > /etc/caddy/Caddyfile <<'EOF'
money.example.net {
reverse_proxy 127.0.0.1:3333
}
EOF
systemctl reload caddy
curl -sI https://money.example.net | head -n 1
# Certificate works? Now give Ghostfolio its public address
printf '%s\n' 'ROOT_URL=https://money.example.net' 'TRUST_PROXY=1' >> /srv/ghostfolio/.env
docker compose -f /srv/ghostfolio/docker/docker-compose.yml up -d --force-recreate ghostfolio
Use your own name in place of the example. Caddy runs as a normal program on the server, so ufw would block it on both IPv4 and IPv6 without the ufw allow line. ROOT_URL tells Ghostfolio its public address, and TRUST_PROXY=1 is the README’s value for a single proxy in front. Already run a proxy for several sites? Our Nginx Proxy Manager guide covers that route.
A backup you can actually restore
The self-hosting FAQ recommends regular full database dumps. The dump is half of it: without the .env you would not have the passwords and salt it was made with. This script saves both, plus the compose file.
# /srv/ghostfolio/nightly-backup.sh (our script; runs as root)
set -euo pipefail
umask 077
dest=/var/backups/ghostfolio-db
mkdir -p "$dest" && chmod 700 "$dest"
stamp=$(date +%F)
docker compose -f /srv/ghostfolio/docker/docker-compose.yml exec -T postgres \
sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB"' | gzip > "$dest/ghostfolio-$stamp.sql.gz"
gzip -t "$dest/ghostfolio-$stamp.sql.gz"
cp /srv/ghostfolio/.env "$dest/env-$stamp"
cp /srv/ghostfolio/docker/docker-compose.yml "$dest/compose-$stamp.yml"
find "$dest" -type f -mtime +14 -name '*-20*' -delete
To run it every night, create /etc/cron.d/ghostfolio-nightly containing this single line: 15 3 * * * root umask 077; mkdir -p /var/backups/ghostfolio-db; chmod 700 /var/backups/ghostfolio-db; bash /srv/ghostfolio/nightly-backup.sh > /var/backups/ghostfolio-db/log-$(date +\%F).txt 2>&1. Then copy the folder somewhere off the server that only you can read, because every file in it is secret.
Going back to an earlier backup on this same machine? Our procedure keeps today’s data in place until the restored copy has proved itself:
- Name the dump and prove it is whole:
dump=/var/backups/ghostfolio-db/ghostfolio-2026-10-22.sql.gz; test -s "$dump" && gzip -t "$dump" && echo dump-ok. No dump-ok, no restore. - Stop Ghostfolio itself, leaving the database running:
docker stop ghostfolio. The commands below use the container names from the official compose file and the database name and user from the.envabove. - Move today's database out of the way by renaming it, not deleting it:
docker exec gf-postgres psql --username=user --dbname=postgres --command='ALTER DATABASE "ghostfolio-db" RENAME TO gf_previous'. - See that the renamed copy is there before going on:
docker exec gf-postgres psql --username=user --dbname=postgres --listshould include gf_previous. - Recreate an empty database under the original name, then feed it the dump:
docker exec gf-postgres createdb --username=user --owner=user ghostfolio-db, followed bygunzip -c "$dump" | docker exec -i gf-postgres psql --set=ON_ERROR_STOP=1 --username=user --dbname=ghostfolio-db. - Bring the app back with
docker start ghostfolio. The README says migrations happen before the server comes up, so give it time: checkdocker psuntil ghostfolio reads (healthy), and only then expectcurl -s 127.0.0.1:3333/api/v1/healthto answer OK. Sign in and look over your accounts. - Happy with what you see? Only now remove the old copy:
docker exec gf-postgres dropdb --username=user gf_previous.
Moving to a new server instead? Copy the saved .env into /srv/ghostfolio with install -m 600, and the compose file into its docker folder, before PostgreSQL ever starts there: the database image creates its login only on a first start, from whatever that .env says. Then start only the postgres and redis services with up -d, restore the dump into the new, still-empty database, and start the rest.
Updates, the licence and Ghostfolio Premium
On the latest tag, updating is two commands from the README: docker compose -f /srv/ghostfolio/docker/docker-compose.yml pull, then the same with up -d. According to the README, schema changes are applied automatically as the new container starts, so take a fresh dump first. Prefer to move on your own schedule? Set a fixed version in the compose file’s image line.
Ghostfolio is released under the GNU AGPL v3, a copyleft licence. What follows is how we read it in general; it is not legal advice. Using it unchanged, for yourself, asks nothing of you. Hand copies to someone else, changed or not, and the source has to go with them. Let other people use a changed version over the network, and you must offer them that version’s source.
Ghostfolio Premium is the project’s fully managed cloud version. Its FAQ describes it as Ghostfolio’s open-source code plus some extras, a markets overview and professional market data among them; the revenue pays for hosting, data and development. Tracking a budget rather than investments? See Actual Budget on your own server.
Questions people ask
- Do I have to give Ghostfolio a public domain?
- No. If you are the only user, you can skip the HTTPS section and reach it through the SSH tunnel whenever you want to look. A domain becomes worth it when you want to open it from your phone or share it with family.
- What if my security token goes missing?
- The sign-up screen is clear that a lost token cannot be recovered, and it displays the token a single time. Copy it into your password manager the moment it appears.
- Is there a phone app for a self-hosted install?
- Not in the app stores, according to the self-hosting FAQ. You add the web app to your home screen instead, and the FAQ says that gives the same experience.
- Can I see Ghostfolio before installing it?
- Yes. The project runs a live demo, linked from its README, so you can click through the screens first.
- Does the Redis cache need backing up?
- No. The compose file gives Redis no volume and the README describes it as caching. Your records are all in PostgreSQL, which the dump covers.