|
| 1 | +* TOC |
| 2 | +{:toc} |
| 3 | + |
| 4 | +## Overview {#overview} |
| 5 | + |
| 6 | +[Ollama](https://ollama.com/){:target="_blank"} is a powerful tool for running Large Language Models (LLMs) locally, but it does not include built-in authentication mechanisms. |
| 7 | +When exposing Ollama on a network, securing the API endpoint becomes your responsibility. |
| 8 | + |
| 9 | +This guide demonstrates how to deploy Ollama with [Nginx](https://nginx.org/){:target="_blank"} as a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy){:target="_blank"} |
| 10 | +to add authentication to your Ollama deployment. The Nginx proxy acts as a security gatekeeper, validating credentials before forwarding requests to the Ollama service. |
| 11 | + |
| 12 | +We will cover two common authentication methods: |
| 13 | +- **HTTP Basic Authentication** (username and password) |
| 14 | +- **Bearer Token Authentication** (a secret API key) |
| 15 | + |
| 16 | +Both services, Ollama and Nginx, will be deployed together as containers using Docker Compose. |
| 17 | +This guide focuses on demonstrating the concept with a working implementation that you can use as a foundation for further customization. |
| 18 | +We use the standard Ollama Docker image without GPU acceleration to keep the setup straightforward, though GPU support can be added later for improved performance. |
| 19 | + |
| 20 | +{% capture https_warning %} |
| 21 | +After completing this guide, we **strongly recommend** securing your [Nginx proxy with HTTPS](https://nginx.org/en/docs/http/configuring_https_servers.html){:target="_blank"} |
| 22 | +to ensure that credentials (passwords or bearer tokens) are always encrypted and not sent in plain text over the network. |
| 23 | +{% endcapture %} |
| 24 | +{% include templates/warn-banner.md content=https_warning %} |
| 25 | + |
| 26 | +## Prerequisites {#prerequisites} |
| 27 | + |
| 28 | +Before you start, ensure you have Docker and Docker Compose installed. |
| 29 | +The easiest way to get both is to install [Docker Desktop](https://docs.docker.com/desktop/){:target="_blank"} and ensure it is running before you proceed. |
| 30 | + |
| 31 | +## Setup: Project Directory {#project-setup} |
| 32 | + |
| 33 | +First, create a main project directory named `ollama-nginx-auth`. All the files we create throughout this guide will be placed inside this directory. |
| 34 | + |
| 35 | +Next, inside the `ollama-nginx-auth` directory, create another directory named `nginx`. This is where you will store your Nginx-specific configuration files. |
| 36 | + |
| 37 | +After you are done, your directory structure should look like this: |
| 38 | +``` |
| 39 | +ollama-nginx-auth/ |
| 40 | +└── nginx/ |
| 41 | +``` |
| 42 | + |
| 43 | +Make sure you are working inside the main `ollama-nginx-auth` directory for the next steps. |
| 44 | + |
| 45 | +## Approach 1: HTTP Basic Authentication {#basic-auth} |
| 46 | + |
| 47 | +This method protects your endpoint with a simple username and password. |
| 48 | +When a request is made, Nginx checks the provided credentials against an encrypted list of users in a `.htpasswd` file to grant or deny access. |
| 49 | + |
| 50 | +The `.htpasswd` file is a standard file used for storing usernames and passwords for basic authentication on web servers like Nginx. |
| 51 | +Each line in the file represents a single user and contains the username followed by a colon and the encrypted (hashed) password. |
| 52 | + |
| 53 | +### Step 1: Create the Credential File {#basic-credentials} |
| 54 | + |
| 55 | +From your project root (`ollama-nginx-auth`), create the `.htpasswd` file inside the `nginx` directory. This command creates a file with the username `myuser` and password `mypassword`. |
| 56 | + |
| 57 | +{% capture tabspec %}htpasswd-setup |
| 58 | +htpasswd-setup-linux-macos,Linux/macOS,shell,/docs/samples/analytics/resources/htpasswd-setup-linux-macos.sh |
| 59 | +htpasswd-setup-windows,Windows (PowerShell),text,/docs/samples/analytics/resources/htpasswd-setup-windows.ps1{% endcapture %} |
| 60 | +{% include tabs.html %} |
| 61 | + |
| 62 | +### Step 2: Create the Nginx Configuration File {#basic-config} |
| 63 | + |
| 64 | +Create a file named `basic_auth.conf` inside the `nginx` directory (`ollama-nginx-auth/nginx/basic_auth.conf`) and paste the following content into it. |
| 65 | +``` |
| 66 | +events {} |
| 67 | +
|
| 68 | +http { |
| 69 | + server { |
| 70 | + listen 80; |
| 71 | + |
| 72 | + location / { |
| 73 | + # This section enforces HTTP Basic Authentication |
| 74 | + auth_basic "Restricted Access"; |
| 75 | + auth_basic_user_file /etc/nginx/.htpasswd; # Path to credentials file inside the container |
| 76 | + |
| 77 | + # If authentication is successful, forward the request to Ollama |
| 78 | + proxy_pass http://ollama:11434; |
| 79 | + proxy_set_header Host $host; |
| 80 | + proxy_set_header X-Real-IP $remote_addr; |
| 81 | + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 82 | + |
| 83 | + # Increase timeouts for slow model responses to prevent 504 Gateway Timeout errors |
| 84 | + proxy_connect_timeout 300s; |
| 85 | + proxy_send_timeout 300s; |
| 86 | + proxy_read_timeout 300s; |
| 87 | + } |
| 88 | + } |
| 89 | +} |
| 90 | +``` |
| 91 | +{: .copy-code} |
| 92 | + |
| 93 | +Here's what the configuration does: |
| 94 | +- `listen 80;`: Nginx listens on port 80 inside the Docker container. |
| 95 | +- `auth_basic "Restricted Access";`: Enables HTTP Basic Authentication. |
| 96 | +- `auth_basic_user_file /etc/nginx/.htpasswd;`: Specifies the location of the password file inside the container. We will mount our local file to this path. |
| 97 | +- `proxy_pass http://ollama:11434;`: Forwards any authenticated requests to the `ollama` service at its internal address. |
| 98 | + |
| 99 | +### Step 3: Create the Docker Compose File {#basic-compose} |
| 100 | + |
| 101 | +Create a file named `docker-compose.basic.yml` in the root of your project (`ollama-nginx-auth/docker-compose.basic.yml`) and paste the following content into it. |
| 102 | +```yml |
| 103 | +services: |
| 104 | + ollama: |
| 105 | + image: ollama/ollama |
| 106 | + container_name: ollama |
| 107 | + volumes: |
| 108 | + - ollama_data:/root/.ollama |
| 109 | + restart: unless-stopped |
| 110 | + |
| 111 | + nginx: |
| 112 | + image: nginx:latest |
| 113 | + container_name: nginx_proxy |
| 114 | + ports: |
| 115 | + - "8880:80" |
| 116 | + volumes: |
| 117 | + - ./nginx/basic_auth.conf:/etc/nginx/nginx.conf:ro |
| 118 | + - ./nginx/.htpasswd:/etc/nginx/.htpasswd:ro |
| 119 | + depends_on: |
| 120 | + - ollama |
| 121 | + restart: unless-stopped |
| 122 | + |
| 123 | +volumes: |
| 124 | + ollama_data: |
| 125 | +``` |
| 126 | +{: .copy-code} |
| 127 | +
|
| 128 | +### Step 4: Run and Test {#basic-test} |
| 129 | +
|
| 130 | +Start the services using the dedicated compose file. The `-f` flag specifies which file to use. This may take a some time. |
| 131 | +```shell |
| 132 | +docker compose -f docker-compose.basic.yml up -d |
| 133 | +``` |
| 134 | +{: .copy-code} |
| 135 | + |
| 136 | +Pull a model by executing the command directly inside the Ollama container. We'll use `gemma3:1b`, a lightweight model suitable for testing. This may take a some time. |
| 137 | +```shell |
| 138 | +docker exec -it ollama ollama pull gemma3:1b |
| 139 | +``` |
| 140 | +{: .copy-code} |
| 141 | + |
| 142 | +Test with your user (`myuser`): |
| 143 | + |
| 144 | +{% capture tabspec %}http-basic-test |
| 145 | +http-basic-test-linux-macos,Linux/macOS,shell,/docs/samples/analytics/resources/http-basic-test-linux-macos.sh |
| 146 | +http-basic-test-windows,Windows (PowerShell),text,/docs/samples/analytics/resources/http-basic-test-windows.ps1{% endcapture %} |
| 147 | +{% include tabs.html %} |
| 148 | + |
| 149 | +Test an API call with incorrect credentials to see it fail: |
| 150 | + |
| 151 | +{% capture tabspec %}http-basic-failed-test |
| 152 | +http-basic-failed-test-linux-macos,Linux/macOS,shell,/docs/samples/analytics/resources/http-basic-failed-test-linux-macos.sh |
| 153 | +http-basic-failed-test-windows,Windows (PowerShell),text,/docs/samples/analytics/resources/http-basic-failed-test-windows.ps1{% endcapture %} |
| 154 | +{% include tabs.html %} |
| 155 | + |
| 156 | +The output will show `401 Unauthorized` error. |
| 157 | + |
| 158 | +### Step 5 (Optional): Manage Users {#basic-manage-users} |
| 159 | + |
| 160 | +You can easily add or remove users from the `.htpasswd` file. Changes to this file take effect immediately without needing to restart Nginx. |
| 161 | + |
| 162 | +{% capture adding-users-via-htpasswd %} |
| 163 | +Always use the `htpasswd` command to add users. This utility correctly encrypts the password and ensures the credentials are stored in the format that Nginx requires. |
| 164 | +Manually adding plain-text passwords to the file will not work. |
| 165 | +{% endcapture %} |
| 166 | +{% include templates/info-banner.md content=adding-users-via-htpasswd %} |
| 167 | + |
| 168 | +**To add a new user:** |
| 169 | + |
| 170 | +Run the `htpasswd` command again. This example adds `anotheruser` with password `anotherpassword`. |
| 171 | + |
| 172 | +{% capture tabspec %}http-basic-add-user |
| 173 | +http-basic-add-user-linux-macos,Linux/macOS,shell,/docs/samples/analytics/resources/http-basic-add-user-linux-macos.sh |
| 174 | +http-basic-add-user-windows,Windows (PowerShell),text,/docs/samples/analytics/resources/http-basic-add-user-windows.ps1{% endcapture %} |
| 175 | +{% include tabs.html %} |
| 176 | + |
| 177 | +You can repeat this command for as many users as you need. |
| 178 | + |
| 179 | +**To remove a user:** |
| 180 | + |
| 181 | +Simply open the file `./nginx/.htpasswd` in a text editor and delete the line corresponding to the user you want to remove. |
| 182 | + |
| 183 | +## Approach 2: Bearer Token (API Key) Authentication {#bearer-token} |
| 184 | + |
| 185 | +This method uses a secret token. You will manage your keys in a simple text file, and Nginx will be configured to read them without needing a service restart. |
| 186 | + |
| 187 | +### Step 1: Create the API Keys File {#bearer-keys} |
| 188 | + |
| 189 | +Create a file named `api_keys.txt` inside the `nginx` directory (`ollama-nginx-auth/nginx/api_keys.txt`) and paste your API keys into it, one per line. |
| 190 | +``` |
| 191 | +my-secret-api-key-1 |
| 192 | +admin-key-abcdef |
| 193 | +``` |
| 194 | +{: .copy-code} |
| 195 | +
|
| 196 | +### Step 2: Create the Nginx Configuration File {#bearer-config} |
| 197 | +
|
| 198 | +Create a file named `bearer_token.conf` inside the `nginx` directory (`ollama-nginx-auth/nginx/bearer_token.conf`) and paste the following content into it. |
| 199 | +This configuration includes a [Lua](https://www.lua.org/) script to read the API keys file dynamically. |
| 200 | +``` |
| 201 | +events {} |
| 202 | + |
| 203 | +http { |
| 204 | + server { |
| 205 | + listen 80; |
| 206 | + |
| 207 | + location / { |
| 208 | + # Lua script to read keys from a file and check against the Authorization header |
| 209 | + # This code runs for every request to this location. |
| 210 | + access_by_lua_block { |
| 211 | + local function trim(s) |
| 212 | + return (s:gsub("^%s*(.-)%s*$", "%1")) |
| 213 | + end |
| 214 | + |
| 215 | + -- Function to read keys from the file into a set for quick lookups |
| 216 | + local function get_keys_from_file(path) |
| 217 | + local keys = {} |
| 218 | + local file = io.open(path, "r") |
| 219 | + if not file then |
| 220 | + ngx.log(ngx.ERR, "cannot open api keys file: ", path) |
| 221 | + return keys |
| 222 | + end |
| 223 | + for line in file:lines() do |
| 224 | + line = trim(line) |
| 225 | + if line ~= "" then |
| 226 | + keys[line] = true |
| 227 | + end |
| 228 | + end |
| 229 | + file:close() |
| 230 | + return keys |
| 231 | + end |
| 232 | + |
| 233 | + -- Path to the keys file inside the container |
| 234 | + local api_keys_file = "/etc/nginx/api_keys.txt" |
| 235 | + local valid_keys = get_keys_from_file(api_keys_file) |
| 236 | + |
| 237 | + -- Check the Authorization header |
| 238 | + local auth_header = ngx.var.http_authorization or "" |
| 239 | + local _, _, token = string.find(auth_header, "Bearer%s+(.+)") |
| 240 | + |
| 241 | + if not token or not valid_keys[token] then |
| 242 | + return ngx.exit(ngx.HTTP_UNAUTHORIZED) |
| 243 | + end |
| 244 | + } |
| 245 | + |
| 246 | + # If access is granted, forward the request to Ollama |
| 247 | + proxy_pass http://ollama:11434; |
| 248 | + proxy_set_header Host $host; |
| 249 | + proxy_set_header X-Real-IP $remote_addr; |
| 250 | + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 251 | + |
| 252 | + # Increase timeouts for slow model responses to prevent 504 Gateway Timeout errors |
| 253 | + proxy_connect_timeout 300s; |
| 254 | + proxy_send_timeout 300s; |
| 255 | + proxy_read_timeout 300s; |
| 256 | + } |
| 257 | + } |
| 258 | +} |
| 259 | +``` |
| 260 | +{: .copy-code} |
| 261 | +
|
| 262 | +Here's what the configuration does: |
| 263 | +- `listen 80;`: Nginx listens on port 80 inside the Docker container. |
| 264 | +- `access_by_lua_block`: Executes a Lua script for each request to validate the Bearer token. |
| 265 | + - The script reads valid API keys from `/etc/nginx/api_keys.txt` on every request. |
| 266 | + - It extracts the token from the `Authorization: Bearer <token>` header. |
| 267 | + - If the token is missing or not found in the valid keys list, it returns a 401 Unauthorized response. |
| 268 | +- `proxy_pass http://ollama:11434;`: Forwards any authenticated requests to the `ollama` service at its internal address. |
| 269 | +
|
| 270 | +### Step 3: Create the Docker Compose File {#bearer-compose} |
| 271 | +
|
| 272 | +Create a file named `docker-compose.bearer.yml` in the root of your project (`ollama-nginx-auth/docker-compose.bearer.yml`) and paste the following content into it. |
| 273 | +This `docker-compose.bearer.yml` uses an Nginx image that includes the required Lua module (`openresty/openresty`). |
| 274 | +```yml |
| 275 | +services: |
| 276 | + ollama: |
| 277 | + image: ollama/ollama |
| 278 | + container_name: ollama |
| 279 | + volumes: |
| 280 | + - ollama_data:/root/.ollama |
| 281 | + restart: unless-stopped |
| 282 | +
|
| 283 | + nginx: |
| 284 | + # Use the OpenResty image which includes the Nginx Lua module |
| 285 | + image: openresty/openresty:latest |
| 286 | + container_name: nginx_proxy |
| 287 | + ports: |
| 288 | + - "8880:80" |
| 289 | + volumes: |
| 290 | + # Mount the new Nginx config and the API keys file |
| 291 | + - ./nginx/bearer_token.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro |
| 292 | + - ./nginx/api_keys.txt:/etc/nginx/api_keys.txt:ro |
| 293 | + depends_on: |
| 294 | + - ollama |
| 295 | + restart: unless-stopped |
| 296 | +
|
| 297 | +volumes: |
| 298 | + ollama_data: |
| 299 | +``` |
| 300 | +{: .copy-code} |
| 301 | + |
| 302 | +### Step 4: Run and Test {#bearer-test} |
| 303 | + |
| 304 | +Start the services using the dedicated compose file. The `-f` flag specifies which file to use. |
| 305 | +```shell |
| 306 | +docker compose -f docker-compose.bearer.yml up -d |
| 307 | +``` |
| 308 | +{: .copy-code} |
| 309 | + |
| 310 | +Pull a model (this will be quick if you did it in Approach 1): |
| 311 | +```shell |
| 312 | +docker exec -it ollama ollama pull gemma3:1b |
| 313 | +``` |
| 314 | +{: .copy-code} |
| 315 | + |
| 316 | +Test a request using a valid API key: |
| 317 | + |
| 318 | +{% capture tabspec %}bearer-test |
| 319 | +bearer-test-linux-macos,Linux/macOS,shell,/docs/samples/analytics/resources/bearer-test-linux-macos.sh |
| 320 | +bearer-test-windows,Windows (PowerShell),text,/docs/samples/analytics/resources/bearer-test-windows.ps1{% endcapture %} |
| 321 | +{% include tabs.html %} |
| 322 | + |
| 323 | +Test with an invalid API key to see it fail: |
| 324 | + |
| 325 | +{% capture tabspec %}bearer-failed-test |
| 326 | +bearer-failed-test-linux-macos,Linux/macOS,shell,/docs/samples/analytics/resources/bearer-failed-test-linux-macos.sh |
| 327 | +bearer-failed-test-windows,Windows (PowerShell),text,/docs/samples/analytics/resources/bearer-failed-test-windows.ps1{% endcapture %} |
| 328 | +{% include tabs.html %} |
| 329 | + |
| 330 | +### Step 5 (Optional): Manage API Keys {#bearer-manage-keys} |
| 331 | + |
| 332 | +Simply open the file `./nginx/api_keys.txt` in a text editor. Add, change, or remove keys (one per line). Save the file. |
| 333 | + |
| 334 | +The changes take effect immediately on the next API request because the Lua script reads the file every time a request is made. |
| 335 | + |
| 336 | +For example, you can edit the file, remove the `admin-key-abcdef` key, save it, and then try to use that key in a test request. |
| 337 | +The request will now fail with a 401 Unauthorized error. |
| 338 | + |
| 339 | +## Usage {#usage} |
| 340 | + |
| 341 | +To start or stop the services, you will use the `docker compose up` and `docker compose down` commands, |
| 342 | +making sure to specify the appropriate file for the authentication approach you want to use (`docker-compose.basic.yml` or `docker-compose.bearer.yml`). |
| 343 | +- To start the services for either approach, run the following command from your project directory, replacing `<compose-file-name>` with the correct file name: |
| 344 | + ```shell |
| 345 | + docker compose -f <compose-file-name> up -d |
| 346 | + ``` |
| 347 | + {: .copy-code} |
| 348 | + |
| 349 | +- When you're finished, stop the containers with the corresponding file name: |
| 350 | + ```shell |
| 351 | + docker compose -f <compose-file-name> down |
| 352 | + ``` |
| 353 | + {: .copy-code} |
| 354 | + |
| 355 | +## Next steps {#next-steps} |
| 356 | + |
| 357 | +Now that you have Ollama endpoint, here are some recommended next steps: |
| 358 | + |
| 359 | +- **Enable HTTPS**: Secure your Nginx proxy with HTTPS by following the [official Nginx HTTPS configuration guide](https://nginx.org/en/docs/http/configuring_https_servers.html){:target="_blank"}. |
| 360 | + |
| 361 | +- **Add GPU Support**: Enable GPU acceleration for Ollama to significantly improve inference speed. |
| 362 | + Use the [Ollama Docker GPU setup instructions](https://github.com/ollama/ollama/blob/main/docs/docker.md){:target="_blank"} as a starting point. |
0 commit comments