Installation from source
Install npm / nodejs >= 22, and sass
If you plan to use video files, it’s recommended you install ffmpeg as well (this is not strictly necessary, it’s just used for better upload validation as well as the extraction of some information from the video files such as resolution, codec, etc):
apt-get install ffmpeg
pip install ffmpeg-python
wger user
It is recommended to add a dedicated user for the application:
sudo adduser wger --disabled-password --gecos ""
The following steps assume you did, but it is not necessary (nor is it necessary to call it ‘wger’). In that case, change the paths as needed.
Database
PostgreSQL
Install the Postgres server, then create a database and a user:
sudo apt-get install postgresql
sudo su - postgres
createdb wger
psql wger -c "CREATE USER wger WITH PASSWORD 'wger'";
psql wger -c "GRANT ALL PRIVILEGES ON DATABASE wger to wger";
The postgresql meta-package pulls in whatever currently-supported version
your distro packages, any Postgres ≥ 15 should work.
You might need to edit your pg_hba.conf to allow local socket connections.
SQLite
If using sqlite, create a folder for it (must be writable by the wger user):
mkdir /home/wger/db
touch /home/wger/db/database.sqlite
chown :www-data -R /home/wger/db
chmod g+w /home/wger/db /home/wger/db/database.sqlite
Application
Install uv if you don’t have it already, then
clone the repository and install wger with the docker dependency group.
This pulls in gunicorn alongside the base dependencies (for the systemd unit
we’ll create in the Webserver section below):
git clone https://github.com/wger-project/wger.git /home/wger/src
cd /home/wger/src
uv sync --group docker --no-managed-python
The --no-managed-python flag tells uv to use your system’s Python
(installed via apt or similar) instead of downloading its own. Drop the flag
if you’d rather have uv manage it.
uv creates the virtualenv at /home/wger/src/.venv automatically. Activate
it for the bootstrap commands below:
source /home/wger/src/.venv/bin/activate
Create folders for static resources and uploaded files. The static folder
contains generated CSS and JS files (read by Caddy), the media folder
receives uploads (read by Caddy, written by gunicorn/django):
mkdir /home/wger/{static,media}
chmod o+w /home/wger/media
Configuration
wger reads its configuration from environment variables. Place them in
/home/wger/wger.env so the systemd unit we’ll define in the Webserver
section can pick them up (via its EnvironmentFile= directive), and so
you can source them into a shell when running one-off commands.
For a complete commented list of available settings, use the Docker
setup’s prod.env as a reference:
https://github.com/wger-project/docker/blob/master/config/prod.env
A minimal example:
# /home/wger/wger.env
# Change these
ALLOWED_HOSTS=example.com,www.example.com
DJANGO_SECRET_KEY=your-very-long-and-random-secret-key
TIME_ZONE=Europe/Berlin
# Application
MEDIA_ROOT=/home/wger/media
STATIC_ROOT=/home/wger/static
# Django Setup
DJANGO_SETTINGS_MODULE=settings.main
PYTHONPATH=/home/wger/src
# Postgres
DJANGO_DB_ENGINE=django.db.backends.postgresql
DJANGO_DB_NAME=wger
DJANGO_DB_USER=wger
DJANGO_DB_PASSWORD=wger
DJANGO_DB_HOST=localhost
DJANGO_DB_PORT=5432
For SQLite, swap the database block for:
DJANGO_DB_ENGINE=django.db.backends.sqlite3
DJANGO_DB_NAME=/home/wger/db/database.sqlite
The file contains secrets, so restrict permissions:
sudo chown wger:wger /home/wger/wger.env
sudo chmod 600 /home/wger/wger.env
Note
The file uses systemd’s EnvironmentFile syntax: KEY=VALUE per
line, no export prefix and no spaces around =. Quotes around
values are usually unnecessary; if you do need them, use single quotes.
Bootstrap
For one-off commands (bootstrap, migrate, collectstatic, …) the env file needs to be sourced into the current shell. As the wger user with the virtualenv active:
set -a; . /home/wger/wger.env; set +a
Run the installation script, this downloads JS/CSS libraries and loads initial data:
wger bootstrap
Collect all static resources:
python manage.py collectstatic
Compile the translation (.po) files:
cd wger
django-admin compilemessages
The bootstrap command also creates a default administrator user. Change the password immediately after first login:
username: admin
password: adminadmin
Webserver
The recommended setup runs gunicorn as the WSGI application server, with Caddy as a reverse proxy in front of it that also serves the static and media files. This mirrors how the Docker image is configured internally.
If you prefer nginx, Apache or another webserver, the building blocks are the
same, just set up a reverse proxy that forwards to gunicorn and serves
/static/ and /media/ directly. See Django’s deployment guide:
https://docs.djangoproject.com/en/dev/howto/deployment/
gunicorn
Create a systemd service file at /etc/systemd/system/wger.service:
[Unit]
Description=wger gunicorn daemon
After=network.target
[Service]
User=wger
Group=wger
WorkingDirectory=/home/wger/src
EnvironmentFile=/home/wger/wger.env
ExecStart=/home/wger/src/.venv/bin/gunicorn wger.wsgi:application \
--preload \
--bind 127.0.0.1:8000 \
--workers 3 \
--threads 2 \
--worker-class gthread \
--timeout 240 \
--access-logfile -
[Install]
WantedBy=multi-user.target
The worker count of (2 × $num_cores) + 1 is a common starting point. The
unit reads your env vars from /home/wger/wger.env via EnvironmentFile.
Reload systemd and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now wger
Check that it’s running with systemctl status wger and
journalctl -u wger -f.
Caddy
Install Caddy following the official instructions: https://caddyserver.com/docs/install
Create /etc/caddy/Caddyfile:
your-domain.example.com {
encode
reverse_proxy 127.0.0.1:8000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {http.X-Forwarded-For} {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Http-Version {http.request.proto}
}
handle /static/* {
root * /home/wger
header Cache-Control "public, max-age=31536000, immutable"
file_server
}
handle /media/* {
root * /home/wger
file_server
}
}
Caddy automatically obtains and renews a Let’s Encrypt certificate as long as the domain’s DNS points to your server. Reload Caddy to pick up the config:
sudo systemctl reload caddy
The X-Forwarded-* headers are important, without them, generated URLs
(e.g. pagination links in the API) and CSRF protection can break. If you see
CSRF errors after setup, check Common errors and pitfalls.
Note
If /static/ or /media/ return 403 or 404, check whether the webserver’s
systemd unit has ProtectHome=true, as it blocks reads from /home/wger/.
Override the unit:
sudo systemctl edit caddy
and add:
[Service]
ProtectHome=off
Then reload: sudo systemctl daemon-reload && sudo systemctl restart caddy.
Next steps
Once your installation is running, see the Administration section for ongoing operations.