2025-07-22
I've been using Guix for a year or two now, and I'm taking things slowly. I have a lot of experience as a sysadmin, and I usually say I'm a good programmer for not being a programmer, but I have a lot to learn about Guile and functional programming.
I really like Guix and the manual makes me feel that it's written by people who want others to understand and use their software. That is nothing I take for granted. I mean to contribute to the manual when I'm more comfortable with Guix and Guile.
I believe my lack of experience with Guile and my stubbornness in not wanting to take things in the right order is making some things hard for me. This post is about sharing what I learned about Guix (and maybe a little Guile).
Note: This is not a perfect Nextcloud setup. Se the TODO heading below.
Here are some sources that helped me along the way;
Just download the latest version of Nextcloud and decompress it somewhere. I put it in /srv/nextcloud, where the layout will be:
/srv/nextcloud/nextcloud
- symlink to Nextcloud version used/srv/nextcloud/nextcloud-10.7.3
- the Nextcloud code/srv/nextcloud/data
- the nextcloud user and app data folder
Make sure the config and apps directory are writable by the php-fpm
user.
How to configure Nextcloud that is beyond the scope of this post.
Configuring postgresql is straight forward. I usually prefer to have my roles be named nextcloud_user
and the database be named nextcloud
, but the service creates the database and role with the same name. I'm fine with this for now.
(service postgresql-role-service-type (postgresql-role-configuration (roles (list (postgresql-role (name "nextcloud") (create-database? #t))))))
--enable-opcache
(modifying packages in guix)
The most interesting thing I learned was this neat thing at the top of my /etc/config.scm
:
(define-module (local packages) #:use-module (guix packages) #:use-module (guix utils) ; gives us substitute-keyword-arguments #:use-module (gnu packages php)) ;; This is how you build a package with an extra configuration flag ;; I love this. (define-public php-with-opcache (package (inherit php) (name "php-with-opcache") (arguments (substitute-keyword-arguments (package-arguments php) ((#:configure-flags cf) `(cons "--enable-opcache" ,cf)))))) ; ...
That's how easy it is to add a build flag to a package in Guix!
To see how something is configured, you can for example run guix edit php
.
You can also install a modified package to your profile using this command:
guix install php --with-configure-flag=php=--enable-opcache
opcache
is about caching compiled PHP code in memory instead of recompiling on each request. With the setting opcache.revalidate_freq=60
this means that a cached script will be in memory for 60 seconds before being recompiled. This includes changes to configuration files and stuff - so if you're making changes when you're testing stuff this can be very confusing.
Configure PHP-FPM and PHP:
;; TODO: memory_limit here doesn't do anything when running PHP on the ;; shell. I think it's because the PHP binary doesn't look for ;; the ini file where Guix puts it. (define %local-php-ini (plain-file "php.ini" "[PHP] memory_limit=2G zend_extension=opcache.so [opcache] opcache.memory_consumption=128 opcache.interned_strings_buffer=32 opcache.max_accelerated_files=4000 opcache.revalidate_freq=20 opcache.enable_cli=1 [PostgreSQL] pgsql.allow_persistent = On pgsql.auto_reset_persistent = Off pgsql.max_persistent = -1 pgsql.max_links = -1 pgsql.ignore_notice = 0 pgsql.log_notice = 0 ")) ;; Nextcloud complains if it can't find some environment variables set. ;; TODO: Figure out a) what paths are good here and ;; b) how to get them in there in a guilish way (define %local-php-fpm-conf (plain-file "php-fpm.conf" "[global] pid = /var/run/php8-fpm.pid error_log = /var/log/php-fpm.www.log [www] php_admin_value[memory_limit] = 2G php_admin_value[max_execution_time] = 1800 php_admin_value[error_log] = /var/log/php.www.log php_admin_flag[log_errors] = on pm = dynamic pm.max_children = 10 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 env[HOSTNAME] = 10.10.10.2 env[PATH] = /usr/local/bin:/usr/bin:/bin env[TMP] = /tmp env[TMPDIR] = /tmp env[TEMP] = /tmp user = php-fpm group = php-fpm listen = /var/run/php-fpm.sock listen.owner = php-fpm listen.group = nginx "))
Define the service:
(service php-fpm-service-type (php-fpm-configuration (socket "/var/run/php-fpm.sock") (php php-with-opcache) (php-ini-file %local-php-ini) (file %local-php-fpm-conf)))
One very confusing and frustrating moment when setting this up were the two settings php-ini-file
and file
for php-fpm-configuration
.
The description for file
is:
An optional override of the whole configuration. You can use the
mixed-text-file
function or an absolute filepath for it.
The description for php-ini-file
is:
An optional override of the default php settings. It may be any "file-like" object (see file-like objects). You can use the
mixed-text-file
function or an absolute filepath for it.
I assumed that "an optional override of the whole configuration" meant including php-ini-file
. This doesn't make sense in hindsight but the configuration files have the same format and are easy to mix up.
I think the file
setting should probably be called something else. If I feel strongly about this the next time I set this up I'll try to create a patch.
Another frustrating thing (again, because I don't want to do things in the correct order) was how to reference a file like nginx's fcgi_param
properly. With some help on #guix on IRC, this is what I ended up with.
Here are two declared locations as examples. See the whole attached file at the end for the rest of my nginx configuration.
;; fastcgi_param HTTPS on; has been commented out ;; as this is a test install without proper certificates (nginx-location-configuration (uri "~ \\.php(?:$|/)") (body `(" # Required for legacy support rewrite ^/(?!index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|ocs-provider\\/.+|.+\\/richdocumentscode(_arm64)?\\/proxy) /index.php$request_uri; fastcgi_split_path_info ^(.+?\\.php)(/.*)$; set $path_info $fastcgi_path_info; try_files $fastcgi_script_name =404;" "include " ,(file-append nginx "/share/nginx/conf/fastcgi.conf;") "fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $path_info; #fastcgi_param HTTPS on; fastcgi_param modHeadersAvailable true; # Avoid sending the security headers twice fastcgi_param front_controller_active true; # Enable pretty urls fastcgi_pass php-handler; fastcgi_intercept_errors on; fastcgi_request_buffering off; fastcgi_max_temp_file_size 0;"))) (nginx-location-configuration (uri "~ ^\\/.+[^\\/]\\.(?:css|js|mjs|woff2?|svg|gif|map)$") (body `(" try_files $uri /index.php$request_uri; expires 1d;" "include " ,(file-append nginx "/share/nginx/conf/mime.types;") "types { application/javascript js mjs; } access_log off;")))
The configuration examples for Nginx look something like this:
(nginx-location-configuration (uri "/foo") (body '("location body stuff;")))
What this means is that the argument to body
is a list, and not an expression we'd like to evaluate just yet. That's supposed to be evaluated in the nginx body code - not here. If we add (file-append nginx "/share/nginx/conf/mime.types;")
to the list above the compiler will complain that it doesn't know what the variable nginx
is, because it's being compiled "somewhere else".
; does not work (body '("include " (file-append nginx "/share/nginx/conf/mime.types;")))
The way to deal with this is to instead of using a regular quote ('
) we'll use a quasiquote (`
). This does the same thing as the quote except that it allows evaluations of expressions prefixed with an "unquote" (,
).
; works (body `("include " ,(file-append nginx "/share/nginx/conf/mime.types;")))
This is well explained in the Guix Scheme Crash Course that I should have read before attempting any of this.
Nextcloud has on it's admin dashboard a list of potential issues with the Nextcloud setup. With the configuration I've got so far there are a few things left to fix, namely;
(define-module (local packages) ;; This is how you build a package with an extra configuration flag ;; I love this. (define-public php-with-opcache (package (inherit php) (name "php-with-opcache") (arguments (substitute-keyword-arguments (package-arguments php) ((#:configure-flags cf) `(cons "--enable-opcache" ,cf)))))) (use-modules (guix channels) (guix gexp) (gnu) (gnu packages admin) (gnu packages bash) (gnu packages bootloaders) (gnu packages certs) (gnu packages compression) (gnu packages curl) (gnu packages databases) (gnu packages dns) (gnu packages file-systems) (gnu packages linux) (gnu packages php) (gnu packages ssh) (gnu packages tmux) (gnu packages version-control) (gnu packages vim) (gnu packages web) (gnu services avahi) (gnu services base) (gnu services databases) (gnu services dns) (gnu services networking) (gnu services ssh) (gnu services virtualization) (gnu services web) (local packages)) ;; Add your SSH keys here (define %ssh-key-user (plain-file "username.pub" " ssh-ed25519 ... ")) ;; TODO: memory_limit here doesn't do anything when running PHP on the ;; shell. I think it's because the PHP binary doesn't look for ;; the ini file where Guix puts it. (define %local-php-ini (plain-file "php.ini" "[PHP] memory_limit=2G zend_extension=opcache.so [opcache] opcache.memory_consumption=128 opcache.interned_strings_buffer=32 opcache.max_accelerated_files=4000 opcache.revalidate_freq=20 opcache.enable_cli=1 [PostgreSQL] pgsql.allow_persistent = On pgsql.auto_reset_persistent = Off pgsql.max_persistent = -1 pgsql.max_links = -1 pgsql.ignore_notice = 0 pgsql.log_notice = 0 ")) ;; Nextcloud complains if it can't find some environment variables set. ;; TODO: Figure out a) what paths are good here and ;; b) how to get them in there in a guilish way (define %local-php-fpm-conf (plain-file "php-fpm.conf" "[global] pid = /var/run/php8-fpm.pid error_log = /var/log/php-fpm.www.log [www] php_admin_value[memory_limit] = 2G php_admin_value[max_execution_time] = 1800 php_admin_value[error_log] = /var/log/php.www.log php_admin_flag[log_errors] = on pm = dynamic pm.max_children = 10 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 env[HOSTNAME] = 10.10.10.2 env[PATH] = /usr/local/bin:/usr/bin:/bin env[TMP] = /tmp env[TMPDIR] = /tmp env[TEMP] = /tmp user = php-fpm group = php-fpm listen = /var/run/php-fpm.sock listen.owner = php-fpm listen.group = nginx ")) (operating-system (locale "en_US.utf8") (timezone "Europe/Stockholm") (host-name "nextcloud") (users (cons (user-account (name "username") (group "users") (supplementary-groups '("wheel" "netdev")) (shell (file-append bash "/bin/bash"))) %base-user-accounts)) (packages (append (list curl dnsmasq git neovim php postgresql-16 tcpdump tmux unzip net-tools) %base-packages)) (services (append (modify-services base-services (guix-service-type config => (guix-configuration (inherit config) (discover? #t)))) (list (service static-networking-service-type (list (static-networking (addresses (list (network-address (device "ens3") (value "10.10.10.30/24")))) (routes (list (network-route (destination "default") (gateway "10.10.10.1")))) (name-servers '("127.0.0.1"))))) (service openssh-service-type (openssh-configuration (openssh openssh-sans-x) (permit-root-login 'prohibit-password) (authorized-keys `(("username" , %ssh-key-user))) (port-number 22))) (service avahi-service-type) (service qemu-guest-agent-service-type) (service postgresql-service-type (postgresql-configuration (postgresql postgresql-16) (data-directory "/srv/postgresql"))) (service postgresql-role-service-type (postgresql-role-configuration (roles (list (postgresql-role (name "nextcloud") (create-database? #t)))))) (service dnsmasq-service-type (dnsmasq-configuration (no-resolv? #f) (query-servers-in-order? #t) (cache-size 5000) (extra-options '("--log-queries")) (listen-addresses '("127.0.0.1")) (servers '("10.10.10.1")))) ;; I prefer not having the version number in the socket path, as it has ;; to be configured in multiple places. (service php-fpm-service-type (php-fpm-configuration (socket "/var/run/php-fpm.sock") (php php-with-opcache) (php-ini-file %local-php-ini) (file %local-php-fpm-conf))) ;; Most settings as recommended from the Nextcloud project. (service nginx-service-type (nginx-configuration (upstream-blocks (list (nginx-upstream-configuration (name "php-handler") (servers (list "unix:/var/run/php-fpm.sock"))))) (server-blocks (list (nginx-server-configuration (server-name '("nextcloud.example.com")) (root "/srv/nextcloud/nextcloud") (locations (list (nginx-location-configuration (uri "^~ /.well-known") (body '(" location = /.well-known/carddav { return 301 /remote.php/dav; } location = /.well-known/caldav { return 301 /remote.php/dav; } return 301 /index.php$request_uri;"))) ;; fastcgi_param HTTPS on; has been commented out ;; as this is a test install without proper certificates (nginx-location-configuration (uri "~ \\.php(?:$|/)") (body `(" # Required for legacy support rewrite ^/(?!index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|ocs-provider\\/.+|.+\\/richdocumentscode(_arm64)?\\/proxy) /index.php$request_uri; fastcgi_split_path_info ^(.+?\\.php)(/.*)$; set $path_info $fastcgi_path_info; try_files $fastcgi_script_name =404;" "include " ,(file-append nginx "/share/nginx/conf/fastcgi.conf;") "fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $path_info; #fastcgi_param HTTPS on; fastcgi_param modHeadersAvailable true; # Avoid sending the security headers twice fastcgi_param front_controller_active true; # Enable pretty urls fastcgi_pass php-handler; fastcgi_intercept_errors on; fastcgi_request_buffering off; fastcgi_max_temp_file_size 0;"))) (nginx-location-configuration (uri "~ ^\\/.+[^\\/]\\.(?:css|js|mjs|woff2?|svg|gif|map)$") (body `(" try_files $uri /index.php$request_uri; expires 1d;" "add_header Referrer-Policy \"no-referrer\" always; add_header X-Content-Type-Options \"nosniff\" always; add_header X-Download-Options \"noopen\" always; add_header X-Frame-Options \"SAMEORIGIN\" always; add_header X-Permitted-Cross-Domain-Policies \"none\" always; add_header X-Robots-Tag \"noindex,nofollow\" always; add_header X-XSS-Protection \"1; mode=block\" always; #add_header Strict-Transport-Security \"max-age=15768000; includeSubDomains; preload;\" always;" "include " ,(file-append nginx "/share/nginx/conf/mime.types;") "types { application/javascript js mjs; } access_log off;"))) (nginx-location-configuration (uri "~ ^\\/.+[^\\/]\\.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$") (body '(" try_files $uri /index.php$request_uri; expires 1d; access_log off;"))) (nginx-location-configuration (uri "~ ^\\/(?:index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|oc[ms]-provider\\/.+|.+\\/richdocumentscode\\/proxy)\\.php(?:$|\\/)") (body `(" fastcgi_split_path_info ^(.+?\\.php)(\\/.*|)$; set $path_info $fastcgi_path_info; try_files $fastcgi_script_name =404;" "include " ,(file-append nginx "/share/nginx/conf/fastcgi.conf;") "fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $path_info; #fastcgi_param HTTPS on; fastcgi_param modHeadersAvailable true; fastcgi_param front_controller_active true; fastcgi_pass php-handler; fastcgi_intercept_errors on; fastcgi_request_buffering off;"))) (nginx-location-configuration (uri "/remote") (body '("return 301 /remote.php$request_uri;"))) (nginx-location-configuration (uri "/") (body '("try_files $uri $uri/ /index.php$request_uri;"))))) (listen '("80")) (raw-content '(" add_header Referrer-Policy \"no-referrer\" always; add_header X-Content-Type-Options \"nosniff\" always; add_header X-Download-Options \"noopen\" always; add_header X-Frame-Options \"SAMEORIGIN\" always; add_header X-Permitted-Cross-Domain-Policies \"none\" always; add_header X-Robots-Tag \"noindex,nofollow\" always; add_header X-XSS-Protection \"1; mode=block\" always; #add_header Strict-Transport-Security \"max-age=15768000; includeSubDomains; preload;\" always;")) (index '("index.php" "index.html" "/index.php$request_uri")) (ssl-certificate #f) (ssl-certificate-key #f))))))))) (bootloader (bootloader-configuration (bootloader grub-bootloader) (targets '("/dev/vda")) (terminal-outputs '(console)))) (kernel-arguments (list "console=ttyS0,115200")) (file-systems (cons* (file-system (mount-point "/") (device "/dev/vda2") (type "ext4")) %base-file-systems)))