How to Deploy a Work Board Using Docker and PostgreSQL
A production-ready work board on a VPS requires Docker, PostgreSQL, and a reverse proxy with TLS termination. FrankBoard ships with an official Compose stack that handles database initialization, persistent volumes, and health checks in a single file. This guide walks through the complete deployment from bare server to working board.
How to Deploy a Work Board Using Docker and PostgreSQL
Prerequisites and Server Setup
Start with a VPS running Ubuntu 22.04 LTS or Debian 12. FrankBoard runs comfortably on a 2 vCPU, 2 GB RAM instance for teams under 20 users. Reserve at least 20 GB of SSD storage for the database, uploaded files, and Docker overhead.
Install Docker Engine and Docker Compose plugin:
sudo apt update && sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Create a dedicated directory and user:
sudo mkdir -p /opt/frankboard
sudo useradd -r -s /bin/false frankboard
sudo chown -R frankboard:frankboard /opt/frankboard
cd /opt/frankboard
The Docker Compose Configuration
FrankBoard distributes a reference docker-compose.yml that orchestrates three services: the application container, a PostgreSQL 16 database, and an optional Redis cache for session storage and rate limiting. Place this file at /opt/frankboard/docker-compose.yml:
version: "3.8"
services:
app:
image: ghcr.io/frankboard/frankboard:latest
restart: unless-stopped
environment:
DATABASE_URL: postgres://frankboard:${DB_PASSWORD}@db:5432/frankboard?sslmode=disable
REDIS_URL: redis://redis:6379/0
FRANKBOARD_URL: https://board.yourdomain.com
FRANKBOARD_SECRET_KEY: ${SECRET_KEY}
volumes:
- ./data/files:/var/www/html/data/files
- ./data/plugins:/var/www/html/plugins
- ./data/config:/var/www/html/config
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- frankboard
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: frankboard
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: frankboard
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U frankboard -d frankboard"]
interval: 5s
timeout: 5s
retries: 5
networks:
- frankboard
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- ./data/redis:/data
networks:
- frankboard
networks:
frankboard:
driver: bridge
Create the environment file at /opt/frankboard/.env:
DB_PASSWORD=$(openssl rand -base64 32)
SECRET_KEY=$(openssl rand -base64 48)
echo "DB_PASSWORD=${DB_PASSWORD}" > .env
echo "SECRET_KEY=${SECRET_KEY}" >> .env
chmod 600 .env
The DATABASE_URL follows PostgreSQL's standard connection URI format. FrankBoard uses this single variable to derive host, port, credentials, and database name. The sslmode=disable parameter is safe inside the Docker network; TLS between containers adds no security benefit and complicates certificate management.
Initial Deployment and Verification
Launch the stack:
sudo docker compose up -d
sudo docker compose logs -f app
Wait for the database health check to pass and migrations to complete. The first startup prints migration progress, then settles into a ready state. Verify with:
sudo docker compose ps
sudo docker compose exec db pg_isready -U frankboard
Both services should report healthy. The application exposes port 8080 by default inside the container network; external access routes through the reverse proxy configured next.
Reverse Proxy and TLS with Caddy
Caddy handles automatic HTTPS certificates and provides a cleaner configuration than raw Nginx for single-site deployments. Install Caddy:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
Create /etc/caddy/Caddyfile:
board.yourdomain.com {
reverse_proxy localhost:8080
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
Reload Caddy and verify certificate issuance:
sudo systemctl reload caddy
sudo caddy validate --config /etc/caddy/Caddyfile
Backup Strategy
PostgreSQL backups require dumping the database while the application remains running. FrankBoard's state splits into two categories: the relational database and the file volume containing attachments, plugin code, and configuration overrides.
Create /opt/frankboard/backup.sh:
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/opt/frankboard/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "${BACKUP_DIR}"
# Database dump
sudo docker compose exec -T db pg_dump -U frankboard frankboard | gzip > "${BACKUP_DIR}/db_${TIMESTAMP}.sql.gz"
# File archive
sudo tar czf "${BACKUP_DIR}/files_${TIMESTAMP}.tar.gz" -C /opt/frankboard data/files data/plugins data/config
# Retention: keep 14 days
find "${BACKUP_DIR}" -name "*.sql.gz" -mtime +14 -delete
find "${BACKUP_DIR}" -name "*.tar.gz" -mtime +14 -delete
echo "Backup completed: ${TIMESTAMP}"
chmod +x /opt/frankboard/backup.sh
Schedule daily execution:
sudo ln -s /opt/frankboard/backup.sh /etc/cron.daily/frankboard-backup
Restore from backup by stopping the stack, wiping the PostgreSQL volume, recreating it, then replaying the dump:
sudo docker compose down
sudo rm -rf /opt/frankboard/data/postgres/*
sudo docker compose up -d db
sleep 5
zcat /opt/frankboard/backups/db_20240115_030000.sql.gz | sudo docker compose exec -T db psql -U frankboard frankboard
sudo docker compose up -d
Performance Tuning
PostgreSQL benefits from modest tuning on small VPS instances. Create /opt/frankboard/data/postgres/postgresql.conf with these overrides appended after container initialization:
# Append to postgresql.conf after first start
shared_buffers = 512MB
effective_cache_size = 1536MB
maintenance_work_mem = 128MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 5242kB
min_wal_size = 1GB
max_wal_size = 4GB
Restart the database container to apply changes. For teams exceeding 15 active users, scale the application horizontally by increasing work_mem and shared_buffers proportionally to available RAM.
Security Hardening
FrankBoard inherits Kanboard's plugin architecture, which means third-party extensions run with full application privileges. Isolate the deployment with these practices:
- Run containers as non-root where possible. The official image already drops to UID 1000.
- Mount plugin directories read-only unless actively installing:
read_only: trueon the plugins volume after setup. - Enable PostgreSQL's
pg_hba.confto reject connections outside the Docker bridge network. - Rotate
SECRET_KEYif credential exposure is suspected; this invalidates all sessions.
For teams evaluating self-hosted versus cloud alternatives, this deployment model keeps data under direct control. No third party processes task contents, attachment names, or user metadata.
Migration from Existing Kanboard Installations
Teams running vanilla Kanboard can migrate by reusing the same PostgreSQL schema. FrankBoard maintains database compatibility with upstream Kanboard releases through version 1.2.35. The migration path:
- Back up the existing Kanboard database using
pg_dumpor the built-in JSON export. - Deploy FrankBoard with a fresh database container.
- Restore the dump into the new PostgreSQL instance.
- Run
docker compose exec app ./cli migrateto apply any FrankBoard-specific schema additions. - Copy
data/filesanddata/pluginsfrom the old installation to the new volume mounts.
Plugin compatibility varies. FrankBoard ships with a curated set of modern UI extensions pre-installed; legacy Kanboard plugins may conflict with these replacements. Consult the plugin compatibility documentation before migrating custom extensions.
Monitoring and Health Checks
Add a simple uptime monitor with Docker's built-in health checks and a status endpoint. Extend the Compose file:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/status"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Install curl in a derived image, or use wget if building from the Alpine base. For production visibility, expose Prometheus metrics on a separate port and scrape with Grafana or a managed alternative.
Key Takeaways
- FrankBoard deploys as a three-container Docker stack: application, PostgreSQL, and optional Redis for caching.
- The official Compose configuration handles database initialization, persistent volumes, and service dependencies automatically.
- Caddy provides automatic HTTPS with zero configuration beyond a domain name and DNS A record.
- Backups require both PostgreSQL dumps and file volume archives; schedule both daily with 14-day retention.
- Database performance scales with
shared_buffersandwork_memtuned to available VPS memory. - Migration from Kanboard preserves task data and attachments, though plugin compatibility requires individual verification.
This deployment pattern suits any self-hosted work board requiring relational integrity and straightforward operational maintenance. FrankBoard's packaging removes the assembly work of configuring PHP-FPM, Nginx, and database drivers manually, delivering a modern Kanban experience on infrastructure you control.