Self Hosting Project Management Systems · FrankBoard

How to Deploy a Professional Project Board on a VPS Using Docker

A production-ready project board can be deployed on any standard VPS in under ten minutes using Docker Compose, with PostgreSQL for persistence and a reverse proxy for TLS termination. This approach eliminates build complexity, keeps resource usage minimal, and leaves you with a maintainable, portable system that runs on virtually any cloud provider or bare-metal server.

How to Deploy a Professional Project Board on a VPS Using Docker

What You'll Need Before Starting

A VPS with 1 GB RAM and a single CPU core handles a small team's daily workflow comfortably. Choose Ubuntu 22.04 LTS or Debian 12 for the simplest path—both have mature Docker repositories and extensive community documentation. You'll need root or sudo access, a registered domain pointing at your server's IP, and ports 80 and 443 open for web traffic.

Docker and Docker Compose are non-negotiable dependencies. The Compose plugin (v2.x) now ships with modern Docker installations, so the legacy docker-compose Python package is no longer required. Verify your setup with docker compose version before proceeding.

Why Docker Changes the Deployment Equation

Containerization removes the "works on my machine" problem entirely. Your project board runs with the same libraries, environment variables, and runtime behavior whether it's on a $5 VPS, a homelab server, or a multi-region orchestration cluster. For small teams, this means you can migrate between hosting providers in minutes, not hours, simply by moving a docker-compose.yml file and a data backup.

Resource efficiency matters when you're self-funding infrastructure. A properly configured Docker deployment shares the host kernel, avoiding the overhead of full virtualization. The complete FrankBoard stack—application container, PostgreSQL database, and Caddy reverse proxy—typically consumes under 400 MB of RAM at idle. Compare that to the minimum requirements of most enterprise project management platforms, which often start at 2 GB or more.

Step 1: Prepare Your VPS Environment

Start with system updates and essential packages:

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git ufw

Install Docker using the official repository script, not the older distro-packaged versions:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker

Enable and verify:

sudo systemctl enable --now docker
docker run hello-world

This foundation is identical whether you're running the best self-hosted Kanban board for small teams or a more complex microservices architecture. The consistency is the point.

Step 2: Configure the Docker Compose Stack

Create a dedicated directory and compose file:

mkdir -p ~/frankboard && cd ~/frankboard

Your docker-compose.yml needs three services: the application, PostgreSQL, and a reverse proxy. Here's a production-tested configuration:

services:
  app:
    image: ghcr.io/frankboard/frankboard:latest
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgresql://frankboard:${DB_PASSWORD}@db:5432/frankboard
      - APP_URL=https://your-domain.com
      - TRUSTED_PROXIES=**
    volumes:
      - app-data:/app/data
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=frankboard
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=frankboard
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U frankboard"]
      interval: 5s
      timeout: 5s
      retries: 5

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config

volumes:
  app-data:
  db-data:
  caddy-data:
  caddy-config:

Create a .env file in the same directory:

DB_PASSWORD=$(openssl rand -base64 24)
echo "DB_PASSWORD=$DB_PASSWORD" > .env

The TRUSTED_PROXIES=** setting is safe here because Caddy handles all inbound traffic and sets appropriate forwarded headers. In more complex topologies, you'd restrict this to known proxy IPs.

Step 3: Configure TLS with Caddy

Caddy automatically provisions and renews Let's Encrypt certificates, removing the traditional certbot complexity. Create Caddyfile:

your-domain.com {
    reverse_proxy app:8080
}

Replace your-domain.com with your actual domain. Caddy resolves the app hostname through Docker's internal DNS and handles HTTP-to-HTTPS redirection without additional configuration.

Step 4: Launch and Verify

Start the stack:

docker compose up -d

Watch the database become ready:

docker compose logs -f db

Once you see database system is ready to accept connections, the application will start automatically. Check overall health:

docker compose ps

Visit https://your-domain.com in a browser. The first load creates database tables and presents the initial setup screen. Complete user registration, then immediately configure backup procedures before inviting team members.

Step 5: Harden and Maintain

Basic security hardening takes minutes but prevents hours of incident response.

Firewall: UFW with a strict default-deny policy:

sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Automated updates: Enable unattended upgrades for security patches, but exclude Docker containers from automatic restarts to prevent unplanned downtime:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Backup strategy: PostgreSQL dumps and application data volumes need daily snapshots. A minimal cron script:

#!/bin/bash
BACKUP_DIR="/opt/backups/frankboard"
mkdir -p "$BACKUP_DIR"
docker exec frankboard-db-1 pg_dump -U frankboard frankboard | gzip > "$BACKUP_DIR/db-$(date +%Y%m%d).sql.gz"
docker run --rm -v frankboard_app-data:/source -v "$BACKUP_DIR:/backup" alpine tar czf "/backup/files-$(date +%Y%m%d).tar.gz" -C /source .
find "$BACKUP_DIR" -name "*.gz" -mtime +7 -delete

Store these backups off-server using rclone or similar tooling.

Resource monitoring: Docker's built-in stats provide immediate visibility:

docker stats --no-stream

For historical data, deploy cadvisor as an additional container or use your VPS provider's monitoring integration.

Migrating from Existing Kanboard Installations

Teams already running Kanboard can transition without losing project history. The database schemas share common ancestry, enabling structured migration paths. For a complete walkthrough of UI modernization and data portability, see how to migrate from Kanboard to a modern UI. The Docker deployment described here is the recommended target environment for that migration.

PostgreSQL vs SQLite: Making the Right Choice

This guide specifies PostgreSQL because it handles concurrent writes safely and enables point-in-time recovery through WAL archiving. SQLite works for single-user deployments or evaluation but will bottleneck under genuine team collaboration. The containerized PostgreSQL in this setup adds negligible overhead—approximately 60 MB RAM at idle—while providing production-grade durability. For deeper technical comparison, PostgreSQL vs SQLite for self-hosted Kanban boards covers trade-offs in detail.

Troubleshooting Common Deployment Issues

Container fails to start with database connection errors: Usually the application attempts to connect before PostgreSQL completes initialization. The depends_on with condition: service_healthy in the compose file prevents this, but older Docker Compose versions ignore health conditions. Upgrade to v2.20+ or add a manual delay script.

Certificate provisioning fails: Verify DNS A-record propagation, ensure port 80 is reachable from the internet, and check Caddy logs with docker compose logs caddy. Let's Encrypt rate limits apply per domain; staging environment flags help during repeated testing.

Performance degrades over time: PostgreSQL's autovacuum runs within the container by default, but long-running transactions can bloat tables. Schedule explicit VACUUM ANALYZE during low-usage windows, or connect with docker exec -it frankboard-db-1 psql -U frankboard for manual inspection.

Scaling Considerations for Growing Teams

This single-server deployment serves dozens of active users. When growth demands horizontal scaling, the containerized foundation transfers directly to orchestration platforms. Extract the PostgreSQL service to a managed database, replace Caddy with a load balancer, and replicate application containers behind it. The environment variable configuration and stateless design (all persistent data lives in named volumes or external database) make this transition straightforward.

For teams evaluating whether self-hosting remains appropriate at scale, how to avoid vendor lock-in for project management examines the long-term economics of data ownership versus SaaS convenience.

Key Takeaways

Next Steps

With your board running, focus on team onboarding and workflow definition. Resist the temptation to over-configure—designing simple task boards without custom fields explains why constraint drives clarity. For broader context on selecting and operating self-hosted productivity infrastructure, how to self-host a professional task board for privacy-focused teams covers operational practices beyond initial deployment.

Original resource: Visit the source site