After failing to adequately secure Photoprism as a public gallery, I gave up and caved in to using Hugo to generate a static site using hugo-theme-gallery.
To fit nicely into a Woodpecker Pipelines-based deployment, I made the potentially bad decision to stuff all the photos into a Git repo. Photos and index.md / _index.md are added as needed; on push, a Woodpecker pipeline builds and deploys the site to the final server in AWS EC2.
Hugo configuration
See also
[[Hugo actually getting started]] for initial setup
In hugo.toml:
baseURL = "https://gallery.example.com"
languageCode = "en-us"
title = "Photo Gallery"
disableKinds = ["taxonomy", "term"]
[module]
[[module.imports]]
path = "github.com/nicokaiser/hugo-theme-gallery/v4"Ensure that there are no files in any of the following:
layouts/static/themes/(this might get populated automatically by hugo’s build process)
Content goes into content/, starting with _index.md (note the leading underscore!) as the “homepage.” This top-level _index.md contains only:
---
title: "Photo Gallery"
---
No other content. The theme takes care of generating the page.
Album layout
Albums are generated directly from folders. content/ is the home page, and within that:
hugo.toml
content/
| _index.md # home page
| album1/
| | index.md # an album's "page"
| | photo1.jpg
| | photo2.jpg
| album2/
| | _index.md # a high-level album's "page"
| | album3/
| | | index.md # a nested album's "page"
| | | photo3.jpg
| | | photo4.jpg
| | album4/
| | | index.md
| | | photo5.jpg
| | | photo6.jpg
The actual “why” about index.md vs _index.md has to do with Hugo’s “page bundles” vs “leaf bundles.” TL;DR: use _index.md if that album will have sub-albums within it; otherwise, index.md.
In both cases (underscored and not-underscored) index files, the content is simply some YAML frontmatter declaring the title:
---
title: "Night Sky"
---
Automatic Deployment
Similarly to this notebook, the static site is first built on a server on my home network before upload to the EC2 host (which doesn’t have nearly enough compute power to do the build in any reasonable timeframe).
Woodpecker build process
Following the linked-above notebook logic, the gallery is built locally and then uploaded using a 2-job Woodpecker Pipelines:
# file: .woodpecker/10-hugo-build.yaml
labels:
# do the CPU-intensive building on a more powerful server
# on my home network
hostname: microsrv
when:
- event: push
branch: master
- event: manual
steps:
# instead of a "don't post my full web URL" check, this is a
# "don't post my location" guard. Any GPS metadata is stripped
# at build time; the files in the git repo *ARE* geotagged, but
# it never makes it online.
- name: wipe-location
image: alpine
commands:
- apk update && apk add --no-cache exiftool
- cd gallery/content
- exiftool -gps*= $(find . -type f -name '*.JPG')
- rm $(find . -type f -name '*.JPG_original')
# do the actual build
- name: build
image: ghcr.io/gohugoio/hugo:latest
commands:
- cd gallery
- hugo build
- ls -lahR public
# static site is built; deploy it to the EC2 server
- name: upload-to-rain
image: appleboy/drone-scp
settings:
host: 192.168.37.1
port: 22
username: misha
target: /home/misha/landing/gallery/
rm: true
source: gallery/public/
key:
from_secret: rain_ssh_key
passphrase: mmmmm_nope_not_putting_this_online_sorryOnce that runs, the deployment on the EC2 server runs:
# file: 20-deploy.yaml
labels:
# run on the EC2 host to finalize deployment
hostname: rain
when:
- event: push
branch: master
- event: manual
depends_on:
- 10-hugo-build
# no need to re-clone the repo, the prebuilt site has already been
# dropped off for us to continue from
skip_clone: true
steps:
- name: deploy_to_site
image: alpine
volumes:
- /opt/docker/gallery:/dest
- /home/misha/landing/gallery/gallery/public:/src
commands:
- cp -r /src/* /dest/
# this is a big chunk of drive space; don't keep duplicates around
- rm -rf /src/*Site hosting
I’m using Compose files on the EC2 server to host the static site:
services:
gallery:
container_name: gallery
image: nginx
restart: unless-stopped
volumes:
# this is where the 20-deploy.yaml Woodpecker job dropped
# the built site
- /opt/docker/gallery:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/nginx.conf:ro
mem_limit: 10mThe nginx.conf file:
events {
worker_connections 1024;
}
http {
sendfile on;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
error_page 404 /404.html;
location / {
include mime.types;
try_files $uri $uri/ =404;
}
}
}
Reverse proxy
Last but not least, access to the container is routed through Caddy as a reverse proxy using the following config:
{
admin off
email misha@example.com
}
# Rate-limiting for public-facing hosts to prevent overloading
# the poor little 1GB-of-RAM EC2 so I can keep it in the (nearly)
# free tier
(public) {
rate_limit {
distributed
zone everyone {
key static
events 100
window 5s
}
zone individual {
key {remote_host}
events 30
window 1s
}
log_key
}
}
gallery.example.com {
import public
reverse_proxy http://gallery
}