Building a Bitbucket-Style File Server with Docker, NGINX, and Cloudflare

Introduction

I recently needed a simple, secure place to drop files and documents (like PDFs, images, and configuration files) and give people a direct, permanent, HTTPS-secured link. Relying on the WordPress media library wasn’t scalable, and I wanted to keep the files on my central NFS-mounted storage here in the homelab.

This post details how I built a robust, low-overhead file server using an NGINX Docker container and secured external access with Cloudflare Tunnels. The best part? It allows for both public links (for QR codes) and a password-protected area—all from the same URL.


Key Technical Goals Achieved

  1. Direct File Serving: Serve files directly from an existing NFS/Samba shared directory on the host.
  2. Zero-Trust Access: Use Cloudflare Tunnels to expose the service via HTTPS without opening any firewall ports.
  3. Dual Security Zones: Create a single endpoint with two distinct access areas:
    • Public: Unrestricted access for things like QR codes and public assets.
    • Secured: Requires a username and password (HTTP Basic Auth).
  4. No Directory Browsing: Prevent unauthenticated users from seeing a list of files (autoindex off).

The Architecture: NGINX and the Magic of Bind Mounts

The entire solution runs inside one minimal nginx:alpine Docker container. The trick is using Docker bind mounts to inject the correct files and folders:

Host Resource (Source)Container Path (Destination)Purpose
/mnt/SSD/HOME/PUBLIC-SHARE/usr/share/nginx/htmlData: The root folder for ALL served files.
/mnt/DockerFiles/nginx-fileshare/default.conf/etc/nginx/conf.d/default.confConfiguration: Tells NGINX how to serve and protect folders.
/mnt/DockerFiles/nginx-fileshare/.htpasswd/etc/nginx/htpasswd/.htpasswdSecurity: Provides usernames and hashed passwords for secured area.

The NGINX Configuration (default.conf)

This file is the core of the solution, establishing the public root and applying basic authentication to the /secured/ subfolder.

Nginx

server {
    listen 80;
    server_name localhost;
    
    # Hide NGINX version for security by obscurity
    server_tokens off; 

    # Sets the web root to the main mounted data folder
    root /usr/share/nginx/html;

    # --- PUBLIC ACCESS (Root Location) ---
    location / {
        # IMPORTANT: Prevents users from seeing a list of all your files
        autoindex off; 
        try_files $uri $uri/ =404; 
    }

    # --- SECURED ACCESS (Only for the /secured/ subfolder) ---
    location /secured/ {
        auth_basic "Private Files - Authentication Required";
        auth_basic_user_file /etc/nginx/htpasswd/.htpasswd;
        
        autoindex off;
        try_files $uri $uri/ =404; 
    }
}

Deployment with Docker Compose

I used a docker-compose.yml file to manage the service, ensuring all mounts were defined correctly.

YAML

version: '3.7'
services:
  nginx-file-server:
    image: nginx:alpine
    container_name: nginx_file_server
    
    ports:
      - "8081:80" # Target for my Cloudflare Tunnel
      
    volumes:
      # Data Mount (My files are here)
      - /mnt/SSD/HOME/PUBLIC-SHARE:/usr/share/nginx/html:ro
      
      # Configuration & Auth Files
      - /mnt/DockerFiles/nginx-fileshare/default.conf:/etc/nginx/conf.d/default.conf:ro
      - /mnt/DockerFiles/nginx-fileshare/.htpasswd:/etc/nginx/htpasswd/.htpasswd:ro
      
    restart: unless-stopped

🔐 Managing Users Securely (Creating the .htpasswd file)

This is a critical security step. Since the password file is mounted as read-only, we update it by running a one-off Docker command from the host machine. Always use a unique username and a strong, complex password for any public-facing service.

To create the file and add the first user (e.g., admin_user):

Bash

# REPLACE 'admin_user' and 'SuperS3c!RetP4ssw0rd' with your own secure values!
docker run --rm httpd htpasswd -nb admin_user 'SuperS3c!RetP4ssw0rd' > /mnt/DockerFiles/nginx-fileshare/.htpasswd

To add a subsequent user (e.g., guest_access):

Bash

# Omit the '>' and use '>>' to append to the file.
docker run --rm httpd htpasswd -nb guest_access 'Temp4cc3ssP4ss' >> /mnt/DockerFiles/nginx-fileshare/.htpasswd

Conclusion

This setup is the perfect balance of simplicity, performance, and security. Once deployed, any file added to the host directory /mnt/SSD/HOME/PUBLIC-SHARE is instantly available as a public link via my custom Cloudflare Tunnel domain. The links are stable, secured by HTTPS, and perfect for embedding in other applications or using in physical QR codes.

Similar posts