SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Self-host Dify on a VPS with Docker Compose

Fifteen containers, 4 GiB of RAM minimum. Change the default passwords in .env before first boot, then claim the admin account at /install before anyone else.

What Dify is, and what you are signing up to run

Dify is a self-hostable platform for building applications on top of large language models. You get a web interface for designing chat apps, agents, and retrieval pipelines, an API to call them from your own code, and one place to manage prompts, datasets, and model keys. It is the kind of tool a small team stands up so everyone builds on one shared private base, rather than scattering API keys across scripts. If words like agent, tool call, and retrieval pipeline are still fuzzy, working through those concepts from scratch first will make Dify's builder screens read as familiar controls rather than a wall of unlabelled switches.

Running it yourself means running a lot of moving parts. As of October 2026, Dify's documented default stack starts 15 long-running containers plus a one-time task that sets file permissions and exits. Seven are Dify's own services: the API server, a websocket server, a background worker, a scheduler, the web frontend, a plugin daemon, and an agent backend. The other eight are dependencies: a Postgres database, a Redis cache, a Weaviate vector database, an nginx front door, two code sandboxes, and two SSRF proxies. Docker Compose wires them all together, but that is a real stack, and Dify's documented minimum is 4 GiB of RAM and two CPU cores. If that VPS is also going to run something else, size it from measured numbers rather than advertised ones, because the real RAM floors of PhotoPrism and Immich sit well above their published minimums, and a photo server sharing the box will starve Dify's database and vector store first. CPU contention bites the same way: a Jellyfin library reskinned as a 90s video store costs almost nothing to serve while it is just browsing artwork, but the moment someone starts a transcode, Dify's worker queues behind it. The consolation is that Dify's container count is fixed however many apps you build on it, which is a gentler shape of bill than OpenBot, where every AI coworker gets its own container and its own browser and each new hire pushes the memory floor up again.

Because Dify holds your model API keys and, often, private documents you have loaded for retrieval, treat the box it runs on as sensitive from the first minute. This guide installs it, then hardens it the way you would harden any service that holds secrets.

Prerequisites

You need a VPS running Ubuntu 24.04 with at least 4 GiB of RAM and two CPU cores, Docker with the Docker Compose plugin installed (Dify requires Compose 2.24.0 or later), and a user with sudo or membership of the docker group. The clone command below also uses git, curl, and jq. If Docker is new to you, the basics of Docker Compose on a VPS covers the install and the core commands this guide assumes. A domain name pointed at the server is worth having, because you will want TLS in front of Dify rather than a bare IP address.

Step 1: Get Dify and its Compose files

Dify keeps its Docker setup in the main repository. Clone the latest release, not the development branch, and move into the docker directory:

git clone --branch "$(curl -s https://api.github.com/repos/langgenius/dify/releases/latest | jq -r .tag_name)" https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .env

If the clone fails with a "Remote branch null" error, the GitHub API lookup returned nothing; pick a version from the releases page and pass it to --branch by hand.

The .env file is the core configuration. Read it before you start anything. The values that matter first are the ones that set passwords and secrets: SECRET_KEY, the Postgres password, and the Redis password. SECRET_KEY ships empty, and if you leave it that way Dify generates a key and keeps it in its storage directory. The passwords are another matter: the example file sets the same difyai123456 for both Postgres and Redis, and fixed, publicly known keys for the code sandbox, the plugin daemon, and Weaviate. Leaving those as shipped is the single most common way a self-hosted Dify ends up compromised. If you would rather set your own secret key, generate one:

openssl rand -base64 42

Paste that into SECRET_KEY, and set a strong, unique value for every password and key field in the file. The Redis password also appears inside CELERY_BROKER_URL, so change it there too, or the worker cannot reach Redis.

Step 2: Start it

Bring the stack up:

docker compose up -d

The first run pulls several images and initialises the database, so give it a minute. Check that the containers are healthy:

docker compose ps

Every long-running service should read running; the one-time permissions task exits once it has done its job. Dify serves its web interface through a bundled nginx container on port 80 by default. On your first visit to http://YOUR_SERVER/install you create the admin account. Do that immediately, before anything else can reach the port, because until that account exists anyone who loads the page can claim it and own your instance.

Step 3: Do not expose it raw. Put TLS and a firewall in front

Here is where most quick installs stop and most incidents begin. Dify's own nginx listens on port 80, in the clear, on every interface, and publishes port 443 as well. You do not want your admin login and your model keys travelling over plain HTTP, and you do not want the internal services reachable from outside.

Lock the box down with a default-deny firewall that allows only SSH and web traffic:

sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Remember that a firewall which only covers IPv4 can leave the same ports open on IPv6, the IPv6 firewall gap that catches so many self-hosters. Confirm both stacks are filtered. Also know that ports Docker publishes are handled by Docker's own firewall rules, not ufw's, and the default stack publishes the plugin daemon's debugging port, 5003, on every interface. Set EXPOSE_PLUGIN_DEBUGGING_PORT in .env to 127.0.0.1:5003 unless you really mean to debug plugins from another machine.

For TLS, the cleanest path is to bind Dify's web port to loopback and run a reverse proxy with a Let's Encrypt certificate in front, so the only thing on the public internet is the proxy speaking HTTPS. Dify's .env lets you change the exposed port; set it to bind on 127.0.0.1 and point your proxy at that. Do the same for EXPOSE_NGINX_SSL_PORT, because Dify's nginx claims host port 443 by default and your proxy needs it. The agent-hardening ideas in running an AI agent safely on a VPS apply here too: keep the moving parts on loopback, expose only what must be public, and let one hardened front door speak TLS. Where a tool's interface is for you alone and never needs a certificate at all, skip the proxy and reach it over an SSH tunnel, the way self-hosting the open-kritt security scanner keeps its dashboard bound to loopback and forwarded to your laptop instead of published. If a whole team needs Dify but the public internet does not, an overlay network scales that idea past one laptop: advertising the server's private subnet to your tailnet lets every approved device reach the builder on a private address while the firewall stays shut to everything but SSH. If you administer this box through a coding agent rather than by hand, settle how much it may do unsupervised before you hand it the keys, because the permission mode you leave Claude Code in decides whether it stops to ask before rewriting .env or restarting the stack. If you end up with one session tailing container logs while another edits the proxy config, those two sessions can pass text to each other on the same box, which beats copying output between terminals every time you restart the stack.

Step 4: Keep it patched

Dify moves quickly, and updates include security fixes. Because you cloned a release tag, updating means checking out the next tag rather than pulling a branch. Back up the database first, then from the docker directory:

docker compose down
git fetch --tags
git checkout NEW_VERSION
docker compose pull
docker compose up -d

Database migrations run automatically on startup, and some of them cannot be reversed, which is why the backup comes first. Read the release notes before every upgrade, not only major ones: Dify says upgrade steps vary between releases, and the 1.17.1 notes (September 2026), for example, tell anyone running the bundled Weaviate with existing data not to jump straight to that version but to step through each minor release. Dify also changes the .env schema between releases, so compare .env.example with your .env after checking out a new tag and carry your changes across, because a new variable you have not set can stop a container from starting.

Step 5: Back up what you cannot regenerate

Two things on a Dify box are irreplaceable: the Postgres database, which holds your apps, users, and settings, and the volume that stores uploaded documents and the vector index. Both live under Docker volumes in the docker directory. Snapshot them on a schedule, and copy the snapshots off the server. Those snapshots carry every model key and uploaded document in a single file, so encrypt them before they leave the box, for the same reason a Vaultwarden backup turns out to be the weak point of an otherwise solid password server. A model API key can be reissued; the app you spent a week building cannot. The same reasoning governs any agent whose state has to outlive the machine it runs on: keeping KiroCrew running as an always-on container comes down to snapshotting the memory and schedules that would otherwise disappear at the next reboot.

When you want the agents you build here to reach past your own datasets and search the live web, pointing them at a self-hosted SearXNG instance keeps the query stream on hardware you control, though it is worth reading up on the prompt injection surface that opens before you switch it on. For a more autonomous, code-running agent, see self-hosting Agent Zero, and building your own AI agent on a VPS covers the foundations beneath all of them.

FAQ

What are the system requirements to self-host Dify?

Dify documents a minimum of 4 GiB of RAM and two CPU cores, plus Docker Compose 2.24.0 or later. That floor reflects the size of the default stack: 15 long-running containers plus a one-time initialisation task, as of October 2026. Add enough disk for your uploaded documents and the vector index, and more memory if anything else shares the box.

Is it safe to expose Dify directly on port 80?

No. Dify's bundled web server listens on plain HTTP, and it fronts your admin login and your model API keys. Put a reverse proxy with a Let's Encrypt certificate in front, bind Dify's own port to loopback, and let only the HTTPS proxy face the internet. Pair that with a default-deny firewall that covers both IPv4 and IPv6.

How do I update a self-hosted Dify?

Back up the database, then from the docker directory run docker compose down, git fetch --tags, git checkout the new release tag, docker compose pull, and docker compose up -d. Read the release notes first, because upgrade steps vary between releases, migrations can be irreversible, and Dify sometimes adds new .env variables, and a missing one can stop a container from coming up.

What is the first thing to do after installing Dify?

Visit /install and create the admin account immediately. Until that account exists, anyone who can reach the page can claim it. Set it up the moment the containers are healthy, and before you open the firewall to the world.