How to self-host Dify with Docker Compose and HTTPS
Deploy Dify with Docker Compose on Ubuntu or Debian: replace default secrets, keep its Nginx on localhost behind Caddy HTTPS, connect Ollama and plan upgrades.
- Intermediate
- 45 min read
- Updated
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13
On this page
- Prerequisites
- Step 1 — Clone a Dify release
- Step 2 — Create .env and replace the default secrets
- Step 3 — Start the stack
- Step 4 — Publish Dify over HTTPS with Caddy
- Step 5 — Create the admin account
- Step 6 — Connect model providers, including Ollama
- Back up and restore
- Update Dify
- Troubleshooting
- Port 80 or 443 is already in use
- fatal: Remote branch null not found in upstream origin
- 502 Bad Gateway after a restart
- The page loads forever or the console shows CORS errors
- An HTTP Request node cannot reach an internal address
- Next steps
Dify is an open-source platform for building applications on top of large language models: chatbots, agents, knowledge bases with retrieval (RAG) and visual workflows, each with its own API. The official way to self-host it is Docker Compose from the project's repository. The default stack runs the API, background workers, the web front end, a plugin daemon, a code sandbox, an agent backend, PostgreSQL, Redis, the Weaviate vector database and an Nginx gateway.
This guide clones a Dify release into /opt/dify, replaces every default password and key before the first start, keeps the bundled Nginx on a localhost port behind Caddy for HTTPS, protects the first-run admin page with an init password and connects Ollama. It finishes with the backup and upgrade routine.
Prerequisites
- A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13. Dify's documentation supports Linux with Docker 19.03 or later and Docker Compose 2.24.0 or later; install both with Install Docker on Ubuntu or Install Docker on Debian.
- A non-root user with
sudorights and SSH key login, see Secure a new Linux server and Set up SSH keys. - A domain name such as
dify.example.comwith an A (and optionally AAAA) record pointing at the server, and Caddy from Caddy reverse proxy. - A model provider account (OpenAI, Anthropic and others) or Ollama from Install Ollama.
| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | 2 cores | 4 vCPU |
| RAM | 4 GiB | 8 GB |
| Disk | Not published | 40 GB or more for images, uploads and vector data |
The suggested column is a conservative starting point, not a benchmark. Confirm your Compose version before you continue:
docker compose versionStep 1 — Clone a Dify release
Dify's documentation clones the latest release tag, not the development branch. The command asks GitHub's API for the newest tag and therefore needs curl and jq; python3 is used in Step 2 to generate two tokens.
sudo apt update
sudo apt install git curl jq openssl python3
sudo mkdir -p /opt/dify
sudo chown $USER:$USER /opt/dify
git clone --branch "$(curl -s https://api.github.com/repos/langgenius/dify/releases/latest | jq -r .tag_name)" https://github.com/langgenius/dify.git /opt/dify
cd /opt/dify
git describe --tagsgit describe --tags prints the release you checked out, for example 1.17.1. If the clone fails with Remote branch null not found, the API call did not return a tag; pick the current version from the releases page and run git clone --branch 1.17.1 https://github.com/langgenius/dify.git /opt/dify with that tag instead.
Step 2 — Create .env and replace the default secrets
Everything you configure lives in /opt/dify/docker/.env, created from .env.example. Optional, provider-specific settings have their own templates under docker/envs/; values in .env take precedence over them.
cd /opt/dify/docker
cp .env.example .env
chmod 600 .envThe example file ships with publicly known defaults, such as difyai123456 for PostgreSQL and Redis and fixed keys for the sandbox, Weaviate and the plugin daemon. Replace them with random values. Several pairs must match, which is why the command reuses the same shell variable: REDIS_PASSWORD also appears inside CELERY_BROKER_URL, SANDBOX_API_KEY must equal CODE_EXECUTION_API_KEY, and the Weaviate key appears twice.
DB_PW=$(openssl rand -hex 24)
REDIS_PW=$(openssl rand -hex 24)
SANDBOX_KEY=$(openssl rand -hex 24)
WEAVIATE_KEY=$(openssl rand -hex 24)
sed -i \
-e "s|^SECRET_KEY=.*|SECRET_KEY=$(openssl rand -base64 42)|" \
-e "s|^INIT_PASSWORD=.*|INIT_PASSWORD=$(openssl rand -hex 12)|" \
-e "s|^DB_PASSWORD=.*|DB_PASSWORD=${DB_PW}|" \
-e "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=${REDIS_PW}|" \
-e "s|^CELERY_BROKER_URL=.*|CELERY_BROKER_URL=redis://:${REDIS_PW}@redis:6379/1|" \
-e "s|^CODE_EXECUTION_API_KEY=.*|CODE_EXECUTION_API_KEY=${SANDBOX_KEY}|" \
-e "s|^SANDBOX_API_KEY=.*|SANDBOX_API_KEY=${SANDBOX_KEY}|" \
-e "s|^WEAVIATE_API_KEY=.*|WEAVIATE_API_KEY=${WEAVIATE_KEY}|" \
-e "s|^WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS=.*|WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS=${WEAVIATE_KEY}|" \
-e "s|^PLUGIN_DAEMON_KEY=.*|PLUGIN_DAEMON_KEY=$(openssl rand -hex 32)|" \
-e "s|^PLUGIN_DIFY_INNER_API_KEY=.*|PLUGIN_DIFY_INNER_API_KEY=$(openssl rand -hex 32)|" \
-e "s|^DIFY_AGENT_API_TOKEN=.*|DIFY_AGENT_API_TOKEN=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')|" \
-e "s|^DIFY_AGENT_SERVER_SECRET_KEY=.*|DIFY_AGENT_SERVER_SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')|" \
.envSECRET_KEY signs sessions, tokens and file URLs; the documentation generates it with openssl rand -base64 42. INIT_PASSWORD (at most 30 characters) protects the /install page until you create the admin account. The two DIFY_AGENT_* values replace development defaults for the agent backend, generated the way the example file suggests.
Next, tell Dify its public address and move every published port to localhost. The Compose file maps Nginx as EXPOSE_NGINX_PORT:80 and EXPOSE_NGINX_SSL_PORT:443, and the plugin daemon's remote-debugging port as EXPOSE_PLUGIN_DEBUGGING_PORT. Putting 127.0.0.1: in front of the host port keeps them off the internet and frees ports 80 and 443 for Caddy.
DIFY_URL=https://dify.example.com
sed -i \
-e "s|^CONSOLE_API_URL=.*|CONSOLE_API_URL=${DIFY_URL}|" \
-e "s|^CONSOLE_WEB_URL=.*|CONSOLE_WEB_URL=${DIFY_URL}|" \
-e "s|^SERVICE_API_URL=.*|SERVICE_API_URL=${DIFY_URL}|" \
-e "s|^APP_API_URL=.*|APP_API_URL=${DIFY_URL}|" \
-e "s|^APP_WEB_URL=.*|APP_WEB_URL=${DIFY_URL}|" \
-e "s|^FILES_URL=.*|FILES_URL=${DIFY_URL}|" \
-e "s|^TRIGGER_URL=.*|TRIGGER_URL=${DIFY_URL}|" \
-e "s|^ENDPOINT_URL_TEMPLATE=.*|ENDPOINT_URL_TEMPLATE=${DIFY_URL}/e/{hook_id}|" \
-e "s|^NEXT_PUBLIC_SOCKET_URL=.*|NEXT_PUBLIC_SOCKET_URL=wss://dify.example.com|" \
-e "s|^EXPOSE_NGINX_PORT=.*|EXPOSE_NGINX_PORT=127.0.0.1:8080|" \
-e "s|^EXPOSE_NGINX_SSL_PORT=.*|EXPOSE_NGINX_SSL_PORT=127.0.0.1:8443|" \
-e "s|^EXPOSE_PLUGIN_DEBUGGING_PORT=.*|EXPOSE_PLUGIN_DEBUGGING_PORT=127.0.0.1:5003|" \
.env
grep -E '^(INIT_PASSWORD|DB_PASSWORD|CONSOLE_WEB_URL|EXPOSE_NGINX_PORT|EXPOSE_NGINX_SSL_PORT)=' .envThe URL settings matter behind a proxy: CONSOLE_API_URL also decides whether Dify issues HTTPS-only cookies, FILES_URL builds file preview links, TRIGGER_URL builds webhook callbacks and NEXT_PUBLIC_SOCKET_URL is the WebSocket address for real-time collaboration on the workflow canvas. Note the INIT_PASSWORD value from the grep output; you need it in Step 5. Sign-up stays closed because ALLOW_REGISTER defaults to false, so new members join only by invitation.
Step 3 — Start the stack
docker compose up -d
docker compose psThe first start pulls a dozen images and takes a few minutes. In docker compose ps, the services should show Up or healthy; init_permissions is a one-time task and showing Exited is expected. The nginx line should list 127.0.0.1:8080->80/tcp and 127.0.0.1:8443->443/tcp, and no port may be bound to 0.0.0.0. Check that the gateway answers locally:
curl -I http://127.0.0.1:8080/installAny HTTP status line (usually 200) means Nginx, the web front end and the API are talking to each other.
Step 4 — Publish Dify over HTTPS with Caddy
Add a site block to /etc/caddy/Caddyfile that forwards to the bundled Nginx:
dify.example.com {
reverse_proxy 127.0.0.1:8080
}Reload Caddy and allow only SSH and web traffic through ufw:
sudo systemctl reload caddy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
curl -I https://dify.example.com/installCaddy passes the original Host header, forwards WebSocket upgrades for the collaboration channel automatically and flushes streamed responses immediately, so chat answers appear token by token. Dify's own HTTPS support in Nginx (NGINX_HTTPS_ENABLED) stays off, because Caddy terminates TLS. Nginx with Certbot (guide) or Traefik work as alternatives; with Nginx, forward the Upgrade and Connection headers on /socket.io/.
Step 5 — Create the admin account
Open https://dify.example.com/install. Dify asks for the init password from Step 2, then for the admin's email address, name and password. Use a long, unique password. After that you land in the studio, and the init password has no further effect.
Invite colleagues from the workspace's member settings instead of opening sign-up. Only the workspace owner and admins can manage model providers.
Dify emails account invitations, password resets and login codes once mail is configured in .env: set MAIL_TYPE=smtp, MAIL_DEFAULT_SEND_FROM, SMTP_SERVER=smtp.example.com, SMTP_PORT=587, SMTP_USERNAME, SMTP_PASSWORD, SMTP_USE_TLS=true and SMTP_OPPORTUNISTIC_TLS=true for STARTTLS, then run docker compose up -d.
Step 6 — Connect model providers, including Ollama
In Dify 1.x every model provider is a plugin. Open Integrations, then Model Provider, install the providers you need from the Marketplace and click Setup to enter API keys. Dify checks each key before it saves it. The plugin daemon downloads plugins from marketplace.dify.ai and verifies their signatures by default (FORCE_VERIFYING_SIGNATURE=true), so keep outbound HTTPS open.
For Ollama on the same server, three things are needed. First, Ollama must listen beyond 127.0.0.1, which the Ollama FAQ does with OLLAMA_HOST:
sudo mkdir -p /etc/systemd/system/ollama.service.d
sudo tee /etc/systemd/system/ollama.service.d/override.conf <<'EOF'
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
EOF
sudo systemctl daemon-reload
sudo systemctl restart ollamaSecond, ufw must let the Dify network reach the host. The Compose project is named after the docker folder, so the network is docker_default. Third, Dify's Compose file defines no host.docker.internal alias, so use the Docker bridge address of the host, usually 172.17.0.1:
docker network inspect docker_default | grep Subnet
sudo ufw allow from 172.19.0.0/16 to any port 11434 proto tcp
ip -4 addr show docker0
curl http://172.17.0.1:11434/api/versionReplace 172.19.0.0/16 with the subnet printed by the first command, and 172.17.0.1 with the inet address of docker0 if it differs. This rule only protects Ollama while ufw is active with its default deny policy, so confirm that sudo ufw status verbose shows Status: active and deny (incoming), and that nc -vz your-server-ip 11434 from another machine fails. The last command should return Ollama's version as JSON. Then install the Ollama plugin in Dify, add a model with the exact name shown by ollama list and set the base URL to http://172.17.0.1:11434. For bigger models, a GPU server makes a large difference.
Back up and restore
Dify keeps its state in /opt/dify/docker/volumes (PostgreSQL data, uploaded files in app/storage, Weaviate vectors, Redis data, plugin storage) and its configuration in .env. Take a logical database dump while the stack runs, then stop it briefly to archive the files consistently:
sudo mkdir -p /opt/backups
sudo chown $USER:$USER /opt/backups
cd /opt/dify/docker
docker compose exec -T db_postgres pg_dumpall -U postgres | gzip > /opt/backups/dify-db-$(date +%F).sql.gz
docker compose stop
sudo tar czf /opt/backups/dify-files-$(date +%F).tar.gz -C /opt/dify/docker volumes .env
docker compose startpg_dumpall captures both the dify database and the plugin daemon's dify_plugin database. To restore on a new server, clone the same release tag as in Step 1, unpack the file archive into the docker folder and start the stack:
cd /opt/dify/docker
sudo tar xzf /opt/backups/dify-files-2026-10-09.tar.gz -C /opt/dify/docker
docker compose up -dThe SQL dump is your second line of defence: load it into an empty PostgreSQL with gunzip -c dify-db-2026-10-09.sql.gz | docker compose exec -T db_postgres psql -U postgres if the data directory itself is damaged. Copy both archives off the server; they contain your secrets.
Update Dify
Dify's upgrade notes can differ between releases, so read the release notes of every version between yours and the target first.
The usual Docker Compose procedure from the release notes, with a full backup copy and the environment sync script added:
cd /opt/dify/docker
docker compose down
sudo cp -a /opt/dify /opt/backups/dify-$(date +%F)
git fetch --tags
git checkout 1.17.1
chmod +x dify-env-sync.sh
./dify-env-sync.sh
docker compose pull
docker compose up -dReplace 1.17.1 with the target tag. cp -a keeps ownership, so the copy can be restored as is. dify-env-sync.sh adds new variables from .env.example to your .env without overwriting your values, and saves the old file in env-backup/ first. Database migrations run automatically when the API starts; watch them with docker compose logs -f api.
Troubleshooting
Port 80 or 443 is already in use
Caddy owns ports 80 and 443 on the host. If docker compose up fails with address already in use, the EXPOSE_NGINX_PORT or EXPOSE_NGINX_SSL_PORT lines in .env were not changed. Fix them as in Step 2 and run docker compose up -d again.
fatal: Remote branch null not found in upstream origin
The GitHub API call returned no tag, often because of rate limiting. Clone with an explicit tag from the releases page, as shown in Step 1.
502 Bad Gateway after a restart
Check docker compose ps: every service must be Up or healthy. Dify's documentation explains that the bundled Nginx can keep forwarding to outdated container addresses after containers restart. Restart the gateway with docker compose restart nginx; after a server reboot, run docker compose down and docker compose up -d.
The page loads forever or the console shows CORS errors
The URL variables in .env do not match the address in your browser. Correct CONSOLE_API_URL, CONSOLE_WEB_URL, SERVICE_API_URL, APP_API_URL, APP_WEB_URL and FILES_URL, then run docker compose down and docker compose up -d.
An HTTP Request node cannot reach an internal address
This is by design: Dify routes outbound requests from HTTP nodes through its ssrf_proxy, which blocks internal and private ranges. Only if you trust every workflow author, add ACL rules to docker/volumes/ssrf_proxy/squid.conf as the documentation describes and restart the proxy container.
Next steps
- Run local models with Install Ollama and connect them as shown in Step 6.
- Compare a visual, code-friendly alternative in Install Langflow.
- Read the official Dify environment variable reference before changing storage or vector database settings.
- See server options for this app on the Dify hosting page.
Frequently asked questions
How much memory does Dify need?
Dify's documentation asks for at least 2 CPU cores and 4 GiB of RAM. The default stack starts more than a dozen containers, including PostgreSQL, Redis and Weaviate, so 8 GB leaves room to work. Local models need their own memory on top.
How do I reset a forgotten Dify account password?
Run docker compose exec api flask reset-password in /opt/dify/docker. The command asks for the account email and the new password, so it also works when no mail server is configured.
Why does the install page ask for a password?
This guide sets INIT_PASSWORD in .env. Dify then requires that password on the /install page before anyone can create the admin account, which protects a fresh server that is already reachable over HTTPS. After setup the value has no further effect.
Can I change the database password later by editing .env?
Not by editing .env alone. PostgreSQL stores the password in its data directory when the database is first created, so a new DB_PASSWORD no longer matches. Set strong values before the first start, or change the password inside PostgreSQL as well.
Can Dify use models running in Ollama?
Yes. Install the Ollama provider plugin from the Dify Marketplace, make Ollama reachable from Docker networks and use the Docker bridge address, usually http://172.17.0.1:11434, as the base URL.
Sources
- docs.dify.ai/en/self-host/quick-start/docker-compose
- docs.dify.ai/en/getting-started/install-self-hosted/docker-compose
- docs.dify.ai/en/self-host/deploy/configuration/environments
- docs.dify.ai/en/self-host/deploy/troubleshooting/docker-issues
- docs.dify.ai/en/self-host/deploy/troubleshooting/storage-and-migration
- docs.dify.ai/en/use-dify/workspace/model-providers
- github.com/langgenius/dify/blob/1.17.1/docker/.env.example
- github.com/langgenius/dify/blob/1.17.1/docker/docker-compose.yaml
- github.com/langgenius/dify/blob/1.17.1/docker/README.md
- github.com/langgenius/dify/releases/tag/1.17.1
- github.com/langgenius/dify/blob/main/LICENSE
- docs.ollama.com/faq