How to Deploy FrankBoard Using Docker and PostgreSQL: A Step-by-Step Guide
Deploying FrankBoard with Docker and PostgreSQL gives you a production-ready, self-hosted Kanban board in under 15 minutes. A properly configured docker-compose.yml with named volumes and health checks ensures your data survives container restarts and your database connection remains stable. This guide covers the complete setup from initial configuration to first login.
How to Deploy FrankBoard Using Docker and PostgreSQL: A Step-by-Step Guide
Prerequisites and System Requirements
Before starting, you need a server or local machine running Docker Engine 20.10+ and Docker Compose v2. FrankBoard distributes as a multi-architecture container image supporting AMD64 and ARM64, so it runs equally well on x86 VPS instances, Raspberry Pi clusters, or Apple Silicon development machines.
Your target environment needs at least 512MB RAM for the application container plus 256MB dedicated to PostgreSQL. For teams exceeding 10 active users, allocate 1GB RAM total and enable swap as a buffer. Storage requirements remain minimal—expect 2-5GB for the application, database, and logs combined during normal operation.
You'll also need a domain or subdomain pointed at your server if you plan to serve FrankBoard behind a reverse proxy with TLS. Local deployments for testing can skip DNS configuration.
Why PostgreSQL Over SQLite for Production Deployments
FrankBoard's Docker image ships with SQLite support enabled by default for zero-configuration testing. For any deployment handling multiple concurrent users or expected to persist data long-term, PostgreSQL eliminates the locking contention and corruption risks inherent to file-based databases under containerized workloads.
PostgreSQL handles concurrent board updates, real-time filtering, and plugin operations without the "database is locked" errors common in SQLite under Docker's layered filesystem. The performance difference becomes noticeable above three simultaneous users or when running automated integrations that hit the API repeatedly.
This guide configures PostgreSQL 15 or newer with persistent storage through Docker named volumes, ensuring your task data, user accounts, and board configurations survive container recreation during updates.
Step 1: Create the Docker Compose Configuration
Create a dedicated directory for your deployment and add a docker-compose.yml file with this production-optimized structure:
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 -d frankboard"]
interval: 5s
timeout: 5s
retries: 5
networks:
- frankboard-net
app:
image: 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: ${FRANKBOARD_URL}
FRANKBOARD_SECRET: ${FRANKBOARD_SECRET}
volumes:
- frankboard_data:/var/www/app/data
- frankboard_plugins:/var/www/app/plugins
ports:
- "8080:80"
networks:
- frankboard-net
volumes:
postgres_data:
frankboard_data:
frankboard_plugins:
networks:
frankboard-net:
driver: bridge
The depends_on condition with service_healthy prevents the application from starting before PostgreSQL accepts connections—a common source of failed first deployments. Named volumes isolate your data from the container lifecycle, making updates and migrations straightforward.
Step 2: Configure Environment Secrets
Create a .env file in the same directory. Never commit this file to version control:
DB_PASSWORD=your-secure-random-password-here-min-32-chars
FRANKBOARD_URL=https://board.yourdomain.com
FRANKBOARD_SECRET=another-long-random-string-for-session-signing
Generate cryptographically secure values with openssl rand -base64 48. The FRANKBOARD_SECRET protects session cookies and must remain stable across container restarts or users will be logged out. Rotate it only during planned maintenance windows.
For local testing, use FRANKBOARD_URL=http://localhost:8080 and skip reverse proxy configuration until ready for production exposure.
Step 3: Launch and Verify the Stack
From your deployment directory, run:
docker compose up -d
docker compose logs -f app
The first pull downloads the FrankBoard application image and PostgreSQL base layer. Initial database migrations run automatically on first startup; expect 10-30 seconds before the application responds to HTTP requests.
Watch logs for the line indicating successful migration completion: "Database schema is up to date." If the app container restarts repeatedly, check that your .env values load correctly with docker compose config.
Verify database connectivity independently:
docker compose exec db pg_isready -U frankboard
# Should return: /var/run/postgresql:5432 - accepting connections
Step 4: Complete First-Time Setup
Navigate to http://your-server-ip:8080 (or your configured domain). The initial setup wizard creates the administrator account. This step requires the database to be fully initialized; if you see a connection error, wait 15 seconds and refresh.
After creating the admin user, immediately navigate to Settings → Application and confirm the database driver shows "Postgres" rather than "SQLite." This verification step catches misconfigurations before you invest time in board creation.
For teams migrating from an existing Kanboard instance, How to Migrate from Kanboard to a Modern UI Without Losing Data or Vendor Lock-in covers database export/import procedures that preserve task history, comments, and file attachments.
Step 5: Production Hardening
Reverse Proxy with Automatic TLS
Expose FrankBoard through nginx, Traefik, or Caddy rather than directly on port 8080. A minimal Caddy configuration:
board.yourdomain.com {
reverse_proxy localhost:8080
}
Caddy automatically provisions and renews Let's Encrypt certificates. For Traefik users, add container labels instead of port mappings:
labels:
- "traefik.enable=true"
- "traefik.http.routers.frankboard.rule=Host(`board.yourdomain.com`)"
- "traefik.http.routers.frankboard.tls.certresolver=letsencrypt"
Remove the ports: section when using a reverse proxy network.
Database Backup Automation
Add a backup service to your compose file for point-in-time recovery:
backup:
image: postgres:15-alpine
container_name: frankboard-backup
restart: unless-stopped
environment:
PGPASSWORD: ${DB_PASSWORD}
volumes:
- /opt/backups/frankboard:/backups
command: >
sh -c "while true; do
pg_dump -h db -U frankboard frankboard > /backups/frankboard_$(date +%Y%m%d_%H%M%S).sql;
find /backups -name '*.sql' -mtime +7 -delete;
sleep 86400;
done"
networks:
- frankboard-net
This creates daily dumps retaining seven days of history. Mount /opt/backups/frankboard to your host's backup destination or S3-syncing location.
Resource Constraints
Prevent runaway memory usage under unexpected load by adding limits to the app service:
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
Step 6: Update Without Downtime
FrankBoard releases follow semantic versioning. Update with zero data loss:
docker compose pull
docker compose up -d
The pull fetches the latest compatible image; the up command recreates containers while preserving named volumes. PostgreSQL data, uploaded files, and installed plugins remain intact.
For cautious updates, especially with custom plugins or API integrations, test on a staging clone first. FrankBoard and Kanboard Plugin Compatibility identifies which extensions maintain compatibility across releases.
Performance Optimization for Small VPS Instances
FrankBoard's architecture remains lightweight by design, but several Docker-specific adjustments improve responsiveness on resource-constrained hosts common in self-hosted deployments.
Enable PostgreSQL's shared_buffers to 25% of available RAM by extending the database service command:
command: >
postgres
-c shared_buffers=128MB
-c effective_cache_size=384MB
-c maintenance_work_mem=64MB
For boards with hundreds of tasks across dozens of columns, mount the frankboard_data volume on an SSD rather than network-attached storage. Latency to the SQLite fallback cache (used for session storage even with PostgreSQL) directly impacts board loading times.
Troubleshooting Common Deployment Issues
Container fails to start with "connection refused" to database: The healthcheck interval may need extension on slower hosts. Add start_period: 30s to the database healthcheck configuration.
Persistent login failures after restore from backup: The FRANKBOARD_SECRET value changed. Restore the original secret from your backup .env or invalidate all sessions through database truncation.
Plugin uploads disappear after recreation: Verify the frankboard_plugins volume mounts to /var/www/app/plugins, not a host path that gets overwritten on image update.
High memory usage over time: FrankBoard's PHP-FPM worker pool defaults suit small teams. On 1GB instances, add PHP_MEMORY_LIMIT=256M to the app environment to prevent individual requests from exhausting available RAM.
Key Takeaways
- PostgreSQL eliminates SQLite's concurrency limitations and is essential for multi-user production deployments
- Named Docker volumes for
postgres_data,frankboard_data, andfrankboard_pluginsensure data persists across container updates - The
depends_onwithcondition: service_healthyprevents race conditions during startup - Environment secrets belong in
.envfiles, never in committed compose files or container images - Automated daily backups with rotation protect against data loss without manual intervention
- Resource limits and PostgreSQL tuning keep performance acceptable on sub-$5/month VPS tiers
For teams evaluating whether self-hosting aligns with their operational constraints, Self-Hosted vs. Cloud Kanban Boards: A Privacy-Focused Comparison examines the tradeoffs in depth. Those seeking the broader context of FrankBoard's positioning among alternatives will find The Best Self-Hosted Kanban Board for Small Teams: A Complete Guide directly relevant.