# 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.

Difficulty: Intermediate\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

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](/guides/install-docker-ubuntu) or [Install Docker on Debian](/guides/install-docker-debian).
- A non-root user with `sudo` rights and SSH key login, see [Secure a new Linux server](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys).
- A domain name such as `dify.example.com` with an A (and optionally AAAA) record pointing at the server, and Caddy from [Caddy reverse proxy](/guides/caddy-reverse-proxy).
- A model provider account (OpenAI, Anthropic and others) or Ollama from [Install Ollama](/guides/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:

```bash
docker compose version
```

## Step 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.

```bash
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 --tags
```

`git 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](https://github.com/langgenius/dify/releases) 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.

```bash
cd /opt/dify/docker
cp .env.example .env
chmod 600 .env
```

The 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.

```bash
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))')|" \
  .env
```

`SECRET_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.

> **Warning**
>
> Set these values before the first `docker compose up`. PostgreSQL and Redis store their passwords when their data directories are created, so changing `DB_PASSWORD` or `REDIS_PASSWORD` later in `.env` alone breaks the connection.

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.

```bash
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)=' .env
```

The 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

```bash
docker compose up -d
docker compose ps
```

The 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:

```bash
curl -I http://127.0.0.1:8080/install
```

Any 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:

```caddyfile
dify.example.com {
    reverse_proxy 127.0.0.1:8080
}
```

Reload Caddy and allow only SSH and web traffic through ufw:

```bash
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/install
```

Caddy 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](/guides/nginx-reverse-proxy-certbot)) or [Traefik](/guides/traefik-reverse-proxy) 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`.

> **Note**
>
> Outbound port 25 is closed by default on HyperDC VPS. For services bought for a term of 3 months or longer, it is opened on request: [open a support ticket](/guides/support-tickets). Until then, send mail through an SMTP relay on port 587.

## 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`:

```bash
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 ollama
```

> **Warning**
>
> Ollama has no authentication. Keep ufw enabled with its default deny policy and only allow the Docker subnet below; never open port 11434 to everyone.

Second, 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`:

```bash
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/version
```

Replace `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](/gpu-servers) 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:

```bash
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 start
```

`pg_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:

```bash
cd /opt/dify/docker
sudo tar xzf /opt/backups/dify-files-2026-10-09.tar.gz -C /opt/dify/docker
docker compose up -d
```

The 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](https://github.com/langgenius/dify/releases) of every version between yours and the target first.

> **Danger**
>
> Some releases need extra steps. Dify 1.17.1, for example, requires a staged, manual upgrade of the bundled Weaviate before the new version starts; pulling and restarting directly can break vector search permanently. Always take a full copy first.

The usual Docker Compose procedure from the release notes, with a full backup copy and the environment sync script added:

```bash
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 -d
```

Replace `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](/guides/install-ollama) and connect them as shown in Step 6.
- Compare a visual, code-friendly alternative in [Install Langflow](/guides/install-langflow).
- Read the official [Dify environment variable reference](https://docs.dify.ai/en/self-host/deploy/configuration/environments) before changing storage or vector database settings.
- See server options for this app on the [Dify hosting](/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.

---

Source: <https://hyperdc.com/guides/tutorials/install-dify>\
Updated: 2026-10-09
