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

Technical Log: Scaling Terminal Tracker with OCR Automation, RBAC Layouts, and Replicated High AvailabilityTechnical Log: Scaling Terminal Tracker with OCR Automation, RBAC Layouts, and Replicated High Availability

We recently completed a major architectural overhaul of the Terminal Tracker App. The objective was clear: automate heavy document ingestion, eliminate single points of failure during critical operational windows, and