Managing Task Assignment in Agentic Workflows · FrankBoard

How to Deploy a Work Board Using Docker and PostgreSQL

A production-ready work board on Docker and PostgreSQL requires a single docker-compose.yml file with three services: the application container, a PostgreSQL database, and a reverse proxy for TLS termination. FrankBoard ships with exactly this architecture, using official images and environment-driven configuration that keeps secrets out of your repository.

How to Deploy a Work Board Using Docker and PostgreSQL

What You'll Need Before Starting

Before running any containers, confirm your host meets the baseline requirements. You need a Linux server with Docker Engine 20.10 or newer and Docker Compose plugin installed, plus a domain name pointed at your server's public IP if you want HTTPS. FrankBoard consumes approximately 512 MB RAM at idle and scales comfortably on a 2 GB VPS for teams under fifty users. PostgreSQL 15 or 16 is recommended; the application handles migrations automatically on first boot.

Storage planning matters more than CPU for most Kanban workloads. Plan for at least 20 GB of persistent volume space, with PostgreSQL data and application file uploads stored on named volumes or bind mounts. Never store database files inside the container layer—this is the most common failure mode in self-hosted deployments.

The Complete Docker Compose Configuration

FrankBoard distributes a reference docker-compose.yml that requires no templating engines or external configuration files. The setup uses three services: the FrankBoard application, PostgreSQL, and Traefik as an automatic HTTPS reverse proxy.

Core Services Architecture

The application container exposes port 8080 internally and relies on environment variables for all runtime configuration. It connects to PostgreSQL via a dedicated Docker network, with no host port exposure required for the database. Traefik listens on ports 80 and 443, handles ACME certificate negotiation with Let's Encrypt, and routes traffic based on your domain label.

Here is the complete, production-ready configuration:

version: "3.8"

networks:
  frankboard:
    driver: bridge

volumes:
  postgres_data:
  frankboard_data:
  traefik_certs:

services:
  traefik:
    image: traefik:v3.0
    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
      - traefik_certs:/letsencrypt
    networks:
      - frankboard

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

  frankboard:
    image: ghcr.io/frankboard/frankboard:latest
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql://frankboard:${POSTGRES_PASSWORD}@postgres:5432/frankboard
      FRANKBOARD_URL: https://board.yourdomain.com
      FRANKBOARD_SECRET_KEY: ${SECRET_KEY:?SECRET_KEY required}
      FRANKBOARD_PLUGIN_DIR: /data/plugins
    volumes:
      - frankboard_data:/data
    networks:
      - frankboard
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.frankboard.rule=Host(`board.yourdomain.com`)"
      - "traefik.http.routers.frankboard.entrypoints=websecure"
      - "traefik.http.routers.frankboard.tls.certresolver=letsencrypt"
      - "traefik.http.services.frankboard.loadbalancer.server.port=8080"

Required Environment Variables

Create a .env file in the same directory as your compose file. Two values are mandatory and validated at startup:

POSTGRES_PASSWORD=your-very-strong-random-password-here
SECRET_KEY=$(openssl rand -hex 32)

The SECRET_KEY signs session cookies and must remain stable across container restarts. Rotating it invalidates all active sessions. Generate it once with the OpenSSL command above and back it up in your password manager.

Step-by-Step Deployment Process

1. Prepare Your Host

Update your system packages and install Docker if absent. Add your user to the docker group to avoid root privileges for compose commands. Create the deployment directory:

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

2. Write Configuration Files

Paste the compose contents above into docker-compose.yml. Create the .env file with your generated secrets. No additional configuration files, build steps, or database initialization scripts are required.

3. Launch the Stack

Run the standard Docker Compose sequence:

docker compose pull
docker compose up -d

The depends_on condition with healthcheck ensures FrankBoard waits for PostgreSQL to accept connections before starting. First boot performs automatic schema migration, which typically completes in under ten seconds for fresh databases.

4. Verify and Create Initial User

Check service health:

docker compose ps
docker compose logs -f frankboard

Once logs show Application startup complete, navigate to your configured domain. The first visitor receives an admin account creation prompt. Complete this immediately—there is no default password, and the prompt disappears once any account exists.

Database Connection and Performance Tuning

FrankBoard uses SQLAlchemy with psycopg2 for PostgreSQL connectivity. The DATABASE_URL format follows standard RFC 3986 URI syntax. For advanced deployments, append connection pool parameters:

postgresql://user:pass@host:5432/dbname?sslmode=require&pool_size=10&max_overflow=20

PostgreSQL tuning for small VPS instances (2-4 GB RAM) benefits from these postgresql.conf adjustments, applied via a custom config bind mount or by extending the image:

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 = 2621kB
min_wal_size = 1GB
max_wal_size = 4GB

These values assume a dedicated database server. For shared hosts running multiple services, reduce shared_buffers proportionally.

Backup Strategy and Disaster Recovery

A production deployment requires automated, tested backups of both PostgreSQL data and application file uploads. FrankBoard stores uploaded attachments in /data/uploads inside the container, mapped to the frankboard_data volume.

Automated PostgreSQL Backups

Add a fourth service to your compose file for continuous archiving:

  pgbackrest:
    image: pgbackrest/pgbackrest:latest
    restart: unless-stopped
    volumes:
      - postgres_data:/var/lib/postgresql/data:ro
      - ./backups:/backups
    environment:
      PGBACKREST_STANZA: frankboard
      PGBACKREST_REPO1_PATH: /backups
      PGBACKREST_REPO1_RETENTION_FULL: 7
    command: >
      bash -c "
        pgbackrest stanza-create --no-online || true &&
        while true; do
          pgbackrest backup --type=incr --no-resume;
          pgbackrest expire;
          sleep 86400;
        done
      "
    networks:
      - frankboard

For simpler setups without pgBackRest, a cron job on the host suffices:

0 3 * * * docker exec frankboard-postgres-1 pg_dump -U frankboard frankboard | gzip > /opt/backups/frankboard-$(date +\%Y\%m\%d).sql.gz

Application Data Backup

The frankboard_data volume contains plugins, uploaded files, and configuration exports. Back this up with:

docker run --rm -v frankboard_data:/data -v /opt/backups:/backups alpine tar czf /backups/frankboard-files-$(date +%Y%m%d).tar.gz -C /data .

Test restoration quarterly. Untested backups are unacknowledged risks.

Security Hardening Checkments

Beyond TLS encryption, several practices harden a self-hosted work board against common attack patterns.

Network Isolation

The compose configuration above uses an internal bridge network with no PostgreSQL port exposed to the host. Never map port 5432 to localhost unless you operate a separate backup host requiring direct access—and even then, restrict via firewall rules to specific source IPs.

Secret Management

The .env file contains credentials in plain text. For production environments, migrate to Docker secrets or an external vault:

secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt
  secret_key:
    file: ./secrets/secret_key.txt

services:
  frankboard:
    secrets:
      - secret_key
    environment:
      SECRET_KEY_FILE: /run/secrets/secret_key

FrankBoard reads *_FILE variants for all sensitive variables, following the Docker secrets convention.

Container Runtime Security

Run with read-only root filesystems where possible. FrankBoard supports this with a tmpfs mount for ephemeral files:

  frankboard:
    read_only: true
    tmpfs:
      - /tmp:noexec,nosuid,size=100m

Monitoring and Observability

Production deployments need visibility into application health. FrankBoard exposes a Prometheus metrics endpoint at /metrics when FRANKBOARD_METRICS_ENABLED=1 is set. Configure scraping in your monitoring stack, or add a simple health check service:

  uptime-kuma:
    image: louislam/uptime-kuma:latest
    restart: unless-stopped
    volumes:
      - uptime_data:/app/data
    networks:
      - frankboard
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.uptime.rule=Host(`status.yourdomain.com`)"
      - "traefik.http.routers.uptime.entrypoints=websecure"
      - "traefik.http.routers.uptime.tls.certresolver=letsencrypt"

Migration from Existing Kanboard Instances

Teams running vanilla Kanboard with SQLite or PostgreSQL can migrate to FrankBoard preserving all projects, tasks, and user relationships. The process requires exporting Kanboard's database and importing into the FrankBoard PostgreSQL instance, followed by a schema migration command. Kanboard Ecosystem Compatibility: Plugin and API Mapping covers the technical mapping between database schemas. For a complete walkthrough, see Deploy FrankBoard with Docker and PostgreSQL.

Key Takeaways

Original resource: Visit the source site