wger
Self hosted FLOSS fitness/workout, nutrition and weight tracker
Alternative to: myfitnesspal, strong, jefit, hevy, trainerize
wger is a free, open source workout and fitness manager for planning custom training routines with automatic weight progression, tracking nutrition and body weight, and browsing a community-maintained exercise and ingredient database. It also offers basic gym management features for multiple users and trainers. wger can be self-hosted with Docker, exposes a REST API, and has companion Android, iOS, and F-Droid apps.
wger Docker Compose example
Self-host wger on your own server, homelab, or VPS starting from this Docker Compose example.
It runs wger in Docker containers using the official docker.io/wger/server:latest, docker.io/nginx:stable, docker.io/postgres:15-alpine, docker.io/redis:latest, docker.io/wger/server:latest, docker.io/wger/server:latest images, with persistent volumes and automatic restarts preconfigured.
Review the environment variables and adjust them to your setup, save the file as compose.yml (or docker-compose.yml), and start the stack with docker compose up -d.
services:
# Django application server (gunicorn). Self-initializes on first boot:
# runs database migrations, collects static files and bootstraps the
# exercise/ingredient database plus a default admin user.
web:
image: docker.io/wger/server:latest
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
volumes:
- static:/home/wger/static
- media:/home/wger/media
expose:
- 8000
stop_grace_period: 60s
environment:
# Public URL of your wger instance. Used for CSRF checks, absolute links
# and emails. Set this to the domain you serve wger on.
SITE_URL: "http://localhost"
# Django secret key. Change this to a long, unique random string.
SECRET_KEY: "changeme"
# Application timezone (e.g. Europe/Berlin, UTC).
TIME_ZONE: "UTC"
# Container timezone.
TZ: "UTC"
# Allow new users to create an account via the web UI.
ALLOW_REGISTRATION: "True"
# Allow the use of temporary guest accounts.
ALLOW_GUEST_USERS: "False"
# Allow users to upload videos to exercises.
ALLOW_UPLOAD_VIDEOS: "True"
# Apply pending database migrations automatically on startup.
DJANGO_PERFORM_MIGRATIONS: "True"
# Collect static files automatically on startup.
DJANGO_COLLECTSTATIC_ON_STARTUP: "True"
# Disable Django debug mode (keep off in production).
DJANGO_DEBUG: "False"
# Serve the app with gunicorn (production WSGI server).
WGER_USE_GUNICORN: "True"
# Number of reverse proxies in front of the app (used for client IP detection).
NUMBER_OF_PROXIES: "1"
# PostgreSQL database credentials. The host defaults to the "db" service.
POSTGRES_USER: "wger"
POSTGRES_PASSWORD: "wger"
POSTGRES_DB: "wger"
# Enable Celery background workers (exercise/ingredient sync, etc.).
USE_CELERY: "True"
# Celery broker and result backend (Redis).
CELERY_BROKER: "redis://cache:6379/2"
CELERY_BACKEND: "redis://cache:6379/2"
# Django cache backend (Redis).
DJANGO_CACHE_BACKEND: "django_redis.cache.RedisCache"
DJANGO_CACHE_LOCATION: "redis://cache:6379/1"
DJANGO_CACHE_CLIENT_CLASS: "django_redis.client.DefaultClient"
# wger instance to sync the exercise/ingredient database from.
WGER_INSTANCE: "https://wger.de"
# Sync the exercise database in the background via Celery.
SYNC_EXERCISES_CELERY: "True"
SYNC_EXERCISE_IMAGES_CELERY: "True"
SYNC_INGREDIENTS_CELERY: "True"
# Sender address for outgoing emails.
FROM_EMAIL: "wger Workout Manager <wger@example.com>"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "-O", "/dev/null", "http://localhost:8000/api/v2/version/"]
interval: 10s
timeout: 5s
start_period: 300s
retries: 5
restart: unless-stopped
# Reverse proxy in front of the app. This service is required even if you run
# your own reverse proxy on top of it: gunicorn does not serve the static and
# media files, nginx does. It is the entry point (port 80) for the whole stack.
nginx:
image: docker.io/nginx:stable
depends_on:
- web
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- static:/wger/static:ro
- media:/wger/media:ro
ports:
- "80:80"
healthcheck:
test: ["CMD", "service", "nginx", "status"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# PostgreSQL database.
db:
image: docker.io/postgres:15-alpine
shm_size: 256mb
environment:
# PostgreSQL database credentials (must match the "web" service).
POSTGRES_USER: "wger"
POSTGRES_PASSWORD: "wger"
POSTGRES_DB: "wger"
# Container timezone.
TZ: "UTC"
volumes:
- postgres-data:/var/lib/postgresql/data/
expose:
- 5432
command:
- "postgres"
- "-c"
- "shared_buffers=256MB"
- "-c"
- "effective_cache_size=768MB"
- "-c"
- "work_mem=8MB"
- "-c"
- "random_page_cost=1.1"
- "-c"
- "max_connections=30"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\""]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# Redis, used as the Django cache and the Celery broker/result backend.
cache:
image: docker.io/redis:latest
command: ["redis-server", "--save", "3600 1 300 100 60 10000"]
volumes:
- redis-data:/data
expose:
- 6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# Celery worker: runs background jobs (exercise/ingredient database sync).
celery_worker:
image: docker.io/wger/server:latest
command: /start-worker
depends_on:
web:
condition: service_healthy
volumes:
- media:/home/wger/media
environment:
SITE_URL: "http://localhost"
SECRET_KEY: "changeme"
TIME_ZONE: "UTC"
TZ: "UTC"
DJANGO_DEBUG: "False"
POSTGRES_USER: "wger"
POSTGRES_PASSWORD: "wger"
POSTGRES_DB: "wger"
USE_CELERY: "True"
CELERY_BROKER: "redis://cache:6379/2"
CELERY_BACKEND: "redis://cache:6379/2"
DJANGO_CACHE_BACKEND: "django_redis.cache.RedisCache"
DJANGO_CACHE_LOCATION: "redis://cache:6379/1"
DJANGO_CACHE_CLIENT_CLASS: "django_redis.client.DefaultClient"
WGER_INSTANCE: "https://wger.de"
SYNC_EXERCISES_CELERY: "True"
SYNC_EXERCISE_IMAGES_CELERY: "True"
SYNC_INGREDIENTS_CELERY: "True"
FROM_EMAIL: "wger Workout Manager <wger@example.com>"
healthcheck:
test: ["CMD", "celery", "-A", "wger", "inspect", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# Celery beat: schedules the periodic background jobs.
celery_beat:
image: docker.io/wger/server:latest
command: /start-beat
depends_on:
celery_worker:
condition: service_healthy
volumes:
- celery-beat:/home/wger/beat/
environment:
SITE_URL: "http://localhost"
SECRET_KEY: "changeme"
TIME_ZONE: "UTC"
TZ: "UTC"
DJANGO_DEBUG: "False"
POSTGRES_USER: "wger"
POSTGRES_PASSWORD: "wger"
POSTGRES_DB: "wger"
USE_CELERY: "True"
CELERY_BROKER: "redis://cache:6379/2"
CELERY_BACKEND: "redis://cache:6379/2"
DJANGO_CACHE_BACKEND: "django_redis.cache.RedisCache"
DJANGO_CACHE_LOCATION: "redis://cache:6379/1"
DJANGO_CACHE_CLIENT_CLASS: "django_redis.client.DefaultClient"
WGER_INSTANCE: "https://wger.de"
SYNC_EXERCISES_CELERY: "True"
SYNC_EXERCISE_IMAGES_CELERY: "True"
SYNC_INGREDIENTS_CELERY: "True"
FROM_EMAIL: "wger Workout Manager <wger@example.com>"
restart: unless-stopped
volumes:
postgres-data:
redis-data:
celery-beat:
media:
static:
networks:
default:
name: wger_network
Values set to changeme are required — replace them with your own
values before starting wger.
Extra files
The Docker Compose configuration above bind-mounts the following file. Save it next to your compose.yml,
keeping the same relative path.
nginx.conf
upstream wger {
server web:8000;
}
# JSON access log format compatible with the Loki/Alloy parser
# (same field names as the Caddy parser, so the same dashboard works).
log_format json_access escape=json
'{'
'"ts":"$time_iso8601",'
'"client_ip":"$remote_addr",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"request_proto":"$server_protocol",'
'"request_host":"$host",'
'"status":$status,'
'"size":$body_bytes_sent,'
'"duration":$request_time,'
'"user_agent":"$http_user_agent",'
'"referer":"$http_referer"'
'}';
# Universal X-Forwarded-Proto handling for all deployment scenarios
# - Uses upstream value when behind reverse proxy (Traefik, Caddy, etc.)
# - Falls back to nginx's scheme for direct connections and port forwarding
map $http_x_forwarded_proto $final_forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
server {
listen 80;
access_log /dev/stdout json_access;
location / {
proxy_pass http://wger;
# Standard proxy headers
proxy_set_header Host $http_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 $final_forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_http_version 1.1;
# Timeouts for WebSocket connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location /static/ {
alias /wger/static/;
# Files in the static folder are preprocessed by collectstatic and have a hash
# in their names, so we can cache longer
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Vary "Accept-Encoding" always;
}
location /media/ {
alias /wger/media/;
}
# Increase max body size to allow for video uploads
client_max_body_size 100M;
} Prefer a managed setup? WinterFlow installs, configures, and updates wger for you using this same Docker Compose configuration.