Host your own private image gallery using Piwigo and Nginx

You want to show some photos of the kids to your family in a secure fashion, but do not want to rely on the cloud folks to store your data? Then running Piwigo on your home server or a VPS is a great way to do exactly that.

Piwigo is free, open source software with a rich feature set and lots of available plugins and themes. If you’re using Lightroom to manage your photo library, there is the Piwigo publisher Plug-In by alloyphoto (one of the best 15$ I’ve ever spent, great support included).

The basic installation of Piwigo is simple and well explained here, but in order to make it secure and “hide it from the bots” some additional setup is required and the documentation still lacks a bit in that regard.

Depending on the level of privacy you demand, the steps required are a bit different

  1. I don’t care about privacy
    • Install Piwigo and you’re done
  2. I don’t want to require login accounts for the family, but keep things off search indexes (Google, Bing) and don’t want any images to be shared on social webs either
    • Configure Piwigo to not serve files directly
    • Configure the webserver to block direct requests to the files
    • Disable hotlinking
    • Disable the ability to right-click -> save image, or drag-and-drop the images
  3. I want total privacy
    • Configure Piwigo to not serve files directly
    • Configure the webserver to block direct access to the files
    • Create users and configure private albums
    • To totally lock down access to the files, use a workaround like piwigo-privacy

I opted for 2. and will explain the steps required below.

Configure Piwigo

1. Set basic URL options

Edit local/config/ either using an editor or the LocalFiles Editor plug-in:

// do not serve files using the original path on filesystem
$conf['original_url_protection'] = 'images';
// protect derivates (resized versions), too
$conf['derivative_url_style'] = 2;

// make URLs look better
$conf['question_mark_in_urls'] = false;
$conf['php_extension_in_urls'] = false;
$conf['category_url_style'] = 'id-name';
$conf['picture_url_style'] = 'id-file';

2. Install the rightClick plug-in.

The rightClick plugin disables the right click and drag events using jquery. Crafty web dudes will still be able to just look at the page’s source code and find what they’re looking for, but definately makes it hard for the average folks.

Depending on the theme used, you might want to configure additional selectors. Here’s an example for “Bootstrap Default” + the videojs plug-in (again in local/config/

$conf['rightClick_selectors'] = array(
  '.placeholder img',
  '.leaflet-popup-content img',

By default, right click and drag events are disabled directly in the images, you could also disable it on the whole html body

$conf['rightClick_selectors'] = array(

3. Put a robots.txt in place

In the document root of your webserver, put a robots.txt in place with following content. At least the “good” search engine robots do care about it. Of course this is no protection against bad robots, but we have something for those later on

User-agent: *
Disallow: /

Configure the Webserver

First, I would advise anyone to secure the site with SSL, no matter if you do care about privacy or don’t. With letsencrypt securing your server has never been easier.

I will simply post my virtual host config here, and explain things in the comments

server {
 server_name gallery.domain.tld;

 # make letsencrypt webroot plugin work
 location ~ .well-known/acme-challenge/ {
   root /srv/http/letsencrypt;
   default_type text/plain;
 # anything else goes https
 location / {
   return 301 https://$server_name;

server {
 listen 443 ssl;
 server_name gallery.domain.tld;
 root /srv/http/piwigo;
 # configure SSL 
 ssl_certificate      /etc/letsencrypt/live/gallery.domain.tld/fullchain.pem;
 ssl_certificate_key  /etc/letsencrypt/live/gallery.domain.tld/privkey.pem;
 ssl_dhparam          /etc/ssl/dh2048.pem;
 ssl_session_cache shared:SSL:1m;
 ssl_session_timeout  5m;
 ssl_protocols TLSv1.2 TLSv1.1;
 ssl_ciphers  FIPS@STRENGTH:!aNULL:!MD5;
 ssl_prefer_server_ciphers   on;
 # enable HSTS
 add_header Strict-Transport-Security max-age=31536000;
 # disallow framing
 add_header X-Frame-Options DENY;

 # compress css and javascript (default is text/html only)
 gzip on;
 gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;

 client_max_body_size 500m;
 client_body_buffer_size 100m;

 location ~ ^/favicon.ico$ {
   log_not_found off;
   access_log off;
 location = /robots.txt {
   log_not_found off;
   access_log off;

 # Deny Piwigo distribution files
 location ~ ^/(README|doc)$ {
   deny all;
 # Deny hidden dot files
 location ~ /\. {
   deny all;
   access_log off;
   log_not_found off;

 # Prevent direct access to Piwigo images and logs on filesystem
 location ~ ^/(_data/(i|logs)|upload)/ {
   return 403;

 # the @rewrite is required when using
 # $conf['question_mark_in_urls'] = false;
 # $conf['php_extension_in_urls'] = false;
 location / {
   index index.php;
   try_files $uri $uri/ @rewrite;

 location @rewrite {
   rewrite ^/picture((/|$).*)$ /picture.php$1 last;
   rewrite ^/index((/|$).*)$ /index.php$1 last;
   rewrite ^/i((/|$).*)$ /i.php$1 last;
   # for piwigo-openstreetmap
   rewrite ^/osmmap((/|$).*)$ /osmmap.php$1 last;

 location ~ ^(?<script_name>.+?\.php)(?<path_info>/.*)?$ {
   try_files $script_name =404;
   include /etc/nginx/fastcgi_params;
   fastcgi_param PATH_INFO $path_info;
   fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

 ### Prevent hotlinks - this section needs to be _after_ the php handler above
 # When uploading images via ws.php (e.g. from Lightroom), the are requests from Piwigo
 # itself to /i/upload using the servers own IP and "Piwigo" user agent. We need to allow those

 # Only the site's own URL is a valid referer.
 # Empty referers and others like google etc are invalid, see Nginx docs for more info
 valid_referers gallery.domain.tld;
 # initialize empty variable
 set $check_referal "";
 # if referer is not our own hostname, set variable's value to "invalid"
 if ($invalid_referer) {
  set $check_referal "invalid";
 # if the user agent is not "Piwigo", append "not_piwigo" to the value
 if ($http_user_agent !~ "Piwigo") {
  set $check_referal "${check_referal}+not_piwigo";
 # Now, for all images, test if referer is invalid and user agent is not piwigo
 # If so, block the request
 location ~* \.(gif|png|jpe?g)$ {
   if ($check_referal = "invalid+not_piwigo") {
     return 403;
   try_files $uri $uri/ @rewrite;


Admittedly, that referer blocking looks complicated, but it’s quite effective. Of course, it’s just obscurity. If someone really want’s to download the picture, there are still ways to get it (curl –referer, ’nuff said). But for protecting against search engines, spiders and human accidental or on purpose posting on social media, this is a good measure.

Full privacy notes

If you really want full privacy for your images, there is something to take care of in Piwigo. You can set up private albums, user accounts, permissions and all, but due to performance reasons Piwigo does not validate the image’s URL alias against that permission scheme. This means: if you know the URL of the image, you can access it, no matter the permissions.

Again, this is no bug, but rather a concious desition made by Piwigo devs, because on larger sites the performance penalty would be too high. Security by obscurity is also a pattern used by big players like Facebook etc. If you know an images fib, you can access it.

If you don’t care about performance penalties or whatnot and just the heck want your images to be protected, take a look at and the accompaning blog post.

One thought to “Host your own private image gallery using Piwigo and Nginx”

  1. Thanks for your nginx conf.
    It helped me fix the option ‘question_mark_in_urls’ that didn’t work.
    And I like your hotlinks prevention.

    I just had a problem with the line “location ~ ^/(_data/(i|logs)|upload)/ {” (using piwigo 2.9.3).
    I had to change it to “location ~ ^/(_data/logs|upload)/ {” (not blocking the path /_data/i/) for the photos to be showed when browsing.

    I’m not sure of other effects that can bring though.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.