| .gitignore | ||
| euro-office.container | ||
| LICENSE | ||
| nextcloud-app.container | ||
| nextcloud-caddy.container | ||
| nextcloud-db.container | ||
| nextcloud-redis.container | ||
| nextcloud.pod | ||
| README.md | ||
Nextcloud podman quadlet setup
Where these files go
Copy all *.pod and *.container files to:
~/.config/containers/systemd/
Then run systemctl --user daemon-reload and systemctl --user start nextcloud-pod.service
(the pod's generated unit name is <pod-basename>-pod.service, e.g. nextcloud-pod.service).
1. Fix the host paths first
Your original YAML mixed relative hostPaths (db, html, caddy/caddy_data,
caddy/Caddyfile, syncthing_config) with absolute ones (/mnt/nextcloud_data, ...).
Quadlets need absolute paths — there's no "current directory" once systemd runs
these as a service. I mapped the relative ones under %h/nextcloud-app/ (i.e.
$HOME/nextcloud-app/...) as a placeholder:
~/nextcloud-app/db
~/nextcloud-app/html
~/nextcloud-app/caddy/caddy_data
~/nextcloud-app/caddy/Caddyfile
~/nextcloud-app/syncthing_config
Edit the Volume= lines in each .container file to point at wherever these actually
live on your host, then create the directories / move existing data into place before
first start.
The two paths that were already absolute (/mnt/nextcloud_data and
/mnt/nextcloud_data/j.p.hagedoorn/files) were kept as-is.
2. SELinux labels preserved
:Z(private, unshared label) — db, syncthing_config, caddy_data, Caddyfile: only one container touches each of these.:z(shared label) — html and data: bothnextcloud-appandnextcloud-caddy(ornextcloud-syncthing) need access to the same directory, so it must be shared.
This mirrors your original manifest exactly.
3. Secrets (individual podman secrets, matching your forgejo pattern)
Your original k8s manifest used envFrom: secretRef to inject a whole set of key/value
pairs at once (e.g. MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER,
MYSQL_PASSWORD for nextcloud-db-secrets; REDIS_HOST_PASSWORD for
nextcloud-redis-secrets). Following the same approach as your forgejo pod, each value
now gets its own podman secret, created from a plaintext file on the host via
ExecStartPre= in nextcloud.pod, and injected with Secret=name,type=env,target=VAR
in whichever .container file needs it.
Assumed key names — I used the standard mariadb/nextcloud-image env vars. Adjust the
target= values in the .container files if your actual k8s Secrets used different
keys:
| Secret file | Target env var | Used by |
|---|---|---|
mysql-root-password.secret |
MYSQL_ROOT_PASSWORD |
db only |
mysql-database.secret |
MYSQL_DATABASE |
db, app |
mysql-user.secret |
MYSQL_USER |
db, app |
mysql-password.secret |
MYSQL_PASSWORD |
db, app |
redis-password.secret |
REDIS_HOST_PASSWORD |
redis, app |
Create the source files — each contains just the raw value, no KEY= prefix:
mkdir -p ~/.config/containers/systemd/secrets
echo -n 'super-secret-root-pw' > ~/.config/containers/systemd/secrets/mysql-root-password.secret
echo -n 'nextcloud' > ~/.config/containers/systemd/secrets/mysql-database.secret
echo -n 'nextcloud' > ~/.config/containers/systemd/secrets/mysql-user.secret
echo -n 'super-secret-db-pw' > ~/.config/containers/systemd/secrets/mysql-password.secret
echo -n 'super-secret-redis-pw' > ~/.config/containers/systemd/secrets/redis-password.secret
chmod 600 ~/.config/containers/systemd/secrets/*.secret
nextcloud.pod's [Service] section runs podman secret create --replace <name> <file>
for each of these before the pod starts, so editing a file and restarting the pod
(systemctl --user restart nextcloud-pod.service) picks up new values — same workflow as
your forgejo database-passwd.secret.
Once a secret is created, podman stores it in its own secrets backend — you can delete
the source .secret files afterward if you like, they're only read again on
ExecStartPre (i.e. every pod (re)start), so it's simplest to just leave them in place
and treat ~/.config/containers/systemd/secrets/ itself as the sensitive directory (keep
its permissions tight, e.g. chmod 700 on the directory too).
4. The redis-server --requirepass $(REDIS_HOST_PASSWORD) trick
Kubernetes expands $(VAR) in args for you. Podman's Exec= does not do shell
expansion — it passes arguments literally to the entrypoint. nextcloud-redis.container
wraps the command in sh -c "..." so the shell performs the substitution at container
start, using the REDIS_HOST_PASSWORD value injected via the redis-password.secret
podman secret.
5. Startup ordering
Kubernetes pods start all containers together with no ordering guarantees beyond what
the app handles via retries. I added explicit After=/Wants= so nextcloud-app waits
for the db and redis containers, and nextcloud-caddy waits for nextcloud-app — mostly
to avoid noisy failed-connection logs on cold start. Nextcloud/PHP-FPM will still need to
retry internally if MariaDB isn't fully ready to accept connections yet (this is normal
and not something quadlets alone can fully solve, same as in Kubernetes).
6. Ports
Ports are published at the pod level (nextcloud.pod), not per-container, since all
containers in a pod share one network namespace — this is the same model Kubernetes
uses. I split syncthing's 22000 into explicit /tcp and /udp and marked 21027 (local
discovery) as /udp, since your original YAML didn't specify protocol and syncthing
actually needs both.