Vikunja is an open-source to-do and project app — lists, Kanban boards, Gantt-style views, and a real CalDAV server behind it — and it runs as one Docker image plus a database. Almost every problem people hit self-hosting it traces back to one of two settings nobody set on purpose: the JWT secret and the public URL.Vikunja ist eine Open-Source-To-do- und Projekt-App — Listen, Kanban-Boards, Gantt-artige Ansichten und dahinter ein echter CalDAV-Server — und läuft als ein Docker-Image plus Datenbank. Fast jedes Problem, auf das Leute beim Self-Hosting stoßen, lässt sich auf eine von zwei Einstellungen zurückführen, die niemand absichtlich gesetzt hat: das JWT-Secret und die öffentliche URL.
Vikunja supports SQLite, MySQL and Postgres. For one person, SQLite is genuinely fine — it is a single file, there is nothing extra to run, and Vikunja's own load on it is light: a handful of API calls per page load, not a stream of writes. The moment more than one person is actively creating and moving tasks at the same time, move to Postgres; concurrent writers are exactly the case SQLite handles worst, and Vikunja documents Postgres as the production-grade option for that reason.Vikunja unterstützt SQLite, MySQL und Postgres. Für eine einzelne Person ist SQLite wirklich in Ordnung — es ist eine einzelne Datei, es läuft nichts extra, und Vikunjas eigene Last darauf ist gering: eine Handvoll API-Aufrufe pro Seitenaufruf, kein Strom von Schreibzugriffen. Sobald mehr als eine Person gleichzeitig aktiv Aufgaben erstellt und verschiebt, wechsle zu Postgres; gleichzeitige Schreibzugriffe sind genau der Fall, mit dem SQLite am schlechtesten zurechtkommt, und Vikunja dokumentiert Postgres deshalb als die produktionstaugliche Option.
Decide this before the first run. Vikunja's database type is a startup-time setting, not something you flip later without a manual export and reimport, so pick the one that matches how many people will actually use the instance rather than defaulting to whichever is less typing today.Entscheide das vor dem ersten Start. Vikunjas Datenbanktyp ist eine Einstellung zum Startzeitpunkt, nichts, was du später ohne manuellen Export und Reimport einfach umschaltest — wähle also die Option, die zur tatsächlichen Anzahl der Nutzer der Instanz passt, statt die zu nehmen, die heute weniger Tipparbeit bedeutet.
This is the Postgres version, because a "small team" is the case worth writing down; drop the db service and swap in VIKUNJA_DATABASE_TYPE=sqlite plus a VIKUNJA_DATABASE_PATH inside the mounted volume if it really is just you.Das hier ist die Postgres-Variante, weil ein „kleines Team“ der Fall ist, den es sich aufzuschreiben lohnt; entfernst du den db-Service und trägst stattdessen VIKUNJA_DATABASE_TYPE=sqlite plus einen VIKUNJA_DATABASE_PATH innerhalb des gemounteten Volumes ein, falls es wirklich nur du bist.
services:
vikunja:
image: vikunja/vikunja:0.24.6 # check hub.docker.com/r/vikunja/vikunja/tags for the current release
restart: unless-stopped
depends_on:
- db
environment:
- VIKUNJA_SERVICE_PUBLICURL=https://tasks.example.com
- VIKUNJA_SERVICE_JWTSECRET=${JWT_SECRET}
- VIKUNJA_SERVICE_ENABLEREGISTRATION=true
- VIKUNJA_DATABASE_TYPE=postgres
- VIKUNJA_DATABASE_HOST=db
- VIKUNJA_DATABASE_USER=vikunja
- VIKUNJA_DATABASE_PASSWORD=${DB_PASSWORD}
- VIKUNJA_DATABASE_DATABASE=vikunja
- VIKUNJA_DATABASE_SSLMODE=disable
volumes:
- vikunja_files:/app/vikunja/files
ports:
- "127.0.0.1:3456:3456"
db:
image: postgres:16 # check hub.docker.com/_/postgres/tags — a newer major (17/18) may now be the better default
restart: unless-stopped
environment:
- POSTGRES_USER=vikunja
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=vikunja
volumes:
- vikunja_db_data:/var/lib/postgresql/data
volumes:
vikunja_files:
vikunja_db_data:
The Vikunja image is the combined one: frontend and API in a single container, listening on 3456, so there is only one thing to reverse-proxy. Bind that port to 127.0.0.1 — only Caddy on the same host should reach it — and generate DB_PASSWORD and JWT_SECRET into a .env file next to the compose file rather than typing real secrets into it:Das Vikunja-Image ist das kombinierte: Frontend und API in einem einzigen Container, der auf 3456 lauscht, sodass es nur ein Ziel für den Reverse-Proxy gibt. Binde diesen Port an 127.0.0.1 — nur Caddy auf demselben Host soll ihn erreichen — und erzeuge DB_PASSWORD und JWT_SECRET in einer .env-Datei neben der Compose-Datei, statt echte Secrets direkt hineinzuschreiben:
printf 'DB_PASSWORD=%s\nJWT_SECRET=%s\n' "$(openssl rand -hex 24)" "$(openssl rand -hex 32)" > .env
chmod 600 .env
run a database on your VPS covers running a database like this one on the same VPS versus a separate box.Eine Datenbank auf deinem VPS betreiben behandelt, ob du eine Datenbank wie diese auf demselben VPS betreibst oder auf einer separaten Maschine.
VIKUNJA_SERVICE_JWTSECRET is not optional in any real sense, even though Vikunja starts fine without it. Leave it unset and Vikunja generates a random one on every container start — which means every login token issued before a restart stops working the moment the container restarts, logging everyone out at once for no visible reason. Set it once, in .env, to a long random string, and it never has to change again unless you deliberately want to invalidate every session.VIKUNJA_SERVICE_JWTSECRET ist in keinem echten Sinn optional, auch wenn Vikunja ohne es klaglos startet. Lässt du es ungesetzt, erzeugt Vikunja bei jedem Containerstart ein zufälliges — was bedeutet, dass jedes vor einem Neustart ausgestellte Login-Token in dem Moment nicht mehr funktioniert, in dem der Container neu startet, und alle ohne erkennbaren Grund auf einen Schlag ausgeloggt werden. Setze es einmal in der .env auf eine lange Zufallszeichenfolge, und es muss sich nie wieder ändern, außer du willst absichtlich jede Sitzung ungültig machen.
VIKUNJA_SERVICE_PUBLICURL is the other one that has to be right from the start. Vikunja uses it to build absolute links — password-reset e-mails, share links for public Kanban boards, and the CalDAV URLs handed to calendar clients. Set it to the exact address people will type, protocol included: https://tasks.example.com, not the bare hostname and not http://. Get this wrong and the app itself keeps working over HTTPS through Caddy, but every link Vikunja generates for someone else points at the wrong place.VIKUNJA_SERVICE_PUBLICURL ist die andere Einstellung, die von Anfang an stimmen muss. Vikunja baut damit absolute Links — Passwort-Reset-E-Mails, Share-Links für öffentliche Kanban-Boards und die CalDAV-URLs, die Kalender-Clients bekommen. Setze sie auf genau die Adresse, die Leute eintippen werden, Protokoll inklusive: https://tasks.example.com, nicht den nackten Hostnamen und nicht http://. Machst du das falsch, funktioniert die App selbst weiterhin über HTTPS via Caddy, aber jeder Link, den Vikunja für jemand anderen erzeugt, zeigt an die falsche Stelle.
tasks.example.com {
reverse_proxy 127.0.0.1:3456
}
That is the whole file. Caddy requests and renews the certificate on its own as soon as it starts, provided the A record already resolves to the machine — point DNS first, let it settle, then start Caddy. Because the combined image serves the API and the frontend from the same port, there is nothing else to route: no separate /api/ block, no second upstream.Das ist die ganze Datei. Caddy fordert das Zertifikat von selbst an und erneuert es, sobald es startet, vorausgesetzt der A-Eintrag zeigt bereits auf die Maschine — richte zuerst DNS ein, lass es sich setzen, und starte dann Caddy. Weil das kombinierte Image API und Frontend über denselben Port ausliefert, gibt es sonst nichts zu routen: keinen separaten /api/-Block, kein zweites Upstream.
A NAT IPv4 VPS forwards a dedicated SSH port and a small number of other ports; whether 80 and 443 are among them depends on the plan, so check before you point a domain at the box. Vikunja itself only ever needs one of those forwarded ports — the app, the CalDAV endpoint, and the mobile apps all talk to the same https:// address on whichever port Caddy terminates HTTPS on — there is no second service and no second port to arrange.Ein NAT-IPv4-VPS leitet einen dedizierten SSH-Port und eine kleine Anzahl weiterer Ports weiter; ob 80 und 443 darunter sind, hängt vom Tarif ab, also prüfe das, bevor du eine Domain auf die Maschine zeigen lässt. Vikunja selbst braucht immer nur einen dieser weitergeleiteten Ports — die App, der CalDAV-Endpunkt und die mobilen Apps sprechen alle dieselbe https://-Adresse auf dem Port an, auf dem Caddy HTTPS terminiert — es gibt keinen zweiten Dienst und keinen zweiten Port einzurichten.
Where this actually bites is running more than one HTTPS app on the same VPS: only one process can bind port 443 on the machine, so Caddy has to be the only thing listening there, with one block per hostname routing to a different internal port each — the compose file above already sets Vikunja up that way, bound to 127.0.0.1 rather than the public interface. If you specifically want a standalone address with 443 to itself rather than sharing Caddy's, that exists, but on our plans that's arranged by e-mail, not something you self-service from the panel. NAT IPv4 vs a dedicated IP and NAT IPv4, ports and forwarding cover the mechanics.Wo das tatsächlich zubeißt, ist beim Betrieb von mehr als einer HTTPS-App auf demselben VPS: Nur ein Prozess kann Port 443 auf der Maschine binden, also muss Caddy das Einzige sein, das dort lauscht, mit einem Block pro Hostname, der jeweils auf einen anderen internen Port routet — die Compose-Datei oben richtet Vikunja bereits so ein, gebunden an 127.0.0.1 statt an das öffentliche Interface. Willst du ausdrücklich eine eigenständige Adresse mit 443 exklusiv für dich, statt sie dir mit Caddy zu teilen, gibt es das, aber bei unseren Tarifen wird das per E-Mail geregelt, nicht als Self-Service im Panel. NAT IPv4 vs. dedizierte IP und NAT IPv4, Ports und Weiterleitung behandeln die Mechanik.
VIKUNJA_SERVICE_ENABLEREGISTRATION defaults to true, which is exactly right for the five minutes it takes to create your own account and exactly wrong for every minute after that. The moment the domain resolves and Caddy has a certificate, an open registration page on a public URL is an invitation, not a feature.VIKUNJA_SERVICE_ENABLEREGISTRATION ist standardmäßig true, was für die fünf Minuten, die du zum Anlegen deines eigenen Accounts brauchst, genau richtig ist und für jede Minute danach genau falsch. Sobald die Domain auflöst und Caddy ein Zertifikat hat, ist eine offene Registrierungsseite unter einer öffentlichen URL eine Einladung, kein Feature.
Sign up once through the UI to create your account, then set VIKUNJA_SERVICE_ENABLEREGISTRATION=false in the compose file's environment block and restart with docker compose up -d. Existing accounts keep working; the sign-up form just stops accepting new ones. Add more people later by creating their accounts directly, or by flipping registration back on for a few minutes and off again — secure your VPS covers the rest of a clean VPS setup this belongs next to.Melde dich einmal über die UI an, um deinen Account anzulegen, setze dann VIKUNJA_SERVICE_ENABLEREGISTRATION=false im environment-Block der Compose-Datei und starte mit docker compose up -d neu. Bestehende Accounts funktionieren weiter; das Anmeldeformular nimmt nur keine neuen mehr an. Füge später weitere Leute hinzu, indem du ihre Accounts direkt anlegst, oder indem du die Registrierung für ein paar Minuten wieder ein- und danach wieder ausschaltest — deinen VPS absichern behandelt den Rest eines sauberen VPS-Setups, zu dem das dazugehört.
Vikunja ships a built-in CalDAV server, no separate app or sync service required. Point any CalDAV client — a phone's calendar app, Thunderbird, a desktop calendar — at your instance's /dav/ path under the same public URL, and it discovers your lists and their due dates as calendars. The exact discovery URL and any client-specific quirks are documented in Vikunja's own CalDAV page; check it rather than guessing at a path, since CalDAV client setup screens vary in what they ask for first (username, or a full discovery URL).Vikunja bringt einen eingebauten CalDAV-Server mit, keine separate App oder ein Sync-Dienst nötig. Richte einen beliebigen CalDAV-Client — die Kalender-App eines Telefons, Thunderbird, ein Desktop-Kalender — auf den /dav/-Pfad deiner Instanz unter derselben öffentlichen URL, und er entdeckt deine Listen und ihre Fälligkeitsdaten als Kalender. Die genaue Discovery-URL und clientspezifische Eigenheiten sind auf Vikunjas eigener CalDAV-Seite dokumentiert; prüfe das nach, statt einen Pfad zu raten, denn die Einrichtungsbildschirme von CalDAV-Clients fragen unterschiedlich zuerst nach (Benutzername oder eine vollständige Discovery-URL).
The official Android and iOS apps are simpler: on first launch they ask for a server URL, and that URL is VIKUNJA_SERVICE_PUBLICURL — the same address Caddy terminates HTTPS for. There is no separate API endpoint to configure and nothing extra to expose, which is the payoff of running the combined image instead of the frontend and API as two containers: one URL is enough for the web UI, CalDAV, and every mobile client at once.Die offiziellen Android- und iOS-Apps sind einfacher: Beim ersten Start fragen sie nach einer Server-URL, und diese URL ist VIKUNJA_SERVICE_PUBLICURL — dieselbe Adresse, für die Caddy HTTPS terminiert. Es gibt keinen separaten API-Endpunkt zu konfigurieren und nichts Zusätzliches freizugeben, was der Lohn dafür ist, das kombinierte Image statt Frontend und API als zwei Container zu betreiben: Eine URL reicht für die Web-UI, CalDAV und jeden mobilen Client gleichzeitig.
Two things persist: the Postgres database and the vikunja_files volume holding attachments. Dump the database with the tool that ships in the same container, so the credentials never touch your shell history:Zwei Dinge sind zustandsbehaftet: die Postgres-Datenbank und das vikunja_files-Volume, das die Anhänge enthält. Dumpe die Datenbank mit dem Tool, das im selben Container mitgeliefert wird, damit die Zugangsdaten nie deine Shell-History berühren:
docker compose exec db sh -c 'pg_dump -U vikunja vikunja' > "vikunja-$(date +%F).sql"
And the files volume, from a throwaway container rather than the running one:Und das Files-Volume, aus einem Wegwerf-Container statt aus dem laufenden:
# alpine:3.21 — check hub.docker.com/_/alpine/tags for the current release
docker run --rm -v vikunja_vikunja_files:/data -v "$(pwd)":/backup alpine:3.21 \
tar czf "/backup/vikunja-files-$(date +%F).tar.gz" -C /data .
Check the real volume name first with docker compose config --volumes — Compose prefixes it with the project directory name, so it will not always match the example. Copy both files off the VPS entirely; a dump sitting on the same disk as the instance it came from is a file, not a backup. back up your VPS covers what off-machine actually means in practice.Prüfe zuerst mit docker compose config --volumes den tatsächlichen Volume-Namen — Compose stellt ihm den Namen des Projektverzeichnisses voran, sodass er nicht immer dem Beispiel entspricht. Kopiere beide Dateien vollständig vom VPS herunter; ein Dump auf derselben Platte wie die Instanz, von der er stammt, ist eine Datei, kein Backup. Backup deines VPS behandelt, was Backup abseits der Maschine in der Praxis tatsächlich bedeutet.
Updating is the usual two lines:Updates sind die üblichen zwei Zeilen:
docker compose pull
docker compose up -d
Vikunja runs its own database migrations automatically on startup, and there is no supported way to move a migrated database back to an older version — take both backups above immediately before you pull, not after something looks wrong, and read the release notes for the version you are jumping to.Vikunja führt seine eigenen Datenbank-Migrationen automatisch beim Start aus, und es gibt keinen unterstützten Weg, eine migrierte Datenbank zurück auf eine ältere Version zu bringen — mach beide Backups von oben unmittelbar bevor du pullst, nicht erst, nachdem etwas verdächtig aussieht, und lies die Release Notes zu der Version, auf die du springst.
Vikunja is a single Go binary; it is not the thing that uses meaningful RAM on this box. 1 GiB (Starter) is enough for a person or a small team: the Go process itself idles in double-digit megabytes, and Postgres with a small database and a modest shared_buffers setting fits comfortably alongside it with room left for the OS and Docker. SQLite instead of Postgres uses even less.Vikunja ist eine einzelne Go-Binary; sie ist nicht das, was auf dieser Maschine nennenswert RAM verbraucht. 1 GiB (Starter) reicht für eine Person oder ein kleines Team: Der Go-Prozess selbst dümpelt im Leerlauf bei einer zweistelligen Megabyte-Zahl, und Postgres mit einer kleinen Datenbank und einer bescheidenen shared_buffers-Einstellung passt bequem daneben, mit Platz übrig für das Betriebssystem und Docker. SQLite statt Postgres braucht noch weniger.
What actually grows is disk, not RAM: attachments on tasks, and Postgres's own on-disk size as history accumulates. 25 GB (Starter) covers years of normal task and project data; a team that attaches large files to every card is the honest reason to move up a tier for the storage, not because Vikunja itself needs more compute. Never size this by headcount — Vikunja does not publish or enforce a user limit, and the app's own resource use scales with how much data and how many concurrent writers you have, not with how many accounts exist.Was tatsächlich wächst, ist die Festplatte, nicht der RAM: Anhänge an Aufgaben und Postgres' eigene Größe auf der Platte, während sich Historie ansammelt. 25 GB (Starter) decken Jahre normaler Aufgaben- und Projektdaten ab; ein Team, das an jede Karte große Dateien hängt, ist der ehrliche Grund, für den Speicherplatz eine Stufe höher zu gehen, nicht weil Vikunja selbst mehr Rechenleistung braucht. Dimensioniere das niemals nach Kopfzahl — Vikunja veröffentlicht oder erzwingt kein Nutzerlimit, und der eigene Ressourcenverbrauch der App skaliert damit, wie viele Daten und wie viele gleichzeitige Schreiber du hast, nicht damit, wie viele Accounts existieren.
Full disclosure: this is what we sell. Starter (1 vCPU / 1 GiB / 25 GB) is enough for a person or a small team running Vikunja with Postgres; move up when Postgres and a growing library of attachments start competing for RAM and disk, not because of how many people you add.Zur vollen Transparenz: Das ist, was wir verkaufen. Starter (1 vCPU / 1 GiB / 25 GB) reicht für eine Person oder ein kleines Team, das Vikunja mit Postgres betreibt; steige höher, wenn Postgres und eine wachsende Sammlung von Anhängen anfangen, um RAM und Festplatte zu konkurrieren, nicht wegen der Anzahl an Leuten, die du hinzufügst.
Linux KVM VPS — EUR 4.99 to EUR 59.99 a month, on our own single-tenant bare metal in Dallas, TX and Charlotte, NC. Full hardware virtualisation (KVM), your own kernel, full root. Six tiers, vps-starter to vps-ultra. Starter is 1 vCPU, 1 GiB RAM, 25 GB disk.Linux-KVM-VPS — 4,99 bis 59,99 EUR im Monat, auf unserer eigenen Single-Tenant-Bare-Metal-Hardware in Dallas, TX und Charlotte, NC. Vollständige Hardware-Virtualisierung (KVM), eigener Kernel, volles Root. Sechs Tarife, vps-starter bis vps-ultra. Starter hat 1 vCPU, 1 GiB RAM, 25 GB Speicher.
You order in the shop, pay by card (Stripe) or SEPA bank transfer, and your login details are e-mailed to you once the service is set up. Support is e-mail, run by one person, with no guaranteed response time. All prices are final totals under the German small-business rule (§19 UStG); no VAT is added or shown.Du bestellst im Shop, zahlst per Karte (Stripe) oder SEPA-Überweisung, und deine Zugangsdaten werden dir per E-Mail zugeschickt, sobald der Dienst eingerichtet ist. Support läuft per E-Mail, von einer einzelnen Person betrieben, ohne garantierte Reaktionszeit. Alle Preise sind Endpreise. Gemäß § 19 UStG wird keine Umsatzsteuer ausgewiesen.
Order vps-starter → · Linux KVM VPS overviewvps-starter bestellen → · Übersicht Linux-KVM-VPS
Written by the person who runs overnight.host: a small, honest hosting company on dedicated bare metal — Linux VPS, game servers, web hosting. Live status at up.overnight.host.Geschrieben von der Person, die overnight.host betreibt: ein kleines, ehrliches Hosting-Unternehmen auf dedizierter Bare-Metal-Hardware — Linux-VPS, Gameserver, Webhosting. Live-Status unter up.overnight.host.
SQLite is enough for one person, genuinely, with nothing extra to run or back up beyond the file itself. Move to Postgres once more than one person is actively using the instance at the same time — concurrent writes are the case SQLite is worst suited to, and it is why Vikunja documents Postgres as the production choice.SQLite reicht für eine einzelne Person wirklich aus, ohne dass über die Datei selbst hinaus etwas zusätzlich läuft oder gesichert werden muss. Wechsle zu Postgres, sobald mehr als eine Person die Instanz gleichzeitig aktiv nutzt — gleichzeitige Schreibzugriffe sind der Fall, für den SQLite am schlechtesten geeignet ist, und deshalb dokumentiert Vikunja Postgres als die Wahl für den Produktivbetrieb.
Vikunja generates a random one on every container start instead of using a fixed value, which means every session token issued since the last restart becomes invalid the next time the container restarts. Set VIKUNJA_SERVICE_JWTSECRET once in your .env file and it never has to change again.Vikunja erzeugt bei jedem Containerstart ein zufälliges, statt einen festen Wert zu verwenden, was bedeutet, dass jedes seit dem letzten Neustart ausgestellte Sitzungs-Token beim nächsten Neustart des Containers ungültig wird. Setze VIKUNJA_SERVICE_JWTSECRET einmal in deiner .env-Datei, und es muss sich nie wieder ändern.
Because VIKUNJA_SERVICE_PUBLICURL is set to the wrong value, or left at a default that does not match what people actually type. Vikunja builds every absolute link it generates — password resets, public board shares, CalDAV discovery — from that one setting, so it has to be the exact https:// address, protocol included.Weil VIKUNJA_SERVICE_PUBLICURL auf den falschen Wert gesetzt ist oder bei einem Standard belassen wurde, der nicht dem entspricht, was Leute tatsächlich eintippen. Vikunja baut jeden absoluten Link, den es erzeugt — Passwort-Resets, öffentliche Board-Shares, CalDAV-Discovery — aus genau dieser einen Einstellung, also muss sie die exakte https://-Adresse sein, Protokoll inklusive.
Yes, as long as whichever port Caddy uses for HTTPS is one your plan forwards — both the apps and CalDAV clients only ever talk to the same public HTTPS address, with no second port to arrange. Check NAT IPv4 vs a dedicated IP and NAT IPv4, ports and forwarding before you point a domain at the box if you have not already sorted that out for this VPS.Ja, solange der Port, den Caddy für HTTPS verwendet, einer ist, den dein Tarif weiterleitet — sowohl die Apps als auch CalDAV-Clients sprechen immer nur dieselbe öffentliche HTTPS-Adresse an, ohne einen zweiten Port einzurichten. Prüfe NAT IPv4 vs. dedizierte IP und NAT IPv4, Ports und Weiterleitung, bevor du eine Domain auf die Maschine zeigen lässt, falls du das für diesen VPS noch nicht geklärt hast.
You can, briefly — flip VIKUNJA_SERVICE_ENABLEREGISTRATION back to true, restart, have them sign up, then set it back to false and restart again. For anything beyond the odd new person, creating the account directly is less exposure than leaving a public sign-up form open even for a day.Kannst du, kurzzeitig — schalte VIKUNJA_SERVICE_ENABLEREGISTRATION wieder auf true, starte neu, lass die Person sich anmelden, setze es dann zurück auf false und starte neu. Für alles über die gelegentliche neue Person hinaus ist es weniger Angriffsfläche, den Account direkt anzulegen, als ein öffentliches Anmeldeformular auch nur für einen Tag offen zu lassen.
Prices are final totals; no VAT is shown (§19 UStG). Need something the shop does not list? Email us for a written offer.Alle Preise sind Endpreise ohne ausgewiesene USt. (§19 UStG). Du brauchst etwas, das nicht im Shop steht? Schreib uns für ein schriftliches Angebot.
Order now →Jetzt bestellen → Request a custom configIndividuelle Konfiguration anfragen