bolt Valebyte VPS from $4/mo — NVMe, 60s deploy.

Get a VPS arrow_forward
eco Beginner Tutorial/How-to

Installing Kestra on a VPS: Task Orchestration, Docker, PostgreSQL, SSL, and Scheduling

calendar_month Oct 26, 2026 schedule 18 min read visibility 31 views
Установка Kestra на VPS: оркестрация задач, Docker, PostgreSQL, SSL и расписания
info

Need a server for this guide? We offer dedicated servers and VPS in 50+ countries with instant setup.

Need a server for this guide?

Deploy a VPS or dedicated server in minutes.

Installing Kestra on a VPS: task orchestration, Docker, PostgreSQL, SSL, and schedules

TL;DR

Kestra is a self-hosted platform for task orchestration, ETL pipelines, API automation, and scheduling. In this guide, you will deploy Kestra on an Ubuntu VPS via Docker Compose, connect PostgreSQL, secure the interface with the HTTPS proxy Caddy, create your first workflow with a cron schedule, and configure backups.

  • A VPS with 2 vCPU, 4 GB RAM, and 40–60 GB NVMe is sufficient for a small production instance.
  • Kestra and PostgreSQL will run in separate Docker containers.
  • Caddy will automatically issue and renew a Let’s Encrypt TLS certificate.
  • All passwords will be stored in the .env file rather than in Docker Compose or the workflow.
  • The first workflow will be started manually and according to a cron schedule.
  • PostgreSQL and configuration backups will run via cron and be sent to external storage.

What we are configuring and why

Kestra is an orchestration platform in which tasks are described using declarative YAML files. It is suitable for running ETL processes, processing files, synchronizing SaaS services, calling APIs, generating regular reports, maintaining infrastructure, running Python scripts and SQL queries, and executing container jobs.

Unlike simple cron, Kestra stores run history, parameters, logs, task statuses, errors, and retries. The web interface allows you to start workflows manually, pass input data, view execution step by step, and restart failed processes.

After completing the instructions, the following components will be running on the server:

  • Kestra Server with a web interface and API;
  • PostgreSQL 16 as a reliable database for workflows, queues, execution history, and metadata;
  • Docker Compose for managing services;
  • Caddy as a reverse proxy with automatic HTTPS;
  • UFW Firewall, SSH keys, and Fail2ban for basic VPS protection;
  • regular backups of PostgreSQL, configuration, and Kestra persistent data;
  • an example workflow with manual execution and a cron schedule.

What you can automate with Kestra

Scenario Workflow example Run frequency
ETL and analytics Fetch a CSV from S3, clean the data, and load it into PostgreSQL Every hour or daily
DevOps automation Check backups, collect metrics, and send a notification Every 15 minutes
SaaS integrations Receive leads from a form and create a record in the CRM Via webhook or every 5 minutes
File processing Convert uploaded PDFs, extract text, and save the result On event
Reports Run SQL, compile a report, and send it to Telegram or email Weekdays at 09:00

Cloud-managed or self-hosted Kestra

The managed version relieves the team of update, data storage, and scaling tasks, but adds recurring costs and dependence on an external platform. Self-hosted Kestra on a VPS requires initial setup, but allows you to control data storage locations, limits, network rules, plugins, outbound IP addresses, and costs.

The self-hosted option is particularly convenient when workflows work with internal databases, private APIs, private Git repositories, or sensitive documents. The server can be located in the required jurisdiction, connected to a VPN network, and access to the panel can be restricted by IP or additional HTTP authentication.

Do not use Kestra’s built-in H2 database for production deployment. It is suitable for local development and testing, but for a VPS with real schedules, logs, and multiple workflows, use PostgreSQL.

What VPS configuration is needed for this task

Kestra’s workload depends not so much on the number of YAML files as on the nature of the tasks being executed. If workflows only call APIs and run SQL queries, the requirements are moderate. If they run Python file processing, Docker containers, browser automation, or transformations of large tables, additional CPU, RAM, and a fast disk are required.

Profile CPU RAM Disk Suitable for
Testing 1 vCPU 2 GB 25 GB NVMe Learning the interface, 1–3 lightweight workflows
Minimum production 2 vCPU 4 GB 40–60 GB NVMe Up to dozens of API, SQL, and cron tasks per day
Standard 4 vCPU 8 GB 100 GB NVMe A team, parallel workflows, large logs
Heavy computations 8 vCPU+ 16–32 GB+ 200 GB NVMe+ Python, Docker jobs, large files, ML, and ETL

A practical starting option is 2 vCPU, 4 GB RAM, 60 GB NVMe, and a connection of at least 100 Mbps. Such a server can usually handle Kestra itself, PostgreSQL, Caddy, basic system services, and a moderate number of simultaneously running tasks. If you plan to run workflows with Docker containers, start with 4 vCPU and 8 GB RAM.

For this scenario, you can choose a VPS with the specified characteristics, install Ubuntu Server 24.04 LTS, and immediately configure a DNS record for the future Kestra domain.

When you need a dedicated server

A dedicated server is needed not for the Kestra interface itself, but for heavy workloads. Move to a dedicated server if you regularly process tens of gigabytes of files, run many containers in parallel, require guaranteed CPU resources, high IOPS, or store a large volume of execution logs locally.

A dedicated server also makes sense if Kestra runs internal CI processes, Playwright browsers, video conversion, machine learning models, or large-scale Spark tasks. For ordinary API, SQL, and backup automation, a VPS is simpler, cheaper, and easier to scale vertically.

How to choose a location

Location affects latency to the database, external APIs, and web interface users. If Kestra connects to a database in a European data center, choose a VPS nearby. If tasks work with local APIs or a corporate network, proximity to the VPN gateway and stable routing are more important.

For the TLS certificate and public webhooks, something else matters more: the domain name must point to the server’s public IP, and ports 80 and 443 must be accessible from the internet. Before installing Caddy, create an A record, for example kestra.example.com, pointing to the VPS IP.

Server preparation

The instructions below assume a clean server with Ubuntu Server 24.04 LTS and SSH access as the root user. Perform the initial steps from a local terminal. Replace SERVER_IP with your VPS’s external address.

Creating an administrator and configuring an SSH key

First, create a separate user. Do not run Docker Compose or work with Kestra as root unless necessary. The username in this example is ops.

ssh root@SERVER_IP
adduser ops
usermod -aG sudo ops

Add a public SSH key for the new user. Run the command below on the local computer where the ~/.ssh/id_ed25519.pub key already exists.

ssh-copy-id ops@SERVER_IP
ssh ops@SERVER_IP

Verify that login with the new user works, and only then disable root login and password authentication. Open a second SSH session so you do not lose access if there is a configuration error.

sudo nano /etc/ssh/sshd_config.d/99-hardening.conf
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
X11Forwarding no
AllowUsers ops

Check the configuration syntax and restart the SSH service.

sudo sshd -t && sudo systemctl restart ssh

Updating the system and installing basic utilities

Update the packages to the latest state. For a production server, it is useful to enable automatic security updates, but Docker and Kestra updates are best performed separately during a controlled maintenance window.

sudo apt update && sudo apt full-upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release \
  ufw fail2ban unattended-upgrades jq htop vim cron

Check the kernel version and whether a reboot is required. If the /var/run/reboot-required file exists, restart the VPS during a suitable maintenance window.

test -f /var/run/reboot-required && cat /var/run/reboot-required
sudo reboot

UFW Firewall and Fail2ban

Only SSH, HTTP, and HTTPS should be open on the server. PostgreSQL on port 5432 is not exposed externally: Kestra accesses it through the internal Docker network.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Fail2ban will block IP addresses after repeated failed SSH login attempts. Create a minimal local jail configuration.

sudo tee /etc/fail2ban/jail.d/sshd.local > /dev/null <<'EOF'
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

If you use a non-standard SSH port, first allow it in UFW and verify the new login, and only then close the old port. Do not change the SSH port as your only security measure: keys, disabling root login, and Fail2ban are more important.

Software Installation — Step by Step

For deployment, we use Docker Engine 27+ and Docker Compose Plugin v2. Kestra will be pinned to a specific image tag rather than latest. This protects against accidental updates when restarting containers.

Installing Docker Engine and Docker Compose

Add the official Docker key and repository for Ubuntu. Run the commands as the ops user via sudo.

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

Add the repository, update the package index, and install Docker Engine, CLI, and Compose Plugin.

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 the user to the Docker group. After running the command, exit the SSH session and reconnect: group membership applies only to a new session.

sudo usermod -aG docker ops
exit

After logging in again, verify Docker. The hello-world container should exit without errors.

docker --version
docker compose version
docker run --rm hello-world

The docker group effectively grants users privileges comparable to root. Add only trusted administrators to it. For a small installation, this is more convenient than running every command via sudo.

Installing Caddy

Caddy will accept traffic on ports 80 and 443, automatically obtain a Let’s Encrypt certificate, and forward requests to Kestra on localhost. Install Caddy as a package from the official repository.

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
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
  sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy

Verify that Caddy is running. Until the configuration is changed, it will not proxy Kestra.

sudo systemctl enable --now caddy
sudo systemctl status caddy --no-pager

Creating the Directory Structure

Keep the project configuration in a single directory. The example uses /opt/kestra. Persistent data will be stored in Docker named volumes, while configuration files, workflow exports, and backups will be stored on the host.

sudo mkdir -p /opt/kestra/{config,backups,flows}
sudo chown -R ops:ops /opt/kestra
cd /opt/kestra
pwd

As of 2026, before a production installation, always verify the current stable tag in the Kestra image registry and the project changelog. The example uses the pinned tag v0.24.0 as a template. Do not replace the tag with latest; instead, update it deliberately after creating a backup and reviewing the release notes.

Configuring Kestra, PostgreSQL, and SSL

Preparing Secrets

Generate separate, long passwords for PostgreSQL and Caddy basic HTTP authentication. Do not add the .env file to Git or send it through messengers. Permissions 600 prevent other server users from reading the file.

cd /opt/kestra
POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d '\n')
CADDY_PASSWORD=$(openssl rand -base64 24 | tr -d '\n')

cat > .env <<EOF
POSTGRES_DB=kestra
POSTGRES_USER=kestra
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
KESTRA_DOMAIN=kestra.example.com
CADDY_USER=admin
CADDY_PASSWORD=${CADDY_PASSWORD}
EOF

chmod 600 .env

Replace kestra.example.com with your own domain. Before starting Caddy, check DNS: the command should return your VPS’s public IP.

source /opt/kestra/.env
getent ahostsv4 "$KESTRA_DOMAIN"
curl -4 ifconfig.me ; echo

Kestra Configuration

Create the config/application.yml file. In it, Kestra uses PostgreSQL as the repository and queue backend. Local storage is suitable for a single Kestra node. When moving to multiple servers, storage should be migrated to S3-compatible object storage.

cat > /opt/kestra/config/application.yml <<'EOF'
datasources:
  postgres:
    url: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
    driverClassName: org.postgresql.Driver
    username: ${POSTGRES_USER}
    password: ${POSTGRES_PASSWORD}

kestra:
  repository:
    type: postgres
  queue:
    type: postgres
  storage:
    type: local
    local:
      base-path: /app/storage
  tasks:
    tmp-dir:
      path: /tmp/kestra-wd/tmp
  url: https://${KESTRA_DOMAIN}/
  server:
    basic-auth:
      enabled: false

logger:
  levels:
    io.kestra: INFO
EOF

Kestra basic authentication is disabled because external access will be protected by Caddy. This simplifies browser and API usage, but means that Caddy must remain the only entry point. The Kestra port is not exposed externally.

Docker Compose for Kestra and PostgreSQL

Create compose.yml. PostgreSQL has no ports section, so it is accessible only to containers on the internal Docker network. Kestra is published on 127.0.0.1:8080, allowing Caddy on the same server to access it while keeping it inaccessible from the internet.

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

  kestra:
    image: kestra/kestra:v0.24.0
    container_name: kestra
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      KESTRA_DOMAIN: ${KESTRA_DOMAIN}
      JAVA_OPTS: "-Xms512m -Xmx1536m"
    command: server standalone --config /etc/kestra/application.yml
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - ./config/application.yml:/etc/kestra/application.yml:ro
      - kestra_storage:/app/storage
      - kestra_workdir:/tmp/kestra-wd
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health | grep -q UP"]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 60s
    networks:
      - kestra_internal

networks:
  kestra_internal:
    name: kestra_internal

volumes:
  postgres_data:
  kestra_storage:
  kestra_workdir:
EOF

The JAVA_OPTS parameter limits the Java heap to 1.5 GB. On a VPS with 4 GB of RAM, this is a reasonable starting point: the remaining memory is available to PostgreSQL, the OS, and the file cache. On a server with 8 GB of RAM, you can increase -Xmx to 3–4 GB after monitoring actual memory usage.

Start the services in the background and check their status. The first startup may take several minutes: Kestra creates tables and applies migrations in PostgreSQL.

cd /opt/kestra
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 kestra

Check the local endpoint before configuring the proxy. The response may be JSON or an HTTP redirect, depending on the Kestra version, but the code must not be 502 or Connection refused.

curl -I http://127.0.0.1:8080/
docker inspect --format='{{.State.Health.Status}}' kestra
docker inspect --format='{{.State.Health.Status}}' kestra-postgres

Configuring Caddy and HTTPS

First, create a bcrypt password hash for Caddy. The command will prompt for the password you saved in .env. Do not enter the password on the command line: it will remain in the shell history.

source /opt/kestra/.env
read -rsp "Password for Caddy user ${CADDY_USER}: " INPUT_PASSWORD; echo
CADDY_HASH=$(printf '%s' "$INPUT_PASSWORD" | caddy hash-password --stdin)
unset INPUT_PASSWORD
printf '%s\n' "$CADDY_HASH"

Create the Caddyfile. Instead of the variable, manually enter the displayed hash in the configuration at PASTE_BCRYPT_HASH_HERE. This is safer than leaving the password in plain text.

sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
kestra.example.com {
    encode zstd gzip

    basic_auth {
        admin PASTE_BCRYPT_HASH_HERE
    }

    reverse_proxy 127.0.0.1:8080 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }

    log {
        output file /var/log/caddy/kestra-access.log
        format json
    }
}
EOF

Replace the domain and hash in the file, create the log directory, and then validate the syntax. Caddy obtains the certificate automatically after successfully loading the configuration if DNS already points to the server and port 80 is accessible externally.

sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager

Open https://kestra.example.com in a browser. The browser will prompt for the Caddy username and password, after which the Kestra interface will open. Also check TLS from the console.

curl -I https://kestra.example.com
sudo journalctl -u caddy -n 100 --no-pager

First Workflow and Cron Schedule

Open Kestra in a browser, create a namespace, for example company.ops, then go to the Flows section and create a flow. For the first check, insert the following YAML. It writes a message to the logs and runs every 15 minutes.

id: health_report
namespace: company.ops
description: Проверочный workflow с cron-расписанием

triggers:
  - id: every_15_minutes
    type: io.kestra.plugin.core.trigger.Schedule
    cron: "/15    "
    timezone: "Europe/Moscow"

tasks:
  - id: write_log
    type: io.kestra.plugin.core.log.Log
    message: "Kestra работает. Время запуска: {{ execution.startDate }}"

  - id: shell_check
    type: io.kestra.plugin.scripts.shell.Commands
    commands:
      - "date -u"
      - "echo Workflow finished successfully"

Save the workflow, click Execute, and open the Executions tab. A SUCCESS status should appear. Then wait for a time divisible by 15 minutes, or temporarily replace the cron with to test every minute. Restore the normal schedule after testing.

Do not store API tokens, S3 keys, database passwords, and webhook secrets directly in YAML. Use Kestra secrets, environment variables, or an external secret manager. To start, you can create secrets through the interface in the namespace and later connect Vault, SOPS, or a cloud secrets manager.

Backups and Maintenance

Restoring Kestra requires more than just PostgreSQL data. You need to preserve configuration files, the Compose file, Caddyfile, the .env secrets file, and persistent storage. PostgreSQL contains metadata, execution history, settings, and queue data; storage contains files created by tasks and plugins.

What to back up Why How often
PostgreSQL dump Workflow metadata, executions, queues, settings Daily, before updates
Docker volume Kestra storage Task files, attachments, and local artifacts Daily
/opt/kestra Compose, YAML configurations, secrets, scripts After every change and daily
/etc/caddy/Caddyfile HTTPS proxy and panel access After changes

Local backup script

The following script creates a compressed PostgreSQL dump and archives the configuration directory. It keeps the last 14 days locally. A local backup does not protect against VPS deletion, so it must then be sent to S3-compatible storage or a separate server.

cat > /opt/kestra/backup-kestra.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

BASE="/opt/kestra"
DATE=$(date +%F_%H-%M-%S)
DEST="${BASE}/backups/${DATE}"

mkdir -p "${DEST}"

cd "${BASE}"
docker compose exec -T postgres pg_dump \
  -U "${POSTGRES_USER}" \
  -d "${POSTGRES_DB}" \
  --format=custom \
  --file="/tmp/kestra_${DATE}.dump"

docker cp kestra-postgres:"/tmp/kestra_${DATE}.dump" \
  "${DEST}/postgres.dump"

docker compose exec -T postgres rm -f "/tmp/kestra_${DATE}.dump"

tar --exclude='backups' \
  --exclude='.git' \
  -czf "${DEST}/kestra-config.tar.gz" \
  -C /opt kestra

docker run --rm \
  -v kestra_kestra_storage:/source:ro \
  -v "${DEST}":/backup \
  alpine:3.20 \
  tar -czf /backup/kestra-storage.tar.gz -C /source .

find "${BASE}/backups" -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} \;
EOF

chmod 700 /opt/kestra/backup-kestra.sh
cd /opt/kestra && ./backup-kestra.sh
ls -lah /opt/kestra/backups

The Docker volume name is usually formed from the directory name and the volume name. In this case, the project is launched from /opt/kestra, so the volume is named kestra_kestra_storage. If the command returns an error about a nonexistent volume, check the exact name using docker volume ls and correct the script.

Sending the backup to external storage

For real protection, keep copies outside the primary VPS. Practical options include S3-compatible object storage, a separate backup VPS over SSH, a Borg repository, or a Restic repository. Below is an example using Restic and an S3-compatible backend. Place the access variables in a separate file with 600 permissions.

sudo apt install -y restic
sudo tee /etc/kestra-restic.env > /dev/null <<'EOF'
export RESTIC_REPOSITORY="s3:s3.example.net/kestra-backups"
export RESTIC_PASSWORD="CHANGE_TO_A_LONG_UNIQUE_RESTIC_PASSWORD"
export AWS_ACCESS_KEY_ID="CHANGE_ME"
export AWS_SECRET_ACCESS_KEY="CHANGE_ME"
EOF
sudo chmod 600 /etc/kestra-restic.env

Initialize the repository once, then add the data upload to the end of the backup script. Before using it, check the endpoint and bucket settings with the selected S3 provider.

source /etc/kestra-restic.env
restic init
restic backup /opt/kestra/backups
restic snapshots

Add a daily run. Cron runs under the ops user, so this user must have access to Docker and the project files.

crontab -e
15 2    /opt/kestra/backup-kestra.sh >> /opt/kestra/backups/backup.log 2>&1
30 2    . /etc/kestra-restic.env && restic backup /opt/kestra/backups && restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune >> /opt/kestra/backups/restic.log 2>&1

Updating Kestra without data loss

For a single VPS, an update is a short maintenance window, not a true rolling deployment. First create a backup, read the release notes for the selected version, update the image tag in compose.yml, pull the image, and restart only the Kestra container.

cd /opt/kestra
./backup-kestra.sh
docker compose pull kestra
docker compose up -d --no-deps kestra
docker compose logs -f --tail=100 kestra

After the update, check the interface, run a test workflow, verify the schedule, and check database migrations. Do not remove the old image immediately: it may be useful for rollback if the new version is incompatible with a plugin in use. Before updating the PostgreSQL major version, make a separate verified backup and follow the PostgreSQL upgrade procedure rather than simply changing the container tag.

Troubleshooting + FAQ

Why does Caddy fail to obtain a TLS certificate and show an ACME error in the logs?

First check the domain's A record with getent ahostsv4 kestra.example.com: it should return the VPS IP. Then make sure UFW allows TCP ports 80 and 443 and that the provider does not have an additional cloud firewall. Check that another web server has not occupied port 80: sudo ss -ltnp | grep ':80\|:443'. After correcting DNS and the ports, run sudo systemctl reload caddy and inspect journalctl -u caddy.

Why does the browser display 502 Bad Gateway?

A 502 code means that Caddy is available but cannot connect to Kestra. Run docker compose ps in the /opt/kestra directory: the Kestra container should be in a running or healthy state. Then check locally with curl -I http://127.0.0.1:8080/. If there is no response, inspect docker compose logs kestra and docker compose logs postgres. A common cause is that PostgreSQL has not yet passed its healthcheck or that incorrect database variables were specified.

Kestra keeps restarting. What should I check?

Start with docker compose logs --tail=200 kestra. PostgreSQL connection errors usually indicate an incorrect password, database name, or corrupted JDBC configuration. OutOfMemoryError errors mean that the VPS does not have enough RAM or that the -Xmx parameter is too large. Check memory with free -h and docker stats. On a VPS with 2 GB of RAM, it is better not to run PostgreSQL and Kestra with heavy workflows simultaneously.

Why is the workflow not starting on cron?

Check that the flow is saved, enabled, and contains no YAML errors. In a five-field cron expression, /15 * means every 15 minutes. Make sure the timezone is specified explicitly: without it, the time may be interpreted as UTC. Open the Executions section and look for trigger errors. Also check the logs: docker compose logs kestra | grep -i schedule. For testing, temporarily set the schedule to every minute.

What is the minimum suitable VPS configuration?

For getting familiar with Kestra, you can use 1 vCPU, 2 GB of RAM, and 25 GB of NVMe, but this is not a comfortable production configuration. The practical minimum for Kestra, PostgreSQL, Caddy, and several small workflows is 2 vCPUs, 4 GB of RAM, and 40–60 GB of NVMe. If workflows run Python, Chromium, Docker containers, or process files, choose at least 4 vCPUs and 8 GB of RAM.

What should you choose for this task — a VPS or dedicated?

A VPS is sufficient for API automation, SQL tasks, regular reports, webhooks, and most small ETL processes. A dedicated server is needed for constant heavy CPU loads, dozens of parallel container jobs, large volumes of local data, or strict requirements for dedicated resources. It is usually sensible to start with a VPS and monitor docker stats, CPU load, IOPS, and workflow execution time.

Can PostgreSQL be exposed externally for connection from DBeaver?

Technically, yes, but this is usually unnecessary and unsafe for Kestra. PostgreSQL should remain inside the Docker network. For administrative access, use an SSH tunnel: ssh -L 5432:127.0.0.1:5432 ops@SERVER_IP, but this temporarily requires forwarding PostgreSQL only to localhost. A safer option is to run SQL commands through docker compose exec postgres psql directly on the server.

Why does the backup not see the Kestra Docker volume?

First run docker volume ls and find the volume associated with storage. Docker Compose forms the name from the project name and volume name, so it may differ from the example if the project was launched from another directory or with the -p parameter. Correct the volume name in the backup script. Also check the permissions on the /opt/kestra/backups directory and make sure cron runs under a user belonging to the docker group.

Conclusions and next steps

Kestra is now running on the VPS with PostgreSQL, Docker Compose, HTTPS through Caddy, basic server protection, cron schedules, and backups. The instance is ready to run production workflows if you regularly check the logs, available disk space, and backup success.

  1. Create separate namespaces for development and production, and move secrets to Kestra Secrets or an external secret manager.
  2. Set up notifications in Telegram, Slack, or email for executions with the FAILED status.
  3. As the load increases, move storage to S3-compatible storage, PostgreSQL to a separate server or managed database, and isolate worker workloads in separate containers.

Before any Kestra update, create a verifiable backup and manually test critical workflows. This is more important than automatically updating images and allows for a quick rollback in case of plugin incompatibilities or configuration changes.

Was this guide helpful?

Your feedback helps us improve our guides.

Share this post:

Send this guide to someone who may find it useful.

Telegram VKVK WhatsApp Facebook LinkedIn XX

Kestra installation on a VPS: task orchestration, Docker, PostgreSQL, SSL, and scheduling
support_agent
Valebyte Support
Usually replies within minutes
Hi there!
Send us a message and we'll reply as soon as possible.