Installation

To get the full pxvoid experience, it should be running on a server connected to the internet. Performance isn't the deciding factor here; rather, depending on your plans, you should ensure you have enough free storage space, as images require room. In most cases, a simple VPS with 8GB of RAM and 2 cores is sufficient. Since pxvoid is written in Python and doesn't use any esoteric libraries, it should run on any standard Linux server system. You also need a domain or subdomain for your gallery like pxvoid.your-domain.tld or px.your-domain.tld and the DNS entries that point on your server.

Here are two ways to install and configure pxvoid on NixOS and other Linux distributions (like Debian or Ubuntu)

NixOS

Within NixOS it is easy to build a working pxvoid gallery. We just create a .nix file with all the configuration stuff, create some folders, set the permissions and start pxvoid as a service within your configuration.nix. After the rebuild everything will work like a charm.

1. CREATE PROJECT DIRECTORY

It's best to create a separate directory for pxvoid so you have full control over where your files are located. This makes debugging much easier later on.

mkdir -p /srv/pxvoid/{app,media/photos,keys,db}

2. CLONE PXVOID

In the next step, we retrieve the pxvoid code from Codeberg and clone it into our newly created directory.

cd /srv/pxvoid/app
git clone https://codeberg.org/0x17/pxvoid.git .

3. GENERATE ACTIVITYPUB KEY

To ensure your pxvoid instance is properly federated, it needs two keys: a private.pem and a public.pem. We create these with the following commands:

cd /srv/pxvoid/keys
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
chmod 600 private.pem
chmod 644 public.pem

4. CREATE ENVIRONMENT FILE

We create the .env file with the following content

vim /srv/pxvoid/app/.env
PXVOID_BASE_URL=https://your-domain.tld
PXVOID_TOKEN=SUPERSAFETOKEN
ZEROFRAME_DB=/srv/pxvoid/db/gallery.db

Then we assign the appropriate access rights to the .env file.

chmod 600 /srv/pxvoid/app/.env

5. THE NIX FILE

In the next step, we create the basic nix file which configures the starting of the service, virtual host, Python packages, and everything else:

{ config, lib, pkgs, ... }:

let
  cfg = config.services.pxvoid;

  pxvoidPython = pkgs.python312.withPackages (ps: with ps; [
    fastapi
    httpx
    uvicorn
    jinja2
    python-multipart
    cryptography
    requests
    pillow
  ]);
in
{
  options.services.pxvoid = {
    enable = lib.mkEnableOption "pxvoid service";

    domain = lib.mkOption {
      type = lib.types.str;
      example = "pxvoid.example.org";
      description = "Public domain for pxvoid nginx virtual host.";
    };

    appDir = lib.mkOption {
      type = lib.types.str;
      default = "/srv/pxvoid/app";
      description = "Path to pxvoid git checkout.";
    };

    envFile = lib.mkOption {
      type = lib.types.str;
      default = "/srv/pxvoid/app/.env";
      description = "Environment file loaded by systemd (PXVOID_BASE_URL, PXVOID_TOKEN, ZEROFRAME_DB).";
    };

    user = lib.mkOption {
      type = lib.types.str;
      default = "pxvoid";
      description = "System user running pxvoid.";
    };

    group = lib.mkOption {
      type = lib.types.str;
      default = "pxvoid";
      description = "System group running pxvoid.";
    };

    mediaDir = lib.mkOption {
      type = lib.types.str;
      default = "/srv/pxvoid/media";
      description = "Base media directory (photos in media/photos).";
    };

    dbDir = lib.mkOption {
      type = lib.types.str;
      default = "/srv/pxvoid/db";
      description = "Directory that stores gallery.db.";
    };

    keyDir = lib.mkOption {
      type = lib.types.str;
      default = "/srv/pxvoid/keys";
      description = "Directory for ActivityPub private.pem/public.pem.";
    };

    acmeEmail = lib.mkOption {
      type = lib.types.str;
      default = "admin@example.org";
      description = "Email for ACME certificate registration.";
    };
  };

  config = lib.mkIf cfg.enable {
    users.users.${cfg.user} = {
      isSystemUser = true;
      group = cfg.group;
      home = "/srv/pxvoid";
      createHome = false;
    };
    users.groups.${cfg.group} = {};

    systemd.tmpfiles.rules = [
      "d ${cfg.mediaDir} 0750 ${cfg.user} ${cfg.group} -"
      "d ${cfg.mediaDir}/photos 0750 ${cfg.user} ${cfg.group} -"
      "d ${cfg.dbDir} 0750 ${cfg.user} ${cfg.group} -"
      "d ${cfg.keyDir} 0750 ${cfg.user} ${cfg.group} -"
    ];

    systemd.services.pxvoid = {
      description = "pxvoid FastAPI service";
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];

      serviceConfig = {
        Type = "simple";
        User = cfg.user;
        Group = cfg.group;
        WorkingDirectory = cfg.appDir;
        EnvironmentFile = cfg.envFile;
        Environment = [ "PYTHONPATH=${cfg.appDir}" ];
        ExecStart = "${pxvoidPython}/bin/uvicorn --app-dir ${cfg.appDir} app.main:app --host 127.0.0.1 --port 8000";
        Restart = "always";
        RestartSec = "3";
      };
    };

    services.nginx.enable = true;
    services.nginx.recommendedProxySettings = true;

    services.nginx.virtualHosts.${cfg.domain} = {
      forceSSL = true;
      enableACME = true;
      locations."/" = {
        proxyPass = "http://127.0.0.1:8000";
        extraConfig = ''
          client_max_body_size 25M;
        '';
      };
    };

    security.acme = {
      acceptTerms = true;
      defaults.email = cfg.acmeEmail;
    };
  };
}

In the first part of the file, we declare the option that we will later import into our configuration.nix, and in the second section, we set the parameters for the actual pxvoid configuration. The file now needs to be imported and made available. Therefore, two more entries are added to the central configuration.nix:

in {
    imports = [
        ./path/to/pxvoid.nix
    ];

And we start the service itself as follows:

services.pxvoid = {
    enable = true;
    domain = "pxvoid.your-domain.tld";
    appDir = "/srv/pxvoid/";
    envFile = "/srv/pxvoid/app/.env";
    acmeEmail = "mail@your-domain.tld"

6. PERMISSIONS AND REBUILD

If we now perform a rebuild, it might complete without problems, but pxvoid won't start. The problem here is that the user isn't yet assigned to our app directory. Therefore, we'll now assign the correct permissions and then rebuild the system:

chown -R pxvoid:pxvoid /srv/pxvoid/media /srv/pxvoid/db /srv/pxvoid/keys
chmod 750 /srv/pxvoid/media /srv/pxvoid/media/photos /srv/pxvoid/db /srv/pxvoid/keys

and the rebuild:

nixos-rebuild switch

other Linux Distributions (Debian-like)

These installation instruction is Debian and debian-based Systems. If you use another Distro you have to replace commands like apt with your systemequivalent like rpm, pacman and so on. This installation guide is not tested yet. If you have trouble or got errors while installing pxvoid you can contribute a working guide.

1. INSTALL DEPENDENCIES:

First, we install the necessary base so that pxvoid can run:

sudo apt update 
sudo apt install -y python3 python3-venv python3-pip nginx openssl

2. DIRECTORY STRUCTURE

pxvoid needs clear paths for media, keys and database

sudo mkdir -p /srv/pxvoid/{app,media/photos,keys,db}
sudo chown -R $USER:$USER /srv/pxvoid

3. DEPLOYMENT

Now you can clone pxvoid to the directory and setup venv and the python requirements:

cd /srv/pxvoid/app
git clone https://codeberg.org/0x17/pxvoid.git .
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt

4. GENERATE ACTIVITYPUB KEY

You have to generate RSA Keys. Without the keys your federation fails:

cd /srv/pxvoid/keys
openssl genrsa -out private.pem 2048
opensl rsa -in private.pem -pubout -out public.pem
chmod 600 private.pem
chmod 644 public.pem

5. CREATE ENVIRONMENT FILE

Create a .env file (i.e.: in /srv/pxvoid/app/) with following content:

PXVOID_BASE_URL=https://deine-domain.tld
PXVOID_TOKEN=SET-STRONG-TOKEN-HERE
ZEROFRAME_DB=/srv/pxvoid/db/gallery.db

Please note the following:
- PXVOID_BASE_URL must point exactly to your public URL.
- PXVOID_TOKEN protects the Upload and Delete API
- PXVOID_DB explicitly sets the DB path to the server path

6. START APP WITH UVICORN

In this step we start pxvoid via uvicorn:

cd /srv/pxvoid/app
source .venv/bin/activate
export $(grep -v '^#' .env | xargs)
uvicorn app.main:app --host 127.0.0.1 --port 8000

After that, the app should run on localhost. You can test this with the following one-liner:

curl -I http://127.0.0.1:8000/

If you get HTTP/1.1 200 OK, then pxvoid is already running.

7. SYSTEMD SERVICE

You simply create a new file using your preferred editor:

nvim /etc/systemd/system/pxvoid.service

The contents of the file look like this:

[Unit]
            Description=pxvoid FastAPI service
            After=network.target

            [Service]
            User=www-data
            Group=www-data
            WorkingDirectory=/srv/pxvoid/app
            EnvironmentFile=/srv/pxvoid/app/.env
            ExecStart=/srv/pxvoid/app/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
            Restart=always
            RestartSec=3

[Install]
WantedBy=multi-user.target

After that, you can set the correct permissions and start the service:

sudo chown -R www-data:www-data /srv/pxvoid/media /srv/pxvoid/db /srv/pxvoid/keys
sudo chmod 750 /srv/pxvoid/media /srv/pxvoid/media/photos /srv/pxvoid/db /srv/pxvoid/keys
sudo systemctl daemon-reload
sudo systemctl enable --now pxvoid
sudo systemctl status pxvoid

You can see if pxvoid is running correctly in the logs:

journalctl -u pxvoid -f

8. NGINX REVERSE PROXY

Create the following file:

nvim /etc/nginx/sites-available/pxvoid
server {
    listen 80;
    server_name deine-domain.tld;

    client_max_body_size 25M;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}

If the config is written correctly, you can start the proxy as follows:

sudo ln -s /etc/nginx/sites-available/pxvoid /etc/nginx/sites-enabled/pxvoid
sudo nginx -t
sudo systemctl reload nginx

Of course, you should expose your pxvoid instance to the outside via TLS:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d deine-domain.tld

Congratulations! You should now have a working pxvoid gallery of your own!