Skip to Content

Production Guide: Deploy Plane with Docker Compose + Nginx + PostgreSQL on Ubuntu

A production-ready Plane deployment blueprint with secure secrets handling, TLS termination, backups, verification, and practical troubleshooting.

Most teams adopt modern project management tools because Jira feels too heavyweight for internal operations and spreadsheets become impossible to govern once multiple departments start collaborating. Plane is a strong open-source option: it gives product, engineering, support, and operations teams a shared backlog, issue workflows, and roadmap visibility without forcing you into a closed SaaS model. The challenge is not installing Plane onceβ€”the challenge is operating it safely in production with reliable upgrades, backup discipline, and clear verification checks.

This guide shows a production-ready deployment of Plane on Ubuntu using Docker Compose, PostgreSQL, and Nginx as the public reverse proxy with TLS. The goal is practical operations: deterministic setup, explicit secret handling, observable health checks, and a runbook your team can execute under pressure.

Architecture and flow overview

We will run Plane as application containers on an internal Docker network while exposing only Nginx to the public internet. PostgreSQL stays private and inaccessible from outside the host. TLS terminates at Nginx, and traffic is proxied to the Plane web service. Backups are performed from PostgreSQL using a scheduled dump process, and restores are tested before you consider the stack production-ready.

  • Public edge: Nginx on ports 80/443 with redirect to HTTPS and hardened headers.
  • Application: Plane services on a private Docker bridge network.
  • Data layer: PostgreSQL with persistent volume and credential isolation.
  • Operations: health checks, logs, point-in-time style dump cadence, and rollback plan.

If you already run a central edge proxy, you can keep this pattern and relocate only the Nginx vhost file, but the operational controls remain the same: private database, explicit secrets, and verification gates after every deploy.

Prerequisites

  • Ubuntu 22.04 or 24.04 VPS (minimum 4 vCPU, 8 GB RAM, 80+ GB SSD recommended for active teams).
  • A DNS record such as plane.example.com pointing to the server public IP.
  • A sudo-capable user account (avoid direct root workflows).
  • Open firewall ports: 22, 80, 443.
  • Basic familiarity with Docker logs, PostgreSQL dumps, and DNS propagation checks.
sudo apt update && sudo apt -y upgrade
sudo timedatectl set-timezone UTC
sudo apt -y install ca-certificates curl gnupg lsb-release ufw
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable

If the copy button does not work in your browser, select the code manually and copy it.

Step-by-step deployment

1) Install Docker Engine and Compose plugin

Use Docker’s official repository instead of distro-pinned packages so you avoid old Compose behavior and patch lag. Version drift is a common source of failed upgrades in self-hosted stacks.

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 -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker
docker --version
docker compose version

If the copy button does not work in your browser, select the code manually and copy it.

2) Create directory layout and locked-down environment file

Keep operational files predictable. A consistent layout makes on-call work easier and reduces restoration mistakes. We will place runtime files in /opt/plane and restricted secrets in .env with strict file permissions.

sudo mkdir -p /opt/plane/{nginx,backups,compose}
sudo chown -R $USER:$USER /opt/plane
cd /opt/plane
umask 077
touch .env
chmod 600 .env

If the copy button does not work in your browser, select the code manually and copy it.

3) Generate strong secrets and populate environment variables

Do not reuse your Linux user password, and do not commit this file to Git. Rotate secrets on handoff events and after suspected host compromise. Keep at least one offline copy of critical keys in your enterprise secret manager.

openssl rand -base64 33
openssl rand -hex 32

If the copy button does not work in your browser, select the code manually and copy it.

cat > /opt/plane/.env <<'EOF'
DOMAIN=plane.example.com
TZ=UTC
POSTGRES_DB=plane
POSTGRES_USER=plane
POSTGRES_PASSWORD=REPLACE_WITH_LONG_RANDOM_VALUE
PLANE_SECRET_KEY=REPLACE_WITH_LONG_RANDOM_VALUE
PLANE_REDIS_PASSWORD=REPLACE_WITH_LONG_RANDOM_VALUE
EOF
chmod 600 /opt/plane/.env

If the copy button does not work in your browser, select the code manually and copy it.

4) Create production Docker Compose stack

The compose file below keeps PostgreSQL internal and includes health checks so dependent services wait for database readiness. In production, this lowers startup race conditions and avoids false incident alerts during controlled restarts.

cat > /opt/plane/compose/docker-compose.yml <<'EOF'
services:
  postgres:
    image: postgres:16
    container_name: plane-postgres
    env_file: /opt/plane/.env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - plane_pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    restart: unless-stopped
    networks: [plane-internal]

  plane-web:
    image: makeplane/plane:stable
    container_name: plane-web
    env_file: /opt/plane/.env
    depends_on:
      postgres:
        condition: service_healthy
    expose:
      - "3000"
    restart: unless-stopped
    networks: [plane-internal]

networks:
  plane-internal:
    driver: bridge

volumes:
  plane_pgdata:
EOF

If the copy button does not work in your browser, select the code manually and copy it.

5) Configure Nginx reverse proxy with TLS

Nginx provides the public interface and security headers. Keep your app unexposed; only Nginx should listen publicly. Use Certbot for certificate issuance and auto-renewal.

sudo apt -y install nginx certbot python3-certbot-nginx
sudo tee /etc/nginx/sites-available/plane.conf > /dev/null <<'EOF'
server {
    listen 80;
    server_name plane.example.com;
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name plane.example.com;

    ssl_certificate /etc/letsencrypt/live/plane.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/plane.example.com/privkey.pem;

    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-Content-Type-Options nosniff always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;

    client_max_body_size 25m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/plane.conf /etc/nginx/sites-enabled/plane.conf
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d plane.example.com --non-interactive --agree-tos -m [email protected] --redirect

If the copy button does not work in your browser, select the code manually and copy it.

6) Launch the stack and run first health gate

Bring up services in controlled order and confirm health before announcing access to your team. This avoids exposing half-initialized systems that can corrupt first-run migrations.

cd /opt/plane/compose
docker compose pull
docker compose up -d
docker compose ps
docker logs --tail=120 plane-web
docker logs --tail=120 plane-postgres

If the copy button does not work in your browser, select the code manually and copy it.

7) Configure scheduled PostgreSQL backups

Backups are only real when restores are tested. Keep daily logical dumps locally and replicate to object storage for off-host safety. Encrypted backup archives are strongly recommended where policy requires it.

cat > /opt/plane/backups/backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
source /opt/plane/.env
ts=$(date +%F-%H%M%S)
out=/opt/plane/backups/plane-${ts}.sql.gz

docker exec plane-postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > "$out"
find /opt/plane/backups -name 'plane-*.sql.gz' -mtime +14 -delete
EOF

chmod +x /opt/plane/backups/backup.sh
(crontab -l 2>/dev/null; echo "20 2 * * * /opt/plane/backups/backup.sh >> /var/log/plane-backup.log 2>&1") | crontab -

If the copy button does not work in your browser, select the code manually and copy it.

8) Define upgrade and rollback procedure

Production reliability depends on repeatable upgrades. Always create a fresh backup before image pulls, and keep the previous image digest available for rollback. If post-upgrade checks fail, revert immediately and investigate offline.

cd /opt/plane/backups && ./backup.sh
cd /opt/plane/compose
docker compose pull
docker compose up -d

docker compose ps
curl -I https://plane.example.com

If the copy button does not work in your browser, select the code manually and copy it.

Configuration and secrets handling best practices

Teams often lose control not during deployment, but during month two when ad-hoc changes accumulate. Use these rules to stay audit-friendly and predictable:

  • Keep /opt/plane/.env at 0600 and root-owned if multiple admins have shell access.
  • Never store secrets in shell history; use temporary editor sessions or secret injection from your vault.
  • Rotate database and app secrets on a schedule (for example, quarterly) and after staff role changes.
  • Restrict Docker socket access; membership in the docker group is effectively root-level authority.
  • Store backups in two locations: local short retention for fast restore + remote encrypted copy for disaster recovery.
  • Add host-level monitoring (CPU, disk, memory, IO wait) to catch saturation before Plane becomes slow for end users.

For regulated environments, include tamper-evident logging and document your recovery time objective (RTO) and recovery point objective (RPO). A deployment is only production-ready when these targets are explicit and tested.

Verification checklist

Run this checklist after first deployment and after every upgrade window:

  • DNS and TLS: plane.example.com resolves correctly and certificate chain is valid.
  • App availability: landing page and sign-in are reachable over HTTPS with acceptable latency.
  • Database health: PostgreSQL container reports healthy and no repeated auth failures appear in logs.
  • Backups: at least one fresh .sql.gz exists and can be restored in a test container.
  • Security headers: response includes hardening headers from Nginx configuration.
curl -I https://plane.example.com
docker compose -f /opt/plane/compose/docker-compose.yml ps
ls -lh /opt/plane/backups | tail -n 5

If the copy button does not work in your browser, select the code manually and copy it.

Include these checks in your change-management ticket template. That gives your team operational continuity even when the original installer is unavailable.

Common issues and fixes

Plane returns 502 Bad Gateway after restart

This usually means Nginx is healthy but the Plane service is not yet ready or crashed during startup. Check container logs first, then verify environment variables and database connectivity.

Database authentication failed errors

Confirm POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB match in both postgres and app service context. Hidden whitespace in .env is a frequent cause.

Certificates fail to renew automatically

Run sudo certbot renew --dry-run and inspect Nginx listeners. Renewal commonly fails when port 80 is blocked by firewall policy or another service binds first.

Slow performance under peak team usage

Start by checking host memory pressure and swap activity. If sustained, move to larger instance sizing and tune PostgreSQL shared buffers and connection limits according to your load profile.

Backups exist but restore test fails

Run restoration drills in an isolated environment every sprint. Most backup failures are discovered during first real incident because restore procedures were never practiced end-to-end.

FAQ

Can I run Plane behind an existing centralized Nginx or load balancer?

Yes. Keep Plane services on a private Docker network and forward traffic from your edge tier to Plane. Preserve forwarded headers and HTTPS awareness so redirects and callback URLs remain correct.

Is Docker Compose enough for production, or should I move to Kubernetes?

Compose is sufficient for many small and mid-size internal deployments when you implement proper backup, monitoring, and upgrade controls. Move to Kubernetes when you need stronger multi-node scheduling and platform standardization.

How often should I back up the PostgreSQL database?

For active teams, daily full logical dumps are a baseline. If issue churn is high, add more frequent snapshots or WAL-based strategies and align cadence to your RPO target.

What is the safest way to rotate secrets without long downtime?

Stage a maintenance window, rotate one secret group at a time, and validate each dependency before proceeding. Keep a tested rollback plan and previous credentials temporarily available for emergency reversion.

How do I expose Plane only to my company network?

Place Nginx behind VPN access controls or private network rules, then require SSO/identity policy at the edge. Do not rely solely on obscured URLs for access security.

What should I monitor first for day-2 operations?

Track host CPU, memory, disk usage, container restart counts, PostgreSQL health, TLS expiry, and endpoint latency. Those signals catch most reliability problems early.

Related guides on SysBrix

Talk to us

Need help deploying Plane in production, integrating secure workflows across product and engineering teams, or building backup and upgrade runbooks your team can trust? We can help with architecture, hardening, migration, and operational readiness.

Contact Us

Production Guide: Deploy Appsmith with Docker Compose + Traefik + PostgreSQL on Ubuntu
A production-ready Appsmith deployment blueprint with TLS, hardened secrets handling, backups, and practical day-2 operations.