Use case · LLM flow builder

Langflow self hosted on a dedicated server: Compose, PostgreSQL, TLS and backups

To run Langflow self hosted, start the pinned langflowai/langflow:1.13.0 image beside PostgreSQL under Docker Compose, publish it on 127.0.0.1:7860 only, keep the superuser password and LANGFLOW_SECRET_KEY in a mode-600 .env generated on the server, and add a TLS proxy only after a sign-in through an SSH tunnel works. The documented floor is a dual-core CPU with 2 GB of RAM. The official images carry no PyTorch: model calls go out to a provider API or to a local engine such as Ollama.

See the machines

What Langflow needs from the machine

Langflow is one Python web application: editor, REST API and flows run in one process. It ships three image profiles; all run as uid 1000, none installs PyTorch, and all can use an external PostgreSQL. The default langflowai/langflow, used here, carries the same components as uv pip install langflow.

ItemFigureScope and source
Langflow, minimumDual-core CPU, 2 GB RAMPython package install page, Langflow docs
Langflow, recommendedMulti-core CPU, 4 GB RAM or moreSame page
Imagelangflowai/langflow:1.13.0, amd64 and arm64Release tag on Docker Hub
DatabasePostgreSQL 16, postgres:16-trixiePinned in the repository's docker_example; the docs recommend PostgreSQL over SQLite for production
Docker Engine28 or laterOur requirement: older engines let hosts on the same segment reach ports published on 127.0.0.1 (moby#45610)

The docs publish no figure per user or per flow. Size memory and disk by what your flows hold: uploaded files and logs in the config directory, rows in PostgreSQL, any vector store you keep on the box, and a local model if you run one.

Which machine, and why a whole one

If every model is a hosted API, Langflow plus PostgreSQL is a CPU job, and any single-socket rack server in the dedicated server range is above the documented floor: the smallest has 6 cores and 16 GB. Each racked single-, dual- and quad-socket server has an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC), which still shows the screen after a bad ufw rule drops SSH. Nothing else runs on it: the cores, the memory, the disks and root are all yours, with no hypervisor underneath. Ports marked unmetered carry no transfer allowance, so pulling images and model files adds no line to the bill.

Models kept on your own hardware need a card: the GPU servers list every add-in card we fit, each priced as its own line beside the host. Delivery: about 30 minutes for a racked server once its payment is approved, 4 to 24 hours for a build to order. Those times are for the server lines; ask in chat how long a particular card takes.

Langflow executes code: plan for it

The project's security page calls the editor "an IDE and code execution platform": its code editor runs Python with full access to the backend process, its filesystem and its network, and one Langflow process isolates no user from another. Four things follow for a server you expose.

  • Treat a Langflow account as a shell inside the container, with its network access. That is our reading of the docs.
  • LANGFLOW_ENABLE_SIGNUP defaults to True: anyone can register through POST /api/v1/users/. New accounts stay inactive until a superuser activates them, but the setup below turns sign-up off outright.
  • In multi-user mode the docs warn that any active non-admin user can author component code and have the server run it. LANGFLOW_CUSTOM_COMPONENT_ADMIN_ONLY=true keeps authoring to superusers; LANGFLOW_ALLOW_CUSTOM_COMPONENTS=false blocks it for everyone.
  • Customers who must not see each other need their own process, disk, network and database, per the docs. One shared instance is not a multi-tenant product.

Setting it up

Ubuntu 24.04, as root (sudo -i), with Docker (Engine 28 or newer, Compose plugin included) taken from the apt repository Docker maintains. ufw goes on first, and IPV6=yes is set before any rule because ufw writes IPv6 copies only of rules added while it is set. The block stops on an engine older than 28 and refuses to run over an existing .env, so pasting it twice never replaces the keys. All secrets come from openssl or Python on the server itself, and none is typed on a command line.

sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
v=$(docker version --format '{{.Server.Version}}' 2>/dev/null)
if [ -z "$v" ] || [ "$(echo "$v" | cut -d. -f1)" -lt 28 ]; then
  echo "STOP: install Docker Engine 28 or later first"
elif [ -e /opt/langflow/.env ]; then
  echo "STOP: /opt/langflow/.env exists; its keys are left alone"
else
  install -d -m 700 /opt/langflow
  install -d -m 700 -o 1000 -g 0 /opt/langflow/langflow-data
  pg=$(openssl rand -hex 24) && su=$(openssl rand -hex 16) \
  && sk=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))') \
  && [ -n "$pg" ] && [ -n "$su" ] && [ -n "$sk" ] \
  && install -m 600 /dev/null /opt/langflow/.env \
  && cat > /opt/langflow/.env <<EOF && echo "env ok"
POSTGRES_USER=langflow
POSTGRES_PASSWORD=$pg
POSTGRES_DB=langflow
LANGFLOW_DATABASE_URL=postgresql://langflow:$pg@postgres:5432/langflow
LANGFLOW_CONFIG_DIR=/app/langflow
LANGFLOW_AUTO_LOGIN=False
LANGFLOW_SUPERUSER=langflow
LANGFLOW_SUPERUSER_PASSWORD=$su
LANGFLOW_SECRET_KEY=$sk
LANGFLOW_ENABLE_SIGNUP=False
LANGFLOW_NEW_USER_IS_ACTIVE=False
LANGFLOW_ENABLE_SUPERUSER_CLI=False
LANGFLOW_CUSTOM_COMPONENT_ADMIN_ONLY=true
EOF
fi

LANGFLOW_SECRET_KEY encrypts stored credentials such as provider API keys, and the docs generate it with the same secrets.token_urlsafe(32) call. Without the original key Langflow cannot decrypt them, so .env belongs in every backup. Anyone allowed to run docker inspect on the box can read these values from the container's environment.

The Compose file

Langflow stores its database, secret key and uploads inside the container unless told otherwise, and the docs warn that recreating such a container deletes them. Here the database lives in PostgreSQL on /opt/langflow/pg-data, and LANGFLOW_CONFIG_DIR points at /opt/langflow/langflow-data, owned by uid 1000 as the docs ask, so a new image replaces nothing but the program.

cat > /opt/langflow/compose.yaml <<'EOF'
services:
  langflow:
    image: langflowai/langflow:1.13.0
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:7860:7860"
    volumes:
      - ./langflow-data:/app/langflow
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      postgres:
        condition: service_healthy
  postgres:
    image: postgres:16-trixie
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - ./pg-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      retries: 30
EOF
chmod 600 /opt/langflow/compose.yaml

A port mapping with no address is published on 0.0.0.0 and [::]. Published traffic skips ufw's rules on IPv4, and on IPv6 when Docker's own IPv6 networking is on; publishing on 127.0.0.1 keeps it local on Engine 28+. Our changes to the repository's docker_example/docker-compose.yml, all of them:

  • Image pinned to 1.13.0 in place of latest with pull_policy: always.
  • Langflow published on 127.0.0.1:7860 in place of 7860 on every address.
  • PostgreSQL's 5432:5432 mapping removed; only Langflow reaches it, over the Compose network.
  • Database credentials read from .env in place of the fixed langflow/langflow.
  • Langflow's environment: block, including its ${LANGFLOW_SUPERUSER_PASSWORD:?...} guard, replaced by env_file: .env. That file also brings the superuser name, the secret key, auto-login off, sign-up off, new users inactive, the superuser CLI off and admin-only custom components. The guard's job is done by the setup block, which writes a password before anything starts.
  • Named volumes replaced by the two bind mounts, and the top-level volumes: key dropped with them.
  • Added restart: unless-stopped, a pg_isready health check that Langflow waits for, and the host.docker.internal mapping used for Ollama below.

First sign-in, then TLS

  1. Start it: docker compose -f /opt/langflow/compose.yaml up -d. Run docker compose -f /opt/langflow/compose.yaml ps until PostgreSQL reports healthy and Langflow is up, and confirm that ss -ltnp | grep 7860 lists 127.0.0.1:7860 and nothing else.
  2. From your workstation, through your own login: ssh -N -L 7860:127.0.0.1:7860 you@your-server. Open http://localhost:7860/login and sign in as langflow with the password that grep ^LANGFLOW_SUPERUSER_PASSWORD /opt/langflow/.env prints on the server.
  3. Point a DNS record at the server, then put Caddy in front. The caddy package ships with Ubuntu 24.04; older releases need Caddy's own repository. Open the web ports first with ufw allow 80,443/tcp: Caddy is a host process, so without that rule ufw blocks it on IPv4 and IPv6 alike, and its first certificate request would fail. Then run apt install -y caddy, write the site with printf 'flows.example.com {\n reverse_proxy 127.0.0.1:7860\n}\n' > /etc/caddy/Caddyfile and reload it: systemctl reload caddy.
  4. Narrow CORS. The docs call the default (* with credentials allowed) a risk in production: add LANGFLOW_CORS_ORIGINS=https://flows.example.com to .env and recreate the container: docker compose -f /opt/langflow/compose.yaml up -d --force-recreate langflow.

As of 1.12 the open-source edition has no admin page. Further accounts are created, activated and given passwords by a superuser through the Users API under /api/v1/users.

Backups, restores and upgrades

A complete set is the PostgreSQL dump plus .env, compose.yaml and the config directory with its uploads. The script dumps the database while Langflow runs and checks that the dump can be read back. It then stops Langflow while the archive is written, starts it again, and tests the archive. Two-week-old copies are deleted only after both checks pass, because set -e ends the script at the first failure. Every copy holds the secret key: keep it private and copy it off the machine yourself.

install -d -m 700 /var/backups/langflow
cat > /usr/local/sbin/langflow-backup.sh <<'EOF'
set -euo pipefail
umask 077
d=/var/backups/langflow; t=$(date +%F)
dc="docker compose -f /opt/langflow/compose.yaml"
$dc exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -Fc "$POSTGRES_DB"' > "$d/db-$t.dump"
$dc exec -T postgres pg_restore -l < "$d/db-$t.dump" > /dev/null
$dc stop langflow
rc=0
tar -czf "$d/files-$t.tgz" -C /opt/langflow .env compose.yaml langflow-data || rc=$?
$dc start langflow
[ "$rc" -eq 0 ]
tar -tzf "$d/files-$t.tgz" > /dev/null
find "$d" -name 'db-*.dump' -mtime +14 -delete
find "$d" -name 'files-*.tgz' -mtime +14 -delete
EOF
printf '30 3 * * * root /bin/bash /usr/local/sbin/langflow-backup.sh >> /var/backups/langflow/backup.log 2>&1\n' > /etc/cron.d/langflow-backup

To restore, as root, on this box or a fresh one prepared with Docker 28+ and ufw as above:

  1. Check the pair you will use: tar -tzf files-DATE.tgz > /dev/null && [ -s db-DATE.dump ] && echo ok must print ok.
  2. Only if /opt/langflow exists: stop it with docker compose -f /opt/langflow/compose.yaml down, then [ -e /opt/langflow.previous ] && echo STOP || mv /opt/langflow /opt/langflow.previous. The current database and files stay intact as your rollback. On a fresh box, skip this step.
  3. if [ -e /opt/langflow ]; then echo 'STOP: /opt/langflow still exists'; else install -d -m 700 /opt/langflow && tar -xzf files-DATE.tgz -C /opt/langflow && chown -R 1000:0 /opt/langflow/langflow-data && echo restored; fi puts back .env, the Compose file and the config directory before PostgreSQL ever starts. It refuses to unpack over a live directory, and the chown matters on a fresh box, where tar maps owners by name.
  4. Start the database alone: docker compose -f /opt/langflow/compose.yaml up -d postgres. Once ps shows it healthy, load the dump with docker compose -f /opt/langflow/compose.yaml exec -T postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --no-owner' < db-DATE.dump.
  5. Run docker compose -f /opt/langflow/compose.yaml up -d, sign in through the tunnel and check flows and credentials. Delete /opt/langflow.previous only then. To roll back instead: down, move the restored directory aside, move .previous back, up -d.

To upgrade, take a backup, change the tag in compose.yaml, then docker compose -f /opt/langflow/compose.yaml pull and up -d. The docs warn that a new version can run database migrations at start-up and advise testing it against a copy of production data first. A migrated database does not go back, so the rollback is the restore above with the old tag.

Local models through Ollama

Install Ollama on the host as our Ollama server page shows. Its Langflow component defaults to http://127.0.0.1:11434, which points back at Langflow's own container. Ollama checks no credentials, so the firewall rule comes first: ufw allow from 172.16.0.0/12 to any port 11434 proto tcp admits only Docker's private bridge range, and with IPV6=yes ufw's default deny keeps 11434 closed to everyone else on IPv4 and IPv6. Then set OLLAMA_HOST=0.0.0.0:11434 in Ollama's service environment. Langflow 1.13 validates connector hosts against SSRF by default and blocks private addresses other than loopback, so name the host: append LANGFLOW_SSRF_ALLOWED_HOSTS=host.docker.internal to /opt/langflow/.env, then recreate Langflow with docker compose -f /opt/langflow/compose.yaml up -d --force-recreate langflow. Only then set the Ollama component's base URL to http://host.docker.internal:11434.

For a bundled RAG pipeline and app front ends, see Dify; for moving data between SaaS tools, n8n.

Licence and limits

Langflow is MIT-licensed. Our general reading, not legal advice: you may use, change and sell services built on it, and anyone who distributes copies must keep the copyright notice and licence text with them. MIT asks for no source code in return.

  • One host is one failure domain. The docs' production layout runs replicas on Kubernetes with external PostgreSQL and shared storage; this page is the single-box version.
  • LANGFLOW_DEPLOYMENT_PROFILE=prod refuses to boot without external object storage, an environment-set secret key, PostgreSQL and pgvector. This install keeps the default dev profile.
  • Knowledge and memory bases default to local SQLite storage in 1.13, which the docs scope to one host with one worker; several workers or replicas need a shared pgVector or OpenSearch store.

Questions

Does a self-hosted Langflow need a GPU?

No. The official images ship without PyTorch, and the documented floor is a dual-core CPU with 2 GB of RAM. A GPU matters only when the models run on the same machine, for example under Ollama.

Where does the Docker image keep my flows?

Without LANGFLOW_DATABASE_URL or LANGFLOW_SAVE_DB_IN_CONFIG_DIR=true, in a SQLite file inside the container. Without LANGFLOW_CONFIG_DIR on a mounted path, the secret key and uploads sit there too, and the docs warn that recreating the container deletes them. The setup here stores flows in PostgreSQL and mounts the config directory from the host.

Should I run the latest tag?

Not in production. The docs point out that moving tags can switch to a new release without any change to your configuration, and a new version can migrate the database when it starts. Pin a version tag, back up, then change it on purpose.

How do colleagues get accounts?

A superuser creates and activates them through the Users API; the open-source edition has had no admin page since 1.12. With LANGFLOW_ENABLE_SIGNUP=False, only superusers can create accounts. Each account can build flows, so give one only to someone you would trust with code on the server.

Run Langflow on hardware nobody shares

A single-socket rack server carries the editor and PostgreSQL; a GPU build keeps inference beside the flows.

See the machines

Related on Server Room

Other use cases