Deployment
Docker
The fastest way to get started is the pre-built image. Get the example config, fill in the required fields, then start the container:
cp config.example.toml config.toml
$EDITOR config.toml
docker compose up -ddocker-compose.yml:
services:
screenshotter:
image: ghcr.io/mkende/screenshotter:latest
restart: unless-stopped
environment:
SCREENSHOTTER_CONFIG: /etc/screenshotter/config.toml
volumes:
- ./config.toml:/etc/screenshotter/config.toml:ro
- screenshotter-data:/var/lib/screenshotter
ports:
- "127.0.0.1:8080:8080"
volumes:
screenshotter-data:Point server.storage_path and db.dsn under /var/lib/screenshotter in
the config so that the data lands on the volume.
Build from source
Requirements:
- Go 1.25 or later, with CGo enabled (the SQLite driver needs it);
- a C compiler (
gccorclang).
git clone https://github.com/mkende/screenshotter_server
cd screenshotter_server
go build -o screenshotter ./cmd/screenshotterThis produces a single self-contained binary. Database migrations run automatically on startup.
Running
./screenshotter -config /etc/screenshotter/config.tomlThe server listens on listen_addr (0.0.0.0:8080 by default) and leaves TLS
to a reverse proxy. HTTPS is required in production: the session cookie is
marked Secure, so logins over plain HTTP do not stick.
Reverse proxy
Forward to 127.0.0.1:8080 (or whatever listen_addr says), and add the
proxy’s address range to trusted_proxy in config.toml so that the
forwarded headers are honoured.
Caddy (/etc/caddy/Caddyfile):
screenshots.example.com {
reverse_proxy 127.0.0.1:8080
}nginx (/etc/nginx/sites-available/screenshotter):
server {
listen 443 ssl;
server_name screenshots.example.com;
ssl_certificate /etc/ssl/certs/screenshots.example.com.pem;
ssl_certificate_key /etc/ssl/private/screenshots.example.com.key;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 10m;
}
}Keep the proxy’s body size limit above server.max_upload_mb.
systemd
# /etc/systemd/system/screenshotter.service
[Unit]
Description=Screenshotter server
After=network.target
[Service]
ExecStart=/usr/local/bin/screenshotter -config /etc/screenshotter/config.toml
Restart=on-failure
User=screenshotter
Group=screenshotter
# Storage and DB directories must be owned by this user.
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload
systemctl enable --now screenshotterKubernetes
The server keeps its images on a volume and, with SQLite, its database too,
so run it as a single-replica Deployment with a Recreate strategy over one
ReadWriteOnce claim. The image looks for its config where the
SCREENSHOTTER_CONFIG variable points, and secrets are read from the
environment through the *_env_var settings.
apiVersion: v1
kind: ConfigMap
metadata:
name: screenshotter-config
data:
config.toml: |
canonical_address = "https://screenshots.example.com"
jwt_secret_env_var = "SCREENSHOTTER_JWT_SECRET"
# Pod CIDR of the cluster, so the ingress controller's forwarded headers
# (scheme and client address) are honoured.
trusted_proxy = ["10.0.0.0/8"]
[server]
storage_path = "/var/lib/screenshotter/images"
[db]
driver = "sqlite"
dsn = "/var/lib/screenshotter/db.sqlite"
[oidc]
enabled = true
issuer = "https://accounts.google.com"
client_id = "<your-client-id>"
client_secret_env_var = "SCREENSHOTTER_OIDC_CLIENT_SECRET"
---
apiVersion: v1
kind: Secret
metadata:
name: screenshotter-secrets
stringData:
SCREENSHOTTER_JWT_SECRET: "<openssl rand -hex 32>"
SCREENSHOTTER_OIDC_CLIENT_SECRET: "<your-client-secret>"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: screenshotter-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: screenshotter
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: screenshotter
template:
metadata:
labels:
app: screenshotter
spec:
containers:
- name: screenshotter
image: ghcr.io/mkende/screenshotter:latest
env:
- name: SCREENSHOTTER_CONFIG
value: /etc/screenshotter/config.toml
envFrom:
- secretRef:
name: screenshotter-secrets
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: config
mountPath: /etc/screenshotter
readOnly: true
- name: data
mountPath: /var/lib/screenshotter
readinessProbe:
tcpSocket:
port: http
livenessProbe:
tcpSocket:
port: http
volumes:
- name: config
configMap:
name: screenshotter-config
- name: data
persistentVolumeClaim:
claimName: screenshotter-data
---
apiVersion: v1
kind: Service
metadata:
name: screenshotter
spec:
selector:
app: screenshotter
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: screenshotter
annotations:
# ingress-nginx; keep the limit above server.max_upload_mb.
nginx.ingress.kubernetes.io/proxy-body-size: 10m
spec:
ingressClassName: nginx
tls:
- hosts: [screenshots.example.com]
secretName: screenshotter-tls
rules:
- host: screenshots.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: screenshotter
port:
name: httptrusted_proxy matters here: the ingress controller reaches the pod over
plain HTTP, and without it the server would ignore the X-Forwarded-Proto
header, see every request as http and redirect it to the canonical
https address in a loop. Use the ingress controller’s pod address range
(or the whole pod CIDR, as above).
Pin the image to a release tag rather than latest for anything you want to
upgrade deliberately. Several replicas are not supported yet: they would need
PostgreSQL and a shared ReadWriteMany volume, and the server does not
coordinate its writes across instances.
Storage and backups
Images live under server.storage_path:
<storage_path>/
<id>.png # full-size PNG
thumbs/<id>.png # 320 px thumbnail, generated at upload timeBoth directories are created on startup. Titles, source URLs and ownership are in the database, so a backup needs both the storage directory and the SQLite file (or a PostgreSQL dump).
Upgrading
Replace the binary or pull the new image and restart: schema migrations run on startup.