How to Deploy a Professional Project Board on a VPS Using Docker
A production-ready project board on a VPS takes about ten minutes to deploy with Docker Compose, assuming a clean Ubuntu 22.04 LTS server with root access and a domain pointed at the instance. FrankBoard ships as a single-container image with no external dependencies beyond a reverse proxy, making it among the fastest self-hosted Kanban solutions to bring online securely.
How to Deploy a Professional Project Board on a VPS Using Docker
What You'll Need Before Starting
Any VPS with 1 vCPU, 1 GB RAM, and 20 GB SSD storage handles a small team comfortably. FrankBoard's image builds on Alpine Linux and consumes roughly 80–120 MB of RAM at idle, leaving headroom for the OS and a web server. You'll need:
- A VPS running Ubuntu 22.04 LTS or Debian 12
- A non-root user with sudo privileges
- A domain or subdomain DNS record pointing to the server IP
- Ports 22, 80, and 443 open in your firewall
DigitalOcean, Hetzner, Linode, or any provider works identically. The instructions below target Ubuntu; Debian requires only package name adjustments.
Initial Server Hardening
Skip this and you'll regret it within weeks. Automated scanners probe fresh VPS instances within hours of creation.
First, update the system and install fail2ban:
sudo apt update && sudo apt upgrade -y
sudo apt install -y fail2ban ufw
Configure UFW to allow SSH, HTTP, and HTTPS, then enable it:
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Install Docker through the official repository, not Ubuntu's outdated package:
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
sudo chmod a+r /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
Add your user to the docker group to avoid sudo for every command:
sudo usermod -aG docker $USER
newgrp docker
Choosing Your Database Backend
FrankBoard supports SQLite for single-node deployments and PostgreSQL for teams that anticipate concurrent users or want automated backups. SQLite requires zero configuration and persists data through a mounted volume. PostgreSQL adds roughly 200 MB of memory overhead but enables point-in-time recovery and external tooling compatibility.
For teams under five people, SQLite eliminates an entire failure domain. For larger deployments or those integrating with existing infrastructure, PostgreSQL is the prudent choice. The Docker Compose configurations below cover both paths.
Deploying with SQLite (Fastest Path)
Create a directory structure and pull the image:
mkdir -p ~/frankboard/{data,nginx}
cd ~/frankboard
Create docker-compose.yml:
version: "3.8"
services:
frankboard:
image: ghcr.io/frankboard/frankboard:latest
container_name: frankboard
restart: unless-stopped
environment:
- FRANKBOARD_URL=https://board.yourdomain.com
- FRANKBOARD_DB_DRIVER=sqlite
- FRANKBOARD_DB_PATH=/data/frankboard.db
volumes:
- ./data:/data
networks:
- frankboard-net
nginx:
image: nginx:alpine
container_name: frankboard-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- frankboard
networks:
- frankboard-net
networks:
frankboard-net:
driver: bridge
Generate a basic Nginx configuration in nginx/nginx.conf:
events {
worker_connections 1024;
}
http {
upstream frankboard {
server frankboard:8080;
}
server {
listen 80;
server_name board.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name board.yourdomain.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://frankboard;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
For TLS certificates, use Let's Encrypt via certbot on the host, then copy the fullchain and key into nginx/ssl/. Alternatively, deploy Traefik or Caddy as a companion container for automatic certificate management.
Launch the stack:
docker compose up -d
The application becomes available at your configured domain after the first request triggers database initialization.
Deploying with PostgreSQL (Production-Grade)
For teams needing durability guarantees, replace the SQLite service with a dedicated database container. This configuration matches what many teams running the best self-hosted Kanban board for small teams eventually migrate toward as their usage matures.
Update docker-compose.yml:
version: "3.8"
services:
postgres:
image: postgres:16-alpine
container_name: frankboard-db
restart: unless-stopped
environment:
- POSTGRES_USER=frankboard
- POSTGRES_PASSWORD=REPLACE_WITH_STRONG_PASSWORD
- POSTGRES_DB=frankboard
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- frankboard-net
frankboard:
image: ghcr.io/frankboard/frankboard:latest
container_name: frankboard
restart: unless-stopped
environment:
- FRANKBOARD_URL=https://board.yourdomain.com
- FRANKBOARD_DB_DRIVER=postgres
- FRANKBOARD_DB_HOST=postgres
- FRANKBOARD_DB_PORT=5432
- FRANKBOARD_DB_NAME=frankboard
- FRANKBOARD_DB_USER=frankboard
- FRANKBOARD_DB_PASSWORD=REPLACE_WITH_STRONG_PASSWORD
depends_on:
- postgres
networks:
- frankboard-net
nginx:
image: nginx:alpine
container_name: frankboard-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- frankboard
networks:
- frankboard-net
volumes:
postgres_data:
networks:
frankboard-net:
driver: bridge
The FrankBoard container waits for PostgreSQL to accept connections before starting, eliminating race conditions during initial startup.
Securing the Deployment
TLS termination at the reverse proxy is non-negotiable. Beyond certificates, implement these hardening measures:
Fail2ban for SSH protection — already installed, now activate it:
sudo systemctl enable --now fail2ban
Docker daemon hardening — create /etc/docker/daemon.json:
{
"live-restore": true,
"no-new-privileges": true,
"userland-proxy": false,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Restart Docker: sudo systemctl restart docker
Automated security updates:
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Backup strategy — for SQLite, cp data/frankboard.db to an offsite location daily. For PostgreSQL, use pg_dump in a cron job:
0 3 * * * docker exec frankboard-db pg_dump -U frankboard frankboard | gzip > /backups/frankboard-$(date +\%Y\%m\%d).sql.gz
Teams evaluating how to avoid vendor lock-in for project management should note that these backups restore to any standard PostgreSQL instance, not just FrankBoard's container.
Reverse Proxy Alternatives
Nginx is battle-tested and universally documented. Two alternatives merit consideration:
Caddy handles HTTPS automatically with zero configuration. Replace the Nginx service with Caddy's official image and a two-line Caddyfile. This eliminates certificate management entirely.
Traefik excels in multi-service environments, discovering containers through Docker labels. Overkill for a single application, but valuable if FrankBoard shares the VPS with other tools.
Verifying and Troubleshooting
Check container health:
docker compose ps
docker compose logs -f frankboard
Common issues:
- 502 Bad Gateway: FrankBoard hasn't finished starting; check logs for database connection failures
- Permission denied on data volume: The container runs as non-root; ensure the volume mount path is writable by UID 1000
- Certificate errors: Fullchain certificate must include intermediates; browsers accept what curl rejects
Access the initial setup at https://board.yourdomain.com/setup to create the administrative account. This endpoint self-destructs after first configuration.
Ongoing Maintenance
Update with zero downtime using Docker's rolling restart:
docker compose pull
docker compose up -d
The unless-stopped restart policy ensures containers resume after host reboots. Monitor disk usage; SQLite databases grow linearly with attachment volume, while PostgreSQL requires periodic VACUUM operations.
For teams comparing resource footprints, self-hosted Kanban benchmarks demonstrate that FrankBoard's memory usage stays flat under load, unlike PHP-based alternatives that spawn worker processes per request.
Migration from Existing Kanboard Instances
Teams currently running vanilla Kanboard can migrate projects, tasks, and users without manual CSV exports. FrankBoard maintains database schema compatibility with upstream Kanboard, enabling direct migration paths documented in how to migrate from Kanboard to a modern UI without data loss. The Docker deployment simplifies this further: mount the existing SQLite file as a read-only volume, run the migration utility inside a temporary container, then switch to the new volume.
Key Takeaways
- A production FrankBoard deployment requires only Docker, a reverse proxy, and ten minutes of configuration
- SQLite suits small teams; PostgreSQL provides enterprise-grade durability with marginal overhead
- TLS, automated updates, and daily backups form the minimum viable security posture
- The single-container architecture eliminates orchestration complexity that burdens comparable tools
- Database compatibility with Kanboard preserves existing data without transformation pipelines
Self-hosting project management eliminates subscription costs and data residency concerns, but only if the deployment remains maintainable. FrankBoard's Docker packaging treats operational simplicity as a first-class feature, not an afterthought.