π Installing gokapi¶
gokapi is a simple, self-hosted file sharing server with automatic expiration and encryption support. Ideal for teams or individuals who want privacy, control, and no clutter.
Features:
- β³ File shares automatically expire after a chosen number of downloads or days
- π₯ User roles provide controlled access so only registered users can upload
- π Identical files are deduplicated to avoid wasting storage space
- βοΈ Files can be stored in AWS S3 or any S3-compatible service if desired
- π Uploads can be encrypted, including full end-to-end encryption
Info
The project is open-source and can be downloaded here: https://github.com/Forceu/Gokapi.
π₯ Installation¶
This section describes how to install and run Podman services using systemd Quadlet, enabling containers to restart automatically. It allows the service to run in its own isolated rootless environment with a dedicated Linux user.
π Requirements¶
βοΈ Configuration¶
π Service Setup¶
Info
Please follow the following installation steps:
Define Service Variables
# define the service name
SERVICE_NAME=gokapi
Initialize Service Environment
# built-in bash safety and SERVICE_NAME validation
set -euo pipefail; [ -n "${SERVICE_NAME:-}" ] || { echo "SERVICE_NAME is empty"; exit 1; }
# automatically generate variables
SERVICE_HOME="/media/ssd/podman-users/${SERVICE_NAME}"
SERVICE_DIR="/media/ssd/podman/${SERVICE_NAME}"
SERVICE_USER="${SERVICE_NAME}_svc"
SERVICE_ADMIN="${SERVICE_NAME}_admins"
π Service Isolation¶
Configure Rootless Podman UID/GID Namespace Mappings
# allocate rootless Podman UID/GID mappings if missing
SUBID_SIZE=65536
allocate_subids_if_missing() {
local user="$1"
local start
if ! grep -q "^${user}:" /etc/subuid; then
start="$(awk -F: 'BEGIN { max = 100000 } { end = $2 + $3; if (end > max) max = end } END { print max }' /etc/subuid)"
sudo usermod --add-subuids "${start}-$((start + SUBID_SIZE - 1))" "$user"
fi
if ! grep -q "^${user}:" /etc/subgid; then
start="$(awk -F: 'BEGIN { max = 100000 } { end = $2 + $3; if (end > max) max = end } END { print max }' /etc/subgid)"
sudo usermod --add-subgids "${start}-$((start + SUBID_SIZE - 1))" "$user"
fi
}
allocate_subids_if_missing "${SERVICE_USER}"
# verify rootless Podman UID/GID namespace mappings exist
grep -q "^${SERVICE_USER}:" /etc/subuid || { echo "ERROR: missing subuid mappings for ${SERVICE_USER}"; exit 1; }
grep -q "^${SERVICE_USER}:" /etc/subgid || { echo "ERROR: missing subgid mappings for ${SERVICE_USER}"; exit 1; }
# validate that all subordinate ID ranges in a file are non-overlapping
validate_subid_ranges() {
local file="$1"
awk -F: '
{
start = $2
end = $2 + $3 - 1
for (i = 1; i <= count; i++) {
if (start <= ends[i] && end >= starts[i]) {
printf "ERROR: overlapping ranges in %s\n", FILENAME
printf " %s:%s:%s overlaps %s:%s:%s\n",
$1, $2, $3,
names[i], starts[i], sizes[i]
exit 1
}
}
count++
names[count] = $1
starts[count] = start
ends[count] = end
sizes[count] = $3
}
' "$file"
}
validate_subid_ranges /etc/subuid
validate_subid_ranges /etc/subgid
# configure the rootless storage driver BEFORE Podman first initializes its storage.
# fuse-overlayfs is a reliable rootless overlay backend and avoids kernel-version
# quirks of native rootless overlay on ARM/Armbian. It is optional with the default
# userns used here, but recommended; set it before the first storage init so no
# 'podman system reset' is needed later.
sudo -u "${SERVICE_USER}" mkdir -p "${SERVICE_HOME}/.config/containers"
sudo -u "${SERVICE_USER}" tee "${SERVICE_HOME}/.config/containers/storage.conf" >/dev/null <<'EOF'
[storage]
driver = "overlay"
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
EOF
# rebuild rootless Podman state to ensure existing containers/storage use current namespace mappings
# use a login shell (-i) so HOME points at the service user's home
# (storage now initializes with fuse-overlayfs; no 'podman system reset' needed)
sudo -iu "${SERVICE_USER}" podman system migrate
Configure Secure Service Directory Permissions
# Apply the admin/service ACL policy to one directory tree, deterministically.
# $1 = tree to treat
# $2 = (optional) a sub-path to skip entirely (e.g. a container data subtree)
#
# Why setfacl, not chmod: on an ACL'd tree, chmod edits only the mask, never group::,
# so stale execute bits survive. setfacl sets every entry explicitly (mask included),
# lowercase β the result is reproducible and no execute bit can resurface.
apply_acl_tree() {
local tree="$1"
local skip="${2:-}"
local prune=()
[ -n "$skip" ] && prune=( -path "$skip" -prune -o ) # skip this subtree if a 2nd arg is given
sudo find "$tree" "${prune[@]}" -exec chown "${SERVICE_USER}:${SERVICE_ADMIN}" {} + # service user owns; admin group is the owning group
# directories β access ACL (perms on the existing dirs)
sudo find "$tree" "${prune[@]}" -type d -exec setfacl -m g:${SERVICE_ADMIN}:rwx {} + # admin group: access existing dirs
sudo find "$tree" "${prune[@]}" -type d -exec setfacl -m u:${SERVICE_USER}:rwx {} + # service user: access existing dirs
sudo find "$tree" "${prune[@]}" -type d -exec setfacl -m u::rwx,g::rwx,o::-,m::rwx {} + # owner/group rwx, deny others, pin mask rwx
# directories β default ACL (inherited by NEW files/dirs created later)
sudo find "$tree" "${prune[@]}" -type d -exec setfacl -d -m g:${SERVICE_ADMIN}:rwx {} + # admin group: inherit access on new items
sudo find "$tree" "${prune[@]}" -type d -exec setfacl -d -m u:${SERVICE_USER}:rwx {} + # service user: inherit access on new items
sudo find "$tree" "${prune[@]}" -type d -exec setfacl -d -m u::rwx,g::rwx,o::- {} + # default owner/group/other for new items
# files β access ACL (rw only, never executable)
sudo find "$tree" "${prune[@]}" -type f -exec setfacl -m g:${SERVICE_ADMIN}:rw {} + # admin group: access existing files
sudo find "$tree" "${prune[@]}" -type f -exec setfacl -m u:${SERVICE_USER}:rw {} + # service user: access existing files
sudo find "$tree" "${prune[@]}" -type f -exec setfacl -m u::rw,g::rw,o::-,m::rw {} + # owner/group rw, deny others, pin mask rw
sudo find "$tree" "${prune[@]}" -type d -exec chmod g+s {} + # setgid: new items inherit the group (a MODE bit; ACLs can't set it)
}
# --- SERVICE_HOME: Podman's private runtime home ------------------------------
# Own the home + set setgid, then ACL ONLY the .config subtree (containers.conf,
# storage.conf, and the *.network / *.container quadlets you hand-edit) so admins
# can edit them sudo-less.
#
# Leave ~/.local (storage) and ~/.cache to Podman: ACLs on the overlay store break
# the runtime ("OCI permission denied" on the `merged` mount). Admin reaches them via sudo.
sudo chown "${SERVICE_USER}:${SERVICE_ADMIN}" "${SERVICE_HOME}" # service user owns; admin group is the owning group
sudo chmod u=rwx,g=rwx,o=,g+s "${SERVICE_HOME}" # owner/admin access; deny others; inherit group on new items
apply_acl_tree "${SERVICE_HOME}/.config" # .config subtree: full sudo-less admin ACL treatment
# --- bind-mount data dirs: created + owned BEFORE the SERVICE_DIR ACL pass ---------
# Each container writes as a mapped subuid, so its data dir must be owned by that subuid
# (chown as root; only root crosses the id range). We create + own them FIRST so they
# neither inherit SERVICE_DIR's default ACL (inheritance happens at creation) nor get
# walked by apply_acl_tree (which prunes data/).
#
# No ACLs here: Postgres refuses a group-accessible PGDATA. Admin reaches them via sudo;
# back Postgres up with pg_dumpall, not a raw file copy.
SUBUID_BASE="$(awk -F: -v u="${SERVICE_USER}" '$1==u {print $2}' /etc/subuid)"
SUBGID_BASE="$(awk -F: -v u="${SERVICE_USER}" '$1==u {print $2}' /etc/subgid)"
# data subdir : in-container uid/gid it must be owned by
data_dirs=(
"gokapi:1000" # /app/data β matches User=1000 in the quadlet
"config:1000" # /app/config β matches User=1000 in the quadlet
)
mkdir -p "${SERVICE_DIR}/data" # the data/ parent: plain, no ACL
sudo chown "${SERVICE_USER}:${SERVICE_ADMIN}" "${SERVICE_DIR}/data" # owner/admin
sudo chmod 0750 "${SERVICE_DIR}/data" # children are set individually below
for entry in "${data_dirs[@]}"; do
sub="${entry%%:*}" # subdir name
ids="${entry#*:}" # "uid" or "uid:gid"
uid="${ids%%:*}" # uid
gid="${ids#*:}"; [ "$gid" = "$ids" ] && gid="$uid" # gid (defaults to uid)
dir="${SERVICE_DIR}/data/${sub}"
sudo mkdir -p "$dir"
sudo chown -R "$((SUBUID_BASE + uid - 1)):$((SUBGID_BASE + gid - 1))" "$dir" # map both to host subids
sudo chmod 0700 "$dir"
done
# --- SERVICE_DIR: application data, env files, configuration -------------------
# Full treatment so the admin group gets sudo-less access AND the service user keeps
# access even on admin-created files. data/ is EXCLUDED: those dirs are owned by the
# containers' subuids and need engine-specific modes (Postgres rejects a group/other-
# accessible PGDATA), so they are handled separately just below.
apply_acl_tree "${SERVICE_DIR}" "${SERVICE_DIR}/data" # treat everything EXCEPT data/
Configure Secure NGINX Static Assets Permissions
# Grant the NGINX worker user (www-data) read-only access to assets subtrees.
# $1 = service directory β traverse-only (x)
# $2..$n = assets subtrees served directly by NGINX (configs, icons, favicon, manifest, ...)
apply_acl_nginx() {
local tree="$1"; shift
local assets
sudo setfacl -m u:www-data:x "$tree" # traverse only: pass through, no listing, no reading
for assets in "$@"; do
# assets directories β enter + list, existing and future
sudo find "$assets" -type d -exec setfacl -m u:www-data:rx {} + # www-data: read existing dirs
sudo find "$assets" -type d -exec setfacl -d -m u:www-data:rx {} + # www-data: inherit read on new items
# assets files β www-data read-only, owner/admin rw, never executable.
# Entries AND mask are pinned explicitly: a bare `setfacl -m u:www-data:r`
# would recalculate the mask to the union of all entries, resurfacing the
# latent rwx inherited from the default ACL (files would show group rwx).
sudo find "$assets" -type f -exec setfacl \
-m u:www-data:r,u:${SERVICE_USER}:rw,g:${SERVICE_ADMIN}:rw,u::rw,g::rw,o::-,m::rw {} +
done
}
# --- SERVICE_DIR/nginx + SERVICE_DIR/icons: served directly by NGINX -----------
# Created AFTER the apply_acl_tree pass so they inherit the default ACL + setgid
# group (service user/admin keep full access); www-data is then layered on top
# as a read-only named entry.
sudo mkdir -p "${SERVICE_DIR}/nginx" "${SERVICE_DIR}/icons"
sudo chown "${SERVICE_USER}:${SERVICE_ADMIN}" "${SERVICE_DIR}/nginx" "${SERVICE_DIR}/icons"
apply_acl_nginx "${SERVICE_DIR}" "${SERVICE_DIR}/nginx" "${SERVICE_DIR}/icons"
π‘οΈ Rootless Podman Defaults¶
Configure rootless Podman per-user configuration directory
# configure rootless Podman defaults for this service user
# - store per-user Podman configuration under ~/.config/containers
sudo -u "${SERVICE_USER}" mkdir -p "${SERVICE_HOME}/.config/containers/systemd"
Configure rootless Podman defaults
# configure rootless Podman defaults for this service user
# - use k8s-file logging for easier log rotation and inspection
sudo -u "${SERVICE_USER}" tee "${SERVICE_HOME}/.config/containers/containers.conf" >/dev/null <<EOF
[containers]
log_driver = "k8s-file"
[engine]
healthcheck_events=false
EOF
π§ Service Configuration¶
Setup gokapi Parameters
Before deploying, you need to define a few environment variables that will be used throughout the setup process.
HOST_PORT: external port used by NGINX to route traffic to the service
###################################################################################
# NGINX Proxy Configuration
###################################################################################
HOST_PORT=10045
Create Environment files
sudo -u "${SERVICE_USER}" tee "${SERVICE_DIR}/gokapi.env" >/dev/null <<EOF
###################################################################################
# NGINX Proxy Configuration
###################################################################################
HOST_PORT=${HOST_PORT}
###################################################################################
# gokapi Configuration
###################################################################################
GOKAPI_VERSION=latest
TZ=Europe/Paris
EOF
Keep the .env files
All the secret informations will be stored in the .env files.
π§© Quadlet Service¶
Create Network Podman Quadlet
# podman quadlet: create network
sudo -u "${SERVICE_USER}" tee "${SERVICE_HOME}/.config/containers/systemd/gokapi.network" >/dev/null <<EOF
[Network]
NetworkName=gokapi
EOF
Create gokapi Server Podman Quadlet
# podman quadlet: create gokapi server
sudo -u "${SERVICE_USER}" tee "${SERVICE_HOME}/.config/containers/systemd/gokapi-server.container" >/dev/null <<'EOF'
[Unit]
Description=gokapi server
[Container]
EnvironmentFile=/media/ssd/podman/gokapi/gokapi.env
Image=docker.io/f0rc3/gokapi:${GOKAPI_VERSION}
ContainerName=gokapi-server
Network=gokapi.network
# gokapi is a plain Go image: no PUID/PGID, and no USER of its own (it runs as root by
# default). We pin User=1000:1000 so it runs unprivileged as 1000; the data dirs are
# pre-chowned to 1000's mapped subuid in the permissions step, so plain :rw mounts work.
User=1000:1000
# plain bind mount (data dir already owned by 1000's subuid via the permissions step)
Volume=/media/ssd/podman/gokapi/data/gokapi:/app/data:rw
Volume=/media/ssd/podman/gokapi/data/config:/app/config:rw
PublishPort=127.0.0.1:${HOST_PORT}:53842
HealthCmd=curl -fsS -o /dev/null http://127.0.0.1:53842/
HealthStartPeriod=180s
HealthInterval=60s
HealthTimeout=3s
HealthRetries=3
HealthOnFailure=kill
[Service]
EnvironmentFile=/media/ssd/podman/gokapi/gokapi.env
UMask=0007
SuccessExitStatus=143
Restart=always
RestartSec=30
TimeoutStartSec=600
[Install]
WantedBy=default.target
EOF
UMask=0007: removes all permissions forotherswhile preserving read/write access for the service owner and admin group on newly created files and directoriesSuccessExitStatus=143: treat a SIGTERM-style exit (143) as a clean stop, not a failure
βΆοΈ Start gokapi¶
Enable and Start Quadlet Services
# open an interactive shell as the service user
sudo -iu "${SERVICE_USER}"
# reload systemd user units
systemctl --user daemon-reload
# start Podman Quadlet services
systemctl --user start gokapi-server.service
# verify service status
systemctl --user status gokapi-server.service
To follow the service logs in real time, run
sudo journalctl _UID=$(id -u ${SERVICE_USER}) -ffrom the Debian account.
π Deploy gokapi¶
Install NGINX
NGINX needs to be installed, follow the NGINX section.
Configure NGINX
NGINX needs to be configured using a file in /etc/nginx/sites-enabled directory.
This configuration file specify the documentation path:
server {
server_name gokapi.domain.fr;
# setup 404 error_page
error_page 404 /404.html;
include snippets/error-404.conf;
# show maintenance page when backend is down
error_page 502 503 504 /maintenance.html;
include snippets/error-maintenance.conf;
# reverse proxy
location / {
proxy_pass http://127.0.0.1:10045;
# keep it HTTP/1.1
proxy_http_version 1.1;
# SSE support (required for real-time notifications, e.g. /uploadStatus)
# gokapi uses server-sent events, not websockets
proxy_set_header Connection "";
# forwarded headers
include proxy_params;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# application-specific tuning
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
client_body_timeout 3600s;
client_max_body_size 50G;
proxy_buffering off;
proxy_redirect off;
}
}
# restart nginx
sudo nginx -t && sudo service nginx restart
Replace
gokapi.domain.frby the name of your website.
Activate HTTPS
To activate HTTPS protocol, follow theΒ Let's Encrypt section.
βοΈ Configure gokapi¶
Set up the gokapi admin account immediately
A fresh Gokapi installation exposes an unsecured admin setup interface. To protect your uploads and credentials, you must complete the admin account setup as soon as the service is started.
Open the setup page at: https://gokapi.domain.fr/setup
Recommended settings:
- Use SSL: No
- Public facing URL:
https://gokapi.domain.fr/ - Redirection URL for the index:
https://gokapi.domain.fr/login
