dossier README

The project's README from github.com/AienTech/dossier. As one markdown file: README.md.

Dossier

Your job search, on your own server: one profile, a tailored resume and cover letter for every job, and every application tracked from draft to offer.

Latest release CI License: AGPL-3.0 Docker image: ghcr.io/aientech/dossier Next.js 16 PostgreSQL 18

Dossier keeps your career history in one profile and turns it into a clean resume. For every job you apply to, it reads the posting, shows how you match, and drafts a tailored resume and cover letter that only use facts from your profile. You refine them in a chat, download them as PDFs and track each application from draft to offer.

It's for one person on a small server: one password, your data in your own PostgreSQL database, and an AI model of your choice. That can be a local model through Ollama, so your profile and documents never leave your server, or a hosted one (any OpenAI-compatible API, or Anthropic), which then receives what it writes from. Privacy says what goes where.

Demo

Dossier in use: the dashboard, dragging a card on the board, a new application from a pasted posting, refining the tailored resume in the chat, the cover letter, and switching resume themes

Recorded with the demo data (Jane Doe is made up) and a stand-in AI server that answers at once, played at 1.2× speed. With a real model, reading a posting and writing the documents takes from a few seconds to a few minutes.

Features

Screenshots

The dashboard: your funnel, a week-by-week chart and what's worth a look next

An application with its tailored resume and the chat that refines it
An application: the tailored resume, and a chat to refine it
A new application, showing how the profile matches the posting's requirements
A new application: how you match the posting
The applications board, with columns from draft to offer
Every application on a board, from draft to offer
Find jobs: postings near the profile's city, with one open
Find jobs and start an application from a posting
The base resume next to its themes
The base resume, with twelve themes and a theme builder
The profile editor, with its sections and strength meter
One profile that every resume and letter comes from
The inbox: employers' replies from Gmail, what each one asks, and mail to link to an application
Replies from Gmail, read by the AI, with answers it drafts
The calendar's week: an interview next to the rest of the Google Calendar
Your interviews in your Google Calendar's week, kept in step

The screenshots show the demo data: Jane Doe is made up.

Self-hosting

Dossier runs as two containers, the app and PostgreSQL, from one Compose file. These four steps take a fresh server to Dossier on your own domain, with HTTPS.

1. Get a VPS

Any Linux server you can reach over SSH works, from any provider. Give it:

The commands below are for Debian or Ubuntu. Only SSH (port 22) and the web (80 and 443) need to be open to the internet.

2. Install Docker and Docker Compose

Docker's install script sets up Docker Engine and the Compose plugin:

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER

The last line lets you run docker without sudo (membership in the docker group is as good as root, so only add accounts you trust). Log out and back in for it to apply, then check:

docker compose version

To install by hand instead, follow Docker's guide for your distribution.

3. Set up the database

Dossier keeps everything in PostgreSQL 18, which runs in its own container next to the app. Nothing outside can reach it, only the app, and its data lives in a Docker volume that survives restarts and upgrades.

Make a folder for Dossier, fetch the Compose file into it, and give the database a random password:

mkdir ~/dossier && cd ~/dossier
curl -fsSL -o compose.yml https://raw.githubusercontent.com/AienTech/dossier/main/compose.production.yml
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .env
chmod 600 .env

.env holds your secrets, so it's readable only by you. The database starts with the app in the next step; Compose waits until it's ready before it starts Dossier.

4. Set up the app

Dossier needs two more secrets in .env:

echo "DOSSIER_SECRET=$(openssl rand -base64 32)" >> .env
docker run --rm -it ghcr.io/aientech/dossier hash-password

Note: In case you are building or running it locally on Apple Silicon (M-series Mac), use the following command to specify the platform:

docker run --platform linux/amd64 --rm -it ghcr.io/aientech/dossier hash-password

Type your password twice. The command prints one line that starts with argon2id:; add it to .env as DOSSIER_PASSWORD=argon2id:… (for example with nano .env). Then start Dossier and watch it come up:

docker compose up -d
docker compose logs -f app

On every start, Dossier applies the database migrations and loads its built-in themes before it serves, and the log shows each step. Press Ctrl+C to stop following it. curl -s localhost:3000/api/health answers {"status":"ok", …} once it's up.

HTTPS. Dossier only listens on the server itself (127.0.0.1:3000), so put a reverse proxy with HTTPS in front of it. Caddy gets and renews the certificate on its own. Install it (on older releases, see Caddy's install guide) and replace its default configuration with your domain:

sudo apt install -y caddy
sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
dossier.example.com {
    reverse_proxy localhost:3000
}
EOF
sudo systemctl reload caddy

Open https://dossier.example.com and sign in. Then pick an AI model in Settings → AI provider (see Choosing an AI provider) and, if you like, a job source in Settings → Job search.

Another proxy, or no HTTPS at home
  • Traefik, nginx or another proxy: it has to set X-Forwarded-Proto and append the client's address to X-Forwarded-For. Caddy and Traefik do both by default; with nginx, add proxy_set_header X-Forwarded-Proto $scheme; and proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;, and raise client_max_body_size to 12m so imports of up to 10 MB get through. Dossier relies on these headers: X-Forwarded-Proto: https makes the session cookie Secure, and the last address in X-Forwarded-For is the client the sign-in rate limit counts. So never expose port 3000 itself to the internet: requests that skip the proxy could set those headers themselves.
  • A home network without a domain: change the app's ports entry in compose.yml to "3000:3000", run docker compose up -d, and open http://<the server's address>:3000. Everything works, but the session cookie can't be marked Secure.

Environment variables

Dossier is configured with environment variables; everything else is set in the app. Put them in .env next to compose.yml, then run docker compose up -d to apply them (a plain restart keeps the old values). The server doesn't start while a required variable is missing or invalid, and its log says which one and how to fix it.

Required:

Variable What it's for
POSTGRES_PASSWORD The database's password. compose.yml builds the app's DATABASE_URL from it.
DOSSIER_PASSWORD A hash of the password you sign in with, made with hash-password (see step 4); the password itself isn't accepted. It's only read from here, never stored. Changing it signs every session out.
DOSSIER_SECRET At least 32 random characters. Signs sessions and encrypts saved API keys; changing it signs everyone out, and API keys have to be entered again.

Optional:

Variable Default What it's for
TZ UTC The server's time zone, e.g. Europe/Berlin. Nightly backups run at 03:00 in it, and dates the server writes (a letter's date, "today") follow it.
DOSSIER_BACKUP_RETENTION_DAYS 14 How many days of nightly backups to keep.
DOSSIER_TELEMETRY on off stops the usage ping, which sends a random install ID and the version number once on each day Dossier is used (see Privacy).
DOSSIER_AI_PROVIDER none local (Ollama or llama.cpp), openai (any OpenAI-compatible API) or anthropic. A default for Settings → AI provider; a choice saved there wins.
DOSSIER_AI_URL per provider The AI server's address, e.g. http://ollama:11434.
DOSSIER_AI_MODEL per provider The model to use, e.g. qwen2.5:7b or claude-sonnet-5.
DOSSIER_AI_KEY none The API key for a hosted provider.
DOSSIER_AI_TIMEOUT_SECONDS 300 local, 120 hosted How long one AI request may take.
DOSSIER_JOBS_SOURCE off jsearch, adzuna, arbeitsagentur or off. A default for Settings → Job search; a choice saved there wins.
DOSSIER_JSEARCH_KEY none A JSearch key, from OpenWeb Ninja or RapidAPI.
DOSSIER_JSEARCH_HOST openwebninja Where that key is from: openwebninja or rapidapi.
DOSSIER_ADZUNA_APP_ID, DOSSIER_ADZUNA_KEY none Adzuna's app ID and key.
DOSSIER_GOOGLE_CLIENT_ID, DOSSIER_GOOGLE_CLIENT_SECRET none A Google OAuth client, for Gmail and Calendar (see Connecting Gmail and Calendar). A client saved in Settings → Google wins.
DOSSIER_PUBLIC_URL the address in use The address Dossier is reached at, e.g. https://dossier.example.com, for Google's redirect after you sign in. Only needed when your proxy doesn't pass on the host and protocol.
DOSSIER_IMAGE ghcr.io/aientech/dossier:latest The image to run. Set a version, e.g. ghcr.io/aientech/dossier:v1.4.1, to upgrade only when you choose.
DOSSIER_PORT 3000 The port on the server (127.0.0.1) that Dossier listens on, for the reverse proxy.
COMPOSE_PROFILES none ai runs Ollama as well (see Local model with Ollama).

Running the image without the Compose file? Set DATABASE_URL (postgresql://user:password@host:5432/database) instead of POSTGRES_PASSWORD, and mount a volume at /data, where nightly backups and zipped PDFs go.

Choosing an AI provider

Open Settings → AI provider, pick one, and press Test connection:

API keys are stored encrypted with DOSSIER_SECRET and never sent back to the browser. Never invent experience (on by default) keeps drafts to facts from your profile and flags anything it can't find there.

Local model with Ollama

With a local model, your profile, postings and documents never leave your server. The Compose file can run Ollama next to Dossier. Switch it on, start it, and pull a model:

echo "COMPOSE_PROFILES=ai" >> .env
docker compose up -d
docker compose exec ollama ollama pull qwen2.5:7b

Then, in Settings → AI provider → Local model, set the server address to http://ollama:11434, pick the model and press Test connection. Or set DOSSIER_AI_PROVIDER=local, DOSSIER_AI_URL=http://ollama:11434 and DOSSIER_AI_MODEL=qwen2.5:7b in .env.

Which model: qwen2.5:7b is a good start: a 4.7 GB download that needs about 6 GB of memory with Dossier's 16k-token context. On a smaller server, qwen2.5:3b (2 GB, about 3 GB of memory) works, with plainer writing. These are suggestions, not yet benchmarked. On a CPU, reading a posting takes a minute or two and a whole new application several minutes, so the local timeout is 5 minutes per request (DOSSIER_AI_TIMEOUT_SECONDS); 4 or more cores help, a GPU much more.

Ollama elsewhere: on another machine in your network, use http://<its address>:11434, and start Ollama there with OLLAMA_HOST=0.0.0.0 so it listens beyond localhost. For Ollama installed on the same server outside Docker, use http://host.docker.internal:11434, which compose.yml points at the server; Ollama then has to listen beyond localhost too, so make sure your firewall keeps port 11434 closed to the internet. Inside a container, localhost is the container itself, never the server.

Choosing a job source

Find jobs is off until you pick a source in Settings → Job search. LinkedIn and Indeed don't let other apps read their job listings, so Dossier searches one of these instead:

Source What it covers Key and cost
JSearch Google for Jobs: postings from LinkedIn, Indeed, Glassdoor, Stepstone and company sites, in most countries, with their whole text. A key from OpenWeb Ninja or RapidAPI. The free plan has 200 searches a month; each page of results is one.
Adzuna Adzuna's own index in 19 countries. It only shares the start of each posting; Dossier fetches the rest from the posting's page. A free app ID and key from Adzuna's developer site, about 250 searches a day.
Bundesagentur für Arbeit Germany's federal job board, with whole postings. Germany only. No key, free. It isn't an official API (it's the access the agency's own site uses), so it can change without notice.

The search starts from your profile's city, country and current role; change the place, radius (10 to 200 km), how recent and Remote only as you like. Every search is kept with what it found until you remove it, under Past searches, so going back to one, or reloading, doesn't spend another search.

Connecting Gmail and Calendar

This is optional. With a Google account connected, Dossier sends an application from your own Gmail address with its PDFs attached, checks Gmail every few minutes for employers' replies and links them to their applications, has the AI read each reply and draft your answer, offers times you're free in Google Calendar, and adds agreed interviews to it. Nothing is sent, and no status changes, until you confirm it.

The Calendar screen shows your week or month: the events in your Google Calendar, with your interviews linked to their applications. Interviews stay in step both ways: change or remove an interview's date in Dossier and its event moves or goes; move or delete the event in Google Calendar and Dossier follows.

Google doesn't let one sign-in serve every server, so each install brings its own OAuth client:

  1. In Google Cloud Console, create a project and enable the Gmail API and the Google Calendar API.
  2. Set up the OAuth consent screen as External, add yourself as a test user, then publish it (In production). While it's in Testing, Google stops the access after 7 days.
  3. Create an OAuth client ID of type Web application, with https://<your Dossier address>/api/google/callback as an authorised redirect URI. Settings → Google shows the exact address to copy.
  4. Enter the client's ID and secret in Settings → Google, or set DOSSIER_GOOGLE_CLIENT_ID and DOSSIER_GOOGLE_CLIENT_SECRET. Then press Connect Google account and choose what Dossier may do: send, read, and use your calendar.

Google only verifies apps that read mail after a security review, so its sign-in page warns that the app isn't verified. It's your own client: continue under Advanced. The refresh token is stored encrypted with DOSSIER_SECRET. Disconnect revokes Dossier's access at Google.

By default the inbox check only keeps mail in threads Dossier sent and mail from the companies you applied to. Every new email in Settings → Google keeps all new mail outside Promotions and Social, and the AI reads that too. PRIVACY.md says what's kept, for how long, and what goes to Google and the AI.

Backups and restore

Run these from your Dossier folder (~/dossier).

Keep a copy of .env with your backups: saved API keys can't be read without its DOSSIER_SECRET (an export with the key included carries it in plain text instead).

Upgrading

When a new version is out, the sidebar says so, with a link to what's new; Settings → About shows it too. Dossier checks GitHub for it once on each day you use it (switch that off in Settings → About).

Take a backup first (an export, or a database dump). Then, in your Dossier folder:

docker compose pull
docker compose up -d

latest is always the newest release. If you've pinned a version with DOSSIER_IMAGE, change it to the new one first. When a release's notes say compose.yml changed, fetch it again with the curl line from step 3.

The new version updates the database on start. Going back to an older version after that isn't supported: restore the backup you took instead.

From 1.4, Dossier sends a usage ping (see Privacy). If you'd rather it didn't, set DOSSIER_TELEMETRY=off before you upgrade to 1.4 or later.

From 1.0: Dossier 1.0 took the password itself in DOSSIER_PASSWORD; newer versions only take a hash of it, and don't start until it is one. Make the hash with the newer image (docker run --rm -it ghcr.io/aientech/dossier hash-password, typing your current password to keep it), put it in .env, then pull and start as above. Every browser is signed out once and signs in again with the same password.

Troubleshooting

docker compose logs app shows the app's log (add -f to follow it). After changing .env, run docker compose up -d again.

Privacy

Everything you enter is kept in your PostgreSQL database and data directory. PRIVACY.md has the details: what's kept and for how long, what deleting removes, everything that leaves your server and who receives it, and a note on the GDPR.

Known limitations

Performance and sizing

Measured on 28 September 2026 with the production build in a container limited to 2 vCPUs and 1 GB:

Security

What protects your data, how to run Dossier safely and how to report a problem: see SECURITY.md.

Development

Working on Dossier itself: see DEVELOPMENT.md.

License

Dossier is free software under the GNU Affero General Public License v3.0. You may use, change and share it; if you run a changed version for others over a network, you must offer them its source code.

dossier (AienTech/dossier)

Searching Database…

What is this?Dossier is a self-hosted web app for job hunting: you keep one career profile, and for each job posting it drafts a tailored resume and cover letter from it, lets you refine them in a chat, and tracks every application from draft to offer.
Is this a web app I host myself, or a hosted service?It is a self-hosted web app: you run it as two Docker containers (the Next.js app and PostgreSQL) behind your own reverse proxy, with no hosted version available.
How do I get it running?Get a Linux VPS with Docker and Docker Compose installed, fetch the Compose file, set secrets in .env (database password, DOSSIER_SECRET, a hashed DOSSIER_PASSWORD), run docker compose up -d, then put a reverse proxy like Caddy in front for HTTPS.
Do I need an AI provider or API key?Yes, you pick one in Settings: a local model via Ollama which keeps everything on your server for free, or a hosted provider through any OpenAI-compatible API or Anthropic's Claude using your own API key.
Does it ever invent experience I don't actually have?No, drafts are restricted to facts already in your profile, and whatever the posting asks for that is missing gets flagged as a gap instead of being made up.
Can I run this on any small VPS?Yes, any Linux server reachable over SSH works, with about 2 vCPUs and 2GB RAM for a hosted AI provider, or more (roughly 6GB extra memory and 4+ cores) if you run a local model like qwen2.5:7b through Ollama.
Where does my resume data actually live?Everything is stored in your own PostgreSQL database on your server, and if you pick a local Ollama model, your profile, postings and documents never leave that same server.
Where can it search for job postings?Through JSearch, which aggregates LinkedIn, Indeed and more, plus Adzuna and Germany's Bundesagentur für Arbeit, searchable near your city or anywhere you choose.
Can it handle email replies and interview scheduling?Optionally, by connecting Gmail and Google Calendar: it can surface employer replies next to each application, draft answers from your profile, and keep interviews synced two-way with your Calendar, with nothing sent without your confirmation.
How many resume designs come built in?Twelve themes plus a theme builder, all previewed live and rendered to PDF on the server, and you can duplicate or share themes as files.
Who is this really built for?One person running it on a small server of their own: a single password, your own PostgreSQL database, and an AI model you choose, not a multi-user SaaS.
Creator
AienTech
Size
21.1 MB
Stars
166
Age
15d
Commits · 90 Days