Deploying Plane on a VPS: self-hosted project management, SSL, and backups
TL;DR
Plane is a self-hosted platform for managing projects, tasks, sprints, notes, and teams. Below, we will deploy Plane in Docker on Ubuntu 24.04 LTS, place it behind Caddy with an automatic SSL certificate, configure basic server security, and set up backups for the database, configuration, and user files.
- We will use a VPS with at least 4 vCPU, 8 GB RAM, and an SSD of at least 80 GB.
- We will install Docker Engine and Docker Compose Plugin from the official repository.
- We will deploy Plane using the official self-hosted installer and pin the release version.
- We will open only SSH, HTTP, and HTTPS, and restrict administrative access to SSH keys.
- We will configure Caddy to automatically obtain and renew the TLS certificate.
- We will create daily backups of PostgreSQL, the configuration, and object storage data.
1. TL;DR
This guide covers the complete Plane deployment cycle: from a clean VPS and basic Ubuntu security to publishing the application over HTTPS and restoring from a backup. The commands are intended for Ubuntu Server 24.04 LTS and Docker Engine 28.x or a newer stable version available in the official Docker repository at the time of installation.
- Plane runs as a set of containers: web interface, API, background tasks, PostgreSQL, Redis, and file storage.
- The domain name must point to the server’s public IPv4 address before starting Caddy.
- Secrets are stored in the
.envfile with restricted permissions. - A single backup must include not only PostgreSQL, but also uploaded files, configuration, and keys.
2. Contents
The article is structured as a practical scenario for the owner of a new VPS. First, resources and requirements are defined, followed by server preparation, Plane installation, HTTPS publishing, and regular backup configuration.
- Prepare the DNS record for the domain.
- Create a separate administrator and disable password login.
- Install Docker and system utilities.
- Deploy Plane from the official source.
- Configure an external reverse proxy and TLS.
- Verify the application and automate backups.
3. What we are configuring and why
What is Plane
Plane is an open-source development and project management system. It provides workspaces, projects, tasks, statuses, cycles, modules, views, comments, pages, and basic team collaboration capabilities. The interface is suitable both for a small product team and for several independent projects within one organization.
The self-hosted version runs in the owner’s infrastructure. Data is stored on their server, and access to the application is provided through their own domain. This is especially convenient when it is necessary to control data location, network rules, retention periods, or integration with internal services.
What will work after configuration
After completing the instructions, a user will be able to open, for example, https://plane.example.com, create a workspace, and invite team members. Plane will run in Docker containers, while Caddy will accept external HTTPS traffic and forward it to the application through the local interface.
The server will also contain:
- PostgreSQL for persistent application data;
- Redis for queues and caching;
- object storage for attachments and user files;
- Docker volumes for data that must survive container recreation;
- container logs and system logs for diagnostics.
Cloud-managed or self-hosted
| Criterion | Cloud version | Self-hosted on a VPS |
|---|---|---|
| Installation | Practically not required | The server and containers must be maintained |
| Data control | Depends on the provider | Data remains under the owner’s control |
| Cost | Usually depends on the number of users | The main expense is the server, disks, and backup |
| Updates | Performed automatically or by the provider | Must be planned independently |
| Integration | Limited by the service’s capabilities | VPN, SSO, SMTP, and internal webhooks can be configured |
The cloud-managed option is more practical if there is no time to operate a Linux server. Self-hosted Plane on a VPS is chosen when control, predictable infrastructure, independence from SaaS limits, or proximity to other internal services is important.
Prerequisites
You need a domain or subdomain, such as plane.example.com. Create an A-type DNS record pointing to the server’s IPv4 address. If IPv6 is used, add an AAAA record only after verifying that the server and firewall properly handle IPv6.
# Проверяем, что DNS уже указывает на нужный адрес
dig +short plane.example.com A
Before obtaining the certificate, the domain must be accessible from the internet through ports 80 and 443. If an external firewall is located in front of the server, its rules must also allow these ports.
4. What VPS configuration is needed for this task
Plane is not a static HTML page. A self-hosted installation runs several containers, a database, a task queue, and file storage. Therefore, it is incorrect to consider only the size of the web interface: RAM, a fast disk, and CPU headroom for background operations are all important.
Minimum requirements
| Resource | Minimum for a test team | Practical starting point for production |
|---|---|---|
| CPU | 2 vCPU | 4 vCPU |
| RAM | 4 GB | 8 GB |
| Disk | 40 GB SSD | 80–160 GB NVMe SSD |
| Network | 100 Mbps | 1 Gbps or higher |
| Address | Public IPv4 | Public IPv4 and, if necessary, IPv6 |
| OS | Ubuntu 24.04 LTS | Ubuntu 24.04 LTS |
A configuration with 4 vCPU and 8 GB RAM is suitable for a small team, several projects, and a moderate number of attachments. The disk must be selected with file growth in mind: the Plane database usually occupies less space than images, documents, and other attachments.
One option is to get a VPS with 4 vCPU, 8 GB RAM, an NVMe disk of at least 80 GB, and a public IPv4 address. Similar parameters can be obtained from any provider if it offers full root access, virtualization with guaranteed resources, and the ability to create external backups.
When a dedicated server is needed
A dedicated server is justified not by the mere fact of running Plane, but by the workload and isolation requirements. It is needed if Plane, GitLab, CI runners, monitoring, databases, and other resource-intensive services run on the same server simultaneously, or if guaranteed performance without CPU and disk contention is required.
For a team of several dozen people, a dedicated server is usually not mandatory. First, CPU, RAM, I/O, and data size should be measured. Switching to a dedicated server makes sense after sustained load, frequent swapping, or the need to host multiple production systems appears.
Choosing a location
Location affects access latency, legal requirements, and data transfer costs. For a team located in one region, choose a data center with minimal RTT to users. For an international team, a stable route and IPv4 availability are more important than a difference of several milliseconds.
It is preferable to store backups in another geographic zone or at least on another physical server. A backup on the same VPS does not protect against disk deletion, account suspension, hardware failure, or administrator error.
5. Server preparation
Connecting and creating an administrator
It is assumed that the provider has supplied a clean Ubuntu 24.04 LTS server and initial SSH access. Substitute the IP address and username created during installation.
# Подключаемся к серверу по SSH
ssh root@SERVER_IP
We will create the deploy user, add it to the sudo group, and install a public key. Execute this block as root.
# Создаём отдельного администратора
adduser deploy
# Разрешаем пользователю выполнять административные команды
usermod -aG sudo deploy
# Создаём каталог для SSH-ключей
install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
# Открываем редактор для добавления публичного ключа
nano /home/deploy/.ssh/authorized_keys
# Исправляем владельца и права файла ключей
chown deploy:deploy /home/deploy/.ssh/authorized_keys
chmod 600 /home/deploy/.ssh/authorized_keys
Insert the contents of the id_ed25519.pub file from your computer into authorized_keys. Then verify the new login in a separate terminal without closing the current root session.
# Проверяем вход новым пользователем
ssh deploy@SERVER_IP
# Проверяем права sudo
sudo -v
System updates and basic utilities
# Обновляем индексы пакетов и устанавливаем исправления
sudo apt update && sudo apt full-upgrade -y
# Устанавливаем инструменты администрирования и диагностики
sudo apt install -y ca-certificates curl gnupg lsb-release \
unzip jq git vim nano htop tree dnsutils \
ufw fail2ban unattended-upgrades
Reboot the server if the kernel or system libraries were updated.
# Проверяем необходимость перезагрузки
if [ -f /var/run/reboot-required ]; then sudo reboot; fi
SSH keys and disabling passwords
After verifying key-based login, disable password authentication. Before changing the configuration, save a copy of the file and check the SSH syntax.
# Сохраняем резервную копию конфигурации SSH
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
# Открываем конфигурацию SSH
sudo nano /etc/ssh/sshd_config
Make sure the following parameters are present:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
# Проверяем конфигурацию перед перезапуском
sudo sshd -t
# Применяем настройки SSH
sudo systemctl restart ssh
Firewall
We will open SSH, HTTP, and HTTPS. If SSH runs on a non-standard port, replace 22/tcp with the corresponding value. First allow SSH, and only then enable UFW.
# Разрешаем административный доступ и веб-трафик
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Включаем firewall с политикой запрета входящих соединений
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw --force enable
# Проверяем активные правила
sudo ufw status verbose
Fail2ban
Fail2ban temporarily blocks addresses from which repeated unsuccessful SSH login attempts are made. It does not replace keys and a firewall, but it reduces noise from automated scanners.
# Создаём локальную конфигурацию jail для SSH
sudo tee /etc/fail2ban/jail.d/sshd.local > /dev/null <<'EOF'
[sshd]
enabled = true
backend = systemd
port = 22
maxretry = 5
findtime = 10m
bantime = 1h
EOF
# Перезапускаем fail2ban и включаем автозапуск
sudo systemctl enable --now fail2ban
# Проверяем состояние SSH-защиты
sudo fail2ban-client status sshd
6. Software Installation — Step by Step
Installing Docker Engine
On Ubuntu, it is better to use Docker’s official apt repository rather than outdated packages from the standard repository. In 2026, use the current stable Docker Engine 28.x branch or a newer one if it has already been published for Ubuntu 24.04.
# Удаляем конфликтующие неофициальные пакеты
sudo apt remove -y docker.io docker-doc docker-compose podman-docker containerd runc || true
# Создаём каталог для ключей apt
sudo install -m 0755 -d /etc/apt/keyrings
# Загружаем официальный ключ Docker
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Делаем ключ доступным для apt
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Добавляем репозиторий Docker для текущего релиза Ubuntu
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
# Обновляем список пакетов и устанавливаем Docker Compose Plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
# Разрешаем запуск Docker без sudo для пользователя deploy
sudo usermod -aG docker "$USER"
# Проверяем версии
docker --version
docker compose version
After adding the user to the Docker group, you need to open a new SSH session. The docker group effectively grants root privileges, so add only trusted administrators to it.
# Проверяем работу Docker после нового входа
docker run --rm hello-world
Downloading the Official Plane Installer
The self-hosted Plane distribution is published by the Plane project on GitHub. The installer creates a deployment directory, Docker Compose files, and an environment variable template. Before using it in production, check the release notes and compatibility of the selected version.
# Переходим в домашний каталог администратора
cd ~
# Создаём отдельный каталог для Plane
mkdir -p ~/plane
cd ~/plane
# Загружаем официальный установщик из репозитория Plane
curl -fsSL -o setup.sh \
https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/install.sh
# Делаем установщик исполняемым
chmod 700 setup.sh
# Просматриваем установщик перед запуском
less setup.sh
Checking the script before execution is mandatory: it shows which images, directories, and commands will be used. If the project has published an installer for a specific release, it is preferable to obtain it from the release page rather than use the floating master branch.
# Запускаем интерактивную установку Plane
./setup.sh install
Depending on the installer version, the command may be called ./setup.sh install, ./setup.sh start, or may provide an action menu. Use the name shown by the script itself when run with the help parameter.
# Показываем доступные действия конкретной версии установщика
./setup.sh --help
Obtaining the Initial Configuration
Usually, a .env file and one or more Compose files appear in the Plane directory. If the installer created only a template, copy it to a working file and fill in the values.
# Показываем содержимое каталога развёртывания
find ~/plane -maxdepth 2 -type f -printf '%p\n' | sort
# Если присутствует шаблон, создаём рабочий файл окружения
[ -f ~/plane/.env.example ] && cp ~/plane/.env.example ~/plane/.env
# Ограничиваем доступ к секретам
chmod 600 ~/plane/.env
Starting the Containers
Before the first launch, review the final configuration. This allows you to see the service names, published ports, and the volume that needs to be included in backups.
# Переходим в каталог Plane
cd ~/plane
# Проверяем и разворачиваем итоговую Compose-конфигурацию
docker compose config > /tmp/plane-compose-resolved.yml
# Запускаем сервисы в фоне
docker compose up -d
The first launch may take several minutes: Docker downloads images, creates the network and volumes, and the application performs database migrations.
# Проверяем список контейнеров и их состояние
docker compose ps
# Смотрим последние 200 строк общего журнала
docker compose logs --tail=200
# Следим за журналом конкретного сервиса при необходимости
docker compose logs -f --tail=100 web
The web service name depends on the Compose file version. If this service does not exist, first run docker compose config --services and select the actual name of the frontend or proxy service.
7. Configuration
Environment Variables
All Plane secrets must be stored in .env or in a secure secrets manager. Do not insert PostgreSQL passwords, JWT keys, or SMTP passwords directly into the Compose file, Git repository, or shell script accessible to all users.
Variable names depend on the specific Plane release. Do not remove required values from the file created by the installer. Fill in the domain and generate random secrets wherever provided for by the template.
# Генерируем криптографически случайные значения для собственных секретов
openssl rand -hex 32
openssl rand -base64 48
# Открываем файл переменных окружения
nano ~/plane/.env
# Проверяем права на файл секретов
stat -c '%A %U:%G %n' ~/plane/.env
The configuration must correctly specify the application’s public URL, domain, PostgreSQL, Redis, SMTP, and object storage parameters. The public URL must use the final HTTPS address, for example:
WEB_URL=https://plane.example.com
CORS_ALLOWED_ORIGINS=https://plane.example.com
Exact variable names must be checked against the documentation and the template for the selected release. Do not mechanically add nonexistent variables: the application may ignore them and continue working with insecure default values.
Publishing Through Caddy
Caddy automatically obtains and renews Let’s Encrypt certificates. Plane can first be started on a local port inaccessible from the internet, with only Caddy exposed externally.
Check which Plane service publishes the HTTP port:
# Показываем сервисы и опубликованные порты
cd ~/plane
docker compose config --services
docker compose ps
If the Compose file publishes container port 80 on host port 80, change the binding to a local address and an available port, for example 127.0.0.1:8080:80. Do this in a supported override file or using the method provided by the installer. The override example below applies only if the service is actually named proxy and listens on port 80 internally.
services:
proxy:
ports:
- "127.0.0.1:8080:80"
If the internal service has a different name, replace proxy. Do not expose PostgreSQL, Redis, MinIO, or administrative ports directly to the outside.
# Применяем изменение и пересоздаём контейнеры
cd ~/plane
docker compose up -d
# Проверяем локальный HTTP-ответ
curl -I http://127.0.0.1:8080
Install Caddy from the official repository.
# Добавляем официальный ключ репозитория 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
# Добавляем репозиторий Caddy
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
sudo tee /etc/apt/sources.list.d/caddy-stable.list
# Устанавливаем Caddy 2.x
sudo apt update
sudo apt install -y caddy
Create the /etc/caddy/Caddyfile configuration. Replace the domain with your own.
plane.example.com {
reverse_proxy 127.0.0.1:8080
encode zstd gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
}
log {
output file /var/log/caddy/plane-access.log
format json
}
}
# Проверяем синтаксис Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
# Включаем запуск Caddy вместе с системой
sudo systemctl enable --now caddy
# Перезапускаем после изменения конфигурации
sudo systemctl reload caddy
# Проверяем состояние reverse proxy
sudo systemctl status caddy --no-pager
If DNS is already configured, Caddy will automatically request a certificate. The check should be performed from an external computer or through the public domain:
# Проверяем HTTPS и заголовки ответа
curl -I https://plane.example.com
# Проверяем TLS-сертификат
curl -vI https://plane.example.com 2>&1 | grep -E 'SSL connection|subject:|issuer:'
Checking Plane Status
Open the domain in a browser, create the initial account, and check project, task, and attachment creation. Then inspect the containers and the latest errors.
# Проверяем, что контейнеры не находятся в состоянии restarting
cd ~/plane
docker compose ps
# Ищем ошибки в журналах за последний запуск
docker compose logs --since=10m 2>&1 | grep -iE 'error|fatal|panic' || true
# Проверяем место на диске
df -h
docker system df
The ping command checks only ICMP connectivity and does not confirm that HTTPS is working. For the application, it is more useful to use curl, check the container status, and review the logs.
8. Backups and maintenance
What needs to be saved
A minimal Plane backup consists of four parts: a PostgreSQL dump, user files, the .env file, and the Compose configuration. If MinIO or another S3-compatible storage is used, its bucket with attachments cannot be replaced by a database dump alone.
- PostgreSQL: workspaces, projects, tasks, users, and settings.
- Object storage: images, documents, avatars, and attachments.
- Configuration:
.env, Compose files, and Caddyfile. - Keys: only if they are needed for decrypting or accessing the backup.
Do not rely on a VPS snapshot as the only backup. A snapshot is convenient for a quick rollback, but if there is a problem with the account or storage itself, it may become unavailable along with the server.
Installing restic
Restic encrypts the backup before sending it to external storage. The example uses an S3-compatible bucket. Create the bucket in advance, create a separate user with minimal permissions, and store the repository password outside the server or in a secure secret store.
# Устанавливаем restic из репозитория Ubuntu
sudo apt update
sudo apt install -y restic
# Создаём каталоги для временных дампов и скриптов
sudo install -d -m 700 /var/backups/plane
sudo install -d -m 700 /usr/local/sbin
Backup script
The names of the database and object storage services must be determined from the output of docker compose config --services. In the example, the database is named plane-db. If the name is different in your version, change the DB_SERVICE variable.
sudo nano /usr/local/sbin/plane-backup.sh
#!/usr/bin/env bash
set -Eeuo pipefail
PLANE_DIR="/home/deploy/plane"
BACKUP_DIR="/var/backups/plane"
STAMP="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
DB_SERVICE="plane-db"
# Эти переменные лучше загрузить из отдельного root-only файла
source /root/.config/plane-backup/restic.env
mkdir -p "$BACKUP_DIR/$STAMP"
# Создаём логический дамп PostgreSQL внутри контейнера
cd "$PLANE_DIR"
docker compose exec -T "$DB_SERVICE" \
pg_dumpall -U postgres | gzip -9 > "$BACKUP_DIR/$STAMP/postgres.sql.gz"
# Сохраняем конфигурацию и Compose-файлы
tar --exclude='.log' -czf "$BACKUP_DIR/$STAMP/config.tar.gz" \
-C "$PLANE_DIR" .env docker-compose.yml docker-compose.yaml 2>/dev/null || true
# Сохраняем Caddyfile
tar -czf "$BACKUP_DIR/$STAMP/caddy.tar.gz" \
-C /etc caddy/Caddyfile
# Отправляем зашифрованные данные во внешнее S3-хранилище
restic backup "$BACKUP_DIR/$STAMP" \
--tag plane \
--host "$(hostname -f)"
# Удаляем локальные временные файлы старше двух дней
find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d -mtime +2 -exec rm -rf {} +
# Удаляем старые удалённые backup по политике хранения
restic forget --tag plane --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
Create a file with the restic parameters. Only root should have access to it.
# Создаём каталог для параметров резервного копирования
sudo install -d -m 700 /root/.config/plane-backup
# Создаём файл с URL S3 и паролем restic
sudo nano /root/.config/plane-backup/restic.env
# Пример содержимого; замените значения на свои
RESTIC_REPOSITORY="s3:https://s3.example.net/plane-backups"
RESTIC_PASSWORD="GENERATE_AND_STORE_A_LONG_RANDOM_PASSWORD"
AWS_ACCESS_KEY_ID="BACKUP_ACCESS_KEY"
AWS_SECRET_ACCESS_KEY="BACKUP_SECRET_KEY"
# Ограничиваем права
sudo chmod 600 /root/.config/plane-backup/restic.env
# Делаем скрипт исполняемым
sudo chmod 700 /usr/local/sbin/plane-backup.sh
# Инициализируем репозиторий один раз
sudo bash -c 'source /root/.config/plane-backup/restic.env && restic init'
Check the backup manually before adding cron.
# Запускаем полный backup и проверяем код возврата
sudo /usr/local/sbin/plane-backup.sh
# Показываем список снимков restic
sudo bash -c 'source /root/.config/plane-backup/restic.env && restic snapshots --tag plane'
cron scheduler
For a small installation, running it once daily at night is sufficient. If a lot of data is created during the workday, reduce the interval and enable a separate database backup. The most important thing is to periodically test restoration, not just verify that files exist.
# Открываем root crontab
sudo crontab -e
# Ежедневно в 02:30 по времени сервера
30 2 /usr/local/sbin/plane-backup.sh >> /var/log/plane-backup.log 2>&1
Restoration testing
Do not test restoration over the only production database. Create a temporary server or a separate Compose project, restore the latest snapshot, import PostgreSQL, and check login, projects, and attachments.
# Проверяем целостность данных restic
sudo bash -c 'source /root/.config/plane-backup/restic.env && restic check'
# Показываем содержимое последнего снимка
sudo bash -c 'source /root/.config/plane-backup/restic.env && restic ls latest --tag plane'
Plane updates
Before updating, read the release notes for the specific version and make a backup. For a small team, a maintenance window is safer: stop writes, update the images, wait for migrations, and check the main workflows.
# Создаём backup перед обновлением
sudo /usr/local/sbin/plane-backup.sh
# Загружаем новые образы и пересоздаём контейнеры
cd /home/deploy/plane
docker compose pull
docker compose up -d
# Проверяем миграции и состояние сервисов
docker compose ps
docker compose logs --tail=200
Do not use docker system prune -a thoughtlessly: the command may remove images required for a quick rollback. Do not remove volumes until the availability of an external backup and the restoration process have been confirmed.
9. Troubleshooting и FAQ
Why does the domain open with a 502 Bad Gateway error?
First, check that Caddy is running: systemctl status caddy. Then make sure Plane is actually listening on the local port by running curl -I http://127.0.0.1:8080. If there is no response, run docker compose ps and docker compose logs --tail=200 in the Plane directory. A common cause is an incorrect service name in the override file, a stopped container, or Caddy pointing to a port that is not published on the host.
Caddy is not issuing a certificate. What should I check?
Check the A record with dig +short plane.example.com and compare the address with the server’s public IP. Ports 80 and 443 must be allowed in UFW, the external firewall, and the provider’s security group. If a CDN proxy is enabled, make sure it is not blocking the HTTP check, or temporarily use a DNS configuration without proxying. The detailed reason is available in journalctl -u caddy -e.
Plane containers keep restarting. What should I do?
Check the status and exit code: docker compose ps, then the logs of the specific container: docker compose logs --tail=300 SERVICE. Check available RAM and disk space with free -h and df -h. Typical causes include insufficient memory, incorrect secrets, an unavailable database, missing required variables, or a damaged volume. Do not delete volumes before analyzing the logs and checking the backup.
What is the minimum suitable VPS configuration?
For an evaluation installation, you can start with 2 vCPU, 4 GB of RAM, and a 40 GB SSD, but this leaves little capacity for Docker, the database, and updates. A practical minimum production configuration is 4 vCPU, 8 GB of RAM, and an SSD of at least 80 GB. If large attachments are planned, choose the disk size based on projected data growth and store the backup separately. For stable operation, a fast SSD and guaranteed resources are more important than a large number of virtual cores.
Which should you choose for this task—VPS or dedicated?
For a single Plane instance and a small team, a VPS is usually sufficient. A dedicated server is needed when the server also handles heavy CI tasks, GitLab, databases, monitoring, and other applications, or when complete resource isolation is required. The decision is best made based on measurements: if RAM is constantly running out, swap is being used, and the disk is experiencing high I/O load, you can first increase the VPS size and then consider a dedicated server.
Can Caddy be omitted in favor of the built-in reverse proxy?
Yes, if the official Plane deployment already provides a supported proxy container and a correct TLS configuration. An external Caddy is convenient because it separates certificate management from the application and allows internal services not to be published. Do not occupy port 80 with multiple proxies at the same time: choose one external entry layer. In any case, only ports 80 and 443 should be open, while databases and queues should remain on the internal Docker network.
Where can I find the cause of a login error or a task that is not working?
Check the frontend, API, and background worker logs, not just Caddy. The exact service names are shown by the docker compose config --services command. Then use docker compose logs --since=15m SERVICE. If the error is related to email, check the SMTP variables and the availability of the sending server. If attachments are not uploading, check the object storage settings and available disk space.
What should I do if the disk is full?
First, identify the source: df -h, du -xhd1 /var/lib/docker, and docker system df. Check the size of uploads, PostgreSQL, and logs. Old local backups may be deleted only after confirming that the external copy is available. Docker logs should be limited through supported Compose parameters or daemon configuration. Do not delete volumes or use aggressive prune without understanding what data is stored in each volume.
How can I update Plane safely without downtime?
Completely zero-downtime updates depend on the specific Plane version and database schema, so for a single VPS it is better to use a short maintenance window. Make an external backup, check the release notes, download the new images, run docker compose up -d, and check the migrations. To minimize downtime, you can download the images in advance with docker compose pull. Before updating, save the current image tags and Compose files for rollback.
10. Conclusions and next steps
As a result, Plane runs on its own VPS in Docker, is available over HTTPS through Caddy, and the server is protected with SSH keys, UFW, and fail2ban. The configuration, PostgreSQL, and user files are included in an encrypted external backup with regular restoration testing.
Next, it is useful to enable monitoring of CPU, RAM, disk space, and backup expiration, then move object storage to a separate S3-compatible service as the number of attachments grows. As the team expands, you can add SMTP, SSO, a separate database server, or scale the VPS after analyzing actual resource consumption.