Deploy a Work Board with Docker and PostgreSQL: A Complete Developer Guide
Running a self-hosted work board with Docker and PostgreSQL takes about ten minutes if you have a compose file that pins the right images and volumes. FrankBoard ships exactly that: a single docker-compose.yml that pulls its own container, wires it to an official PostgreSQL image, and persists data outside the container lifecycle so updates never risk your tasks.
Deploy a Work Board with Docker and PostgreSQL: A Complete Developer Guide
What You Need Before Starting
A server or local machine with Docker Engine 20.10+ and Docker Compose v2. Any Linux VPS with 1 vCPU, 1 GB RAM, and 10 GB storage handles a small team comfortably. You also need a domain or static IP if you want HTTPS, plus ports 80 and 443 open for Traefik or another reverse proxy.
FrankBoard distributes a ready-made Docker Compose configuration that eliminates manual database setup. You only edit environment variables and run docker compose up.
Why PostgreSQL Over SQLite for Production
SQLite works for solo testing but locks under concurrent writes. PostgreSQL handles multiple team members moving cards simultaneously without corruption. It also enables proper backups through pg_dump, point-in-time recovery, and replication if you scale later.
FrankBoard defaults to PostgreSQL in its production compose file because the Kanboard heritage stores every card movement, comment, and attachment reference in relational tables. Those tables grow. PostgreSQL's query planner keeps board loads fast even after thousands of tasks.
The Complete docker-compose.yml
Save this as docker-compose.yml in a dedicated directory:
version: "3.8"
services:
db:
image: postgres:15-alpine
container_name: frankboard-db
restart: unless-stopped
environment:
POSTGRES_USER: frankboard
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: frankboard
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U frankboard"]
interval: 5s
timeout: 5s
retries: 5
app:
image: ghcr.io/frankboard/frankboard:latest
container_name: frankboard-app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://frankboard:${DB_PASSWORD}@db:5432/frankboard
FRANKBOARD_URL: https://board.yourdomain.com
volumes:
- frankboard_data:/var/www/app/data
- frankboard_plugins:/var/www/app/plugins
labels:
- "traefik.enable=true"
- "traefik.http.routers.frankboard.rule=Host(`board.yourdomain.com`)"
- "traefik.http.routers.frankboard.tls.certresolver=letsencrypt"
traefik:
image: traefik:v2.10
container_name: traefik
restart: unless-stopped
command:
- "--api.insecure=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=admin@yourdomain.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
volumes:
postgres_data:
frankboard_data:
frankboard_plugins:
letsencrypt:
Create a .env file in the same directory:
DB_PASSWORD=$(openssl rand -base64 32)
Then launch:
docker compose up -d
The depends_on with condition: service_healthy prevents FrankBoard from connecting before PostgreSQL accepts queries. This eliminates the race conditions that plague simpler compose setups.
Step-by-Step Deployment Walkthrough
1. Prepare Your Host
Update packages and install Docker:
sudo apt update && sudo apt upgrade -y
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
2. Create the Project Directory
mkdir ~/frankboard && cd ~/frankboard
3. Configure Environment Variables
Generate a strong database password and store it:
echo "DB_PASSWORD=$(openssl rand -base64 32)" > .env
chmod 600 .env
4. Launch the Stack
docker compose up -d
Watch logs to confirm healthy startup:
docker compose logs -f app
You should see Connected to PostgreSQL followed by migration completion.
5. Create the First Admin Account
docker compose exec app ./cli user:create --username admin --role admin
The CLI prompts for a password. This avoids default credentials entirely.
6. Verify HTTPS
Browse to your domain. Traefik automatically provisions a Let's Encrypt certificate. If you prefer local testing first, remove the Traefik labels and expose port 80 directly:
ports:
- "8080:80"
Then access http://your-ip:8080.
Volume Strategy and Data Persistence
Three named volumes matter here. postgres_data holds the database files. frankboard_data stores uploaded attachments and local configuration. frankboard_plugins preserves any extensions you add.
Never mount these as bind mounts to host paths unless you understand POSIX permissions. Named volumes let Docker handle ownership, which prevents the common failure where the container user cannot write to ./data.
For backup purposes, copy the volume contents:
docker run --rm -v frankboard_postgres_data:/source -v ~/backup:/backup alpine tar czf /backup/postgres.tar.gz -C /source .
Or use PostgreSQL's native tools:
docker compose exec db pg_dump -U frankboard frankboard > backup.sql
Database Connection Troubleshooting
If FrankBoard exits with database errors, check three things. First, verify the DATABASE_URL format matches postgres://user:password@db:5432/dbname. Second, ensure the db service hostname resolves inside the app container:
docker compose exec app nslookup db
Third, inspect PostgreSQL logs for authentication failures:
docker compose logs db | grep -i "password\|auth"
The most common mistake is special characters in DB_PASSWORD that aren't URL-encoded. Avoid @, :, and / in passwords, or percent-encode them.
Updating Without Downtime
FrankBoard releases update the container image. Pull and restart:
docker compose pull
docker compose up -d
The unless-stopped restart policy and volume mounts mean your data survives the image swap. PostgreSQL stays running during the app container replacement, so active sessions barely hiccup.
For zero-downtime updates, run a second compose stack on a different port, migrate traffic with your reverse proxy, then retire the old containers. Most small teams do not need this complexity.
Security Hardening
Beyond the compose file, implement these practices:
- Firewall: Restrict port 5432 to the Docker network. Never expose PostgreSQL publicly.
- Secrets: Move
DB_PASSWORDto Docker Swarm secrets or an external vault for multi-node deployments. - Backups: Automate daily
pg_dumpto S3-compatible object storage. - Updates: Subscribe to FrankBoard's GitHub releases for security patches.
FrankBoard's self-hosted architecture keeps all data under your control, which satisfies compliance requirements that cloud SaaS cannot.
Scaling Considerations
The compose above serves ten users easily. For fifty-plus, add:
- Connection pooling: Insert a PgBouncer container between app and database.
- Read replicas: PostgreSQL streaming replication for reporting queries.
- CDN: Serve attachments from MinIO or another S3-compatible store.
FrankBoard remains stateless except for its volumes, so horizontal scaling only requires shared storage for attachments and a consistent FRANKBOARD_URL environment variable.
Key Takeaways
- PostgreSQL eliminates SQLite's concurrency limits and enables proper backups for production work boards.
- Health checks in
depends_onprevent startup race conditions between the application and database. - Named Docker volumes persist data across image updates without manual path management.
- Traefik labels automate HTTPS through Let's Encrypt with no separate certificate management.
- FrankBoard's compose template requires only domain substitution and a generated password to go live.
Related Reading
- Deploy FrankBoard with Docker and PostgreSQL
- How to Set Up a Work Board on a VPS for Maximum Privacy
- Self-Hosted vs. Cloud Kanban Boards: A Privacy-Focused Comparison