wger logo

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.