A self-hosted web app to track job applications from Indeed, LinkedIn, and other sources. Tracks progress, statuses, salary, contacts, dated notes, and Google Drive attachments. Runs entirely in Docker.
Prerequisites: Docker Desktop
cp .env.example .env # Optional: edit credentials
docker compose up -d # Build and start the stackThe first startup builds the app image (~2 min) and creates the database schema automatically.
- Track applications from Indeed, LinkedIn, and other sources
- Multiple status tracking (Applied, Interview Scheduled, Offer, Rejected, etc.)
- Salary range, location, and company information
- Dated notes per application (call logs, follow-ups, etc.)
- Contacts per application (recruiters, hiring managers)
- Google Drive attachment links (resumes, cover letters)
- Real-time statistics and analytics
- Search and filter by status, source, or text
- Mobile-friendly responsive design
- Frontend: React 18
- Backend: Node.js + Express
- Database: PostgreSQL 15
- Hosting: Docker (single
docker compose up -d)
The stack runs as two containers connected by a private Docker network:
| Service | Image | Port | Purpose |
|---|---|---|---|
app |
Built from Dockerfile |
3000 | Express API + React build served as static files |
postgres |
postgres:15 |
5432 | Database (bind-mounted to ./data/postgres/) |
The app container waits for postgres to pass its healthcheck before starting. The Express server automatically runs schema migrations on first start.
All configuration is in .env (created from .env.example):
| Variable | Description |
|---|---|
POSTGRES_PASSWORD |
Database password. Must match in both postgres and app services. |
AUTH_USERNAME |
HTTP Basic Auth username. Leave blank to disable. |
AUTH_PASSWORD |
HTTP Basic Auth password. Leave blank to disable. |
# Start the stack
docker compose up -d
# View logs
docker compose logs -f
# Rebuild the app image (after pulling code changes)
docker compose up -d --build
# Stop the stack
docker compose down
# Stop and delete all data (irreversible)
docker compose down -v
# Access the database directly
docker exec -it job-tracker-db psql -U postgres job_tracker
# Access the app container shell
docker exec -it job-tracker-app shOr use the npm scripts (shortcuts for the above):
npm run up # docker compose up -d
npm run logs # docker compose logs -f
npm run rebuild # docker compose up -d --build
npm run down # docker compose downYour job application data is stored in a bind-mounted directory on the host:
./data/postgres/
This folder is in .gitignore so it won't be committed. It contains the raw PostgreSQL data files.
Why bind-mount instead of a Docker volume? Your data is visible in your file browser, can be backed up with normal file tools, and can be synced to cloud storage (Dropbox, iCloud Drive, Google Drive) automatically.
# Linux/Mac
./backup.sh
# Windows
backup.batThis creates a compressed SQL dump in ~/Backups/job-tracker/ and keeps the last 30 days.
# Create SQL dump
docker exec job-tracker-db pg_dump -U postgres job_tracker | gzip > backup-$(date +%Y%m%d).sql.gz
# Restore
gunzip -c backup-20260510.sql.gz | docker exec -i job-tracker-db psql -U postgres job_tracker# Stop the database
docker compose down
# Copy the data folder
cp -r ./data/postgres ./data/postgres-backup-$(date +%Y%m%d)
# Restart
docker compose up -dPut the project (or just the data/ folder) in a cloud-synced directory like Dropbox or iCloud Drive for automatic off-site backup.
After pulling new code:
docker compose up -d --buildIf you encounter 'ContainerConfig' KeyError during rebuild (a known docker-compose bug):
docker compose down
docker compose up -d --buildGET /api/jobs- List jobs (supports?status=,?source=,?search=)GET /api/jobs/:id- Single job with attachments, contacts, and notesPOST /api/jobs- Create jobPUT /api/jobs/:id- Update jobDELETE /api/jobs/:id- Delete jobGET /api/jobs/stats/summary- Aggregate statistics
GET /api/attachments/job/:jobId- List attachments for a jobPOST /api/attachments- Add Google Drive attachment linkDELETE /api/attachments/:id- Delete attachment
GET /api/health- Liveness check (no auth required)
id, company, position, source, status, job_url, location, salary_range, description, applied_date, created_at, updated_at
id, job_id, file_name, file_type, google_drive_id, google_drive_url, created_at
id, job_id, name, email, phone, position, notes, created_at
id, job_id, note_date, note_text, created_at
Migrations live in server/migrations/ and run automatically on container startup.
- The app uses HTTP Basic Auth when
AUTH_USERNAMEandAUTH_PASSWORDare set in.env. Set them in production deployments. - The PostgreSQL port (
5432) is not published to the host — only the app container can reach the database over the private Docker network. - Database password is configurable via
POSTGRES_PASSWORDin.env. The default in.env.exampleispassword123— change it before exposing this stack to the internet.
Port 3000 already in use
Edit docker-compose.yml and change "3000:3000" to "3001:3000" (or any free port).
App container keeps restarting
docker compose logs appUsually means the database isn't ready yet. The depends_on: condition: service_healthy should prevent this; if it persists, check that Docker has enough resources allocated.
Build fails with 'ContainerConfig' KeyError
Run docker compose down then docker compose up -d --build.
Permission errors on ./data/postgres
sudo chown -R 999:999 ./data/postgres(PostgreSQL in the container runs as UID 999.)
Want to start completely fresh
docker compose down -v # WARNING: deletes all data
rm -rf ./data/postgres
docker compose up -d --buildMIT