DEV Community

selfhosting.sh
selfhosting.sh

Posted on • Originally published at selfhosting.sh

Self-Hosting wger Workout Manager with Docker

What Is wger?

wger (pronounced "veger") is an open-source fitness tracking application that covers workouts, nutrition logging, and body measurements. It comes with a library of 800+ exercises (synced from wger.de), meal planning with nutritional data, and a REST API for building custom frontends or integrations. Self-hosting means your workout data, body measurements, and dietary information stay private.

Prerequisites

  • A Linux server (Ubuntu 22.04+ recommended)
  • Docker and Docker Compose installed (guide)
  • 2 GB of RAM minimum (PostgreSQL + Redis + Celery workers)
  • 10 GB of free disk space (grows with exercise images/videos)
  • A domain name (optional, for remote access)

Docker Compose Configuration

wger requires several services: the application server, PostgreSQL, Redis, Nginx, and Celery workers for background tasks. This looks complex but the official stack works reliably out of the box.

Create a docker-compose.yml file:

services:
  web:
    image: wger/server:2.4
    container_name: wger
    restart: unless-stopped
    environment:
      # Security — CHANGE THESE
      SECRET_KEY: "generate-a-random-50-char-string-here"   # CHANGE THIS
      SIGNING_KEY: "generate-a-different-random-string"      # CHANGE THIS
      SITE_URL: "http://localhost"                            # CHANGE to your domain

      # Database
      DJANGO_DB_ENGINE: django.db.backends.postgresql
      DJANGO_DB_DATABASE: wger
      DJANGO_DB_USER: wger
      DJANGO_DB_PASSWORD: change-this-db-password            # CHANGE THIS
      DJANGO_DB_HOST: db
      DJANGO_DB_PORT: "5432"
      DJANGO_PERFORM_MIGRATIONS: "True"

      # Cache
      DJANGO_CACHE_BACKEND: django_redis.cache.RedisCache
      DJANGO_CACHE_LOCATION: redis://cache:6379/1
      DJANGO_CACHE_TIMEOUT: "1296000"

      # Celery
      USE_CELERY: "True"
      CELERY_BROKER: redis://cache:6379/2
      CELERY_BACKEND: redis://cache:6379/2

      # App settings
      WGER_INSTANCE: https://wger.de
      ALLOW_REGISTRATION: "True"
      ALLOW_GUEST_USERS: "True"
      TIME_ZONE: UTC
      WGER_USE_GUNICORN: "True"
    volumes:
      - wger-static:/home/wger/static
      - wger-media:/home/wger/media
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-q", "--no-check-certificate", "--spider", "http://localhost:8000"]
      interval: 30s
      timeout: 10s
      retries: 5
    networks:
      - wger

  nginx:
    image: nginx:stable
    container_name: wger-nginx
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - wger-static:/wger/static:ro
      - wger-media:/wger/media:ro
    depends_on:
      - web
    networks:
      - wger

  db:
    image: postgres:15-alpine
    container_name: wger-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: wger
      POSTGRES_USER: wger
      POSTGRES_PASSWORD: change-this-db-password             # Must match DJANGO_DB_PASSWORD
    volumes:
      - wger-postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U wger"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - wger

  cache:
    image: redis:7-alpine
    container_name: wger-cache
    restart: unless-stopped
    volumes:
      - wger-redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - wger

  celery_worker:
    image: wger/server:2.4
    container_name: wger-celery-worker
    restart: unless-stopped
    command: /start-worker
    environment:
      DJANGO_DB_ENGINE: django.db.backends.postgresql
      DJANGO_DB_DATABASE: wger
      DJANGO_DB_USER: wger
      DJANGO_DB_PASSWORD: change-this-db-password
      DJANGO_DB_HOST: db
      DJANGO_DB_PORT: "5432"
      DJANGO_CACHE_BACKEND: django_redis.cache.RedisCache
      DJANGO_CACHE_LOCATION: redis://cache:6379/1
      USE_CELERY: "True"
      CELERY_BROKER: redis://cache:6379/2
      CELERY_BACKEND: redis://cache:6379/2
      CELERY_WORKER_CONCURRENCY: "2"
      WGER_INSTANCE: https://wger.de
    volumes:
      - wger-media:/home/wger/media
    depends_on:
      web:
        condition: service_healthy
    networks:
      - wger

  celery_beat:
    image: wger/server:2.4
    container_name: wger-celery-beat
    restart: unless-stopped
    command: /start-beat
    environment:
      DJANGO_DB_ENGINE: django.db.backends.postgresql
      DJANGO_DB_DATABASE: wger
      DJANGO_DB_USER: wger
      DJANGO_DB_PASSWORD: change-this-db-password
      DJANGO_DB_HOST: db
      DJANGO_DB_PORT: "5432"
      USE_CELERY: "True"
      CELERY_BROKER: redis://cache:6379/2
      CELERY_BACKEND: redis://cache:6379/2
    volumes:
      - wger-beat:/home/wger/beat
    depends_on:
      celery_worker:
        condition: service_healthy
    networks:
      - wger

volumes:
  wger-static:
  wger-media:
  wger-postgres:
  wger-redis:
  wger-beat:

networks:
  wger:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Create the Nginx configuration file nginx.conf:

upstream wger {
    server web:8000;
}

server {
    listen 80;
    server_name _;

    location /static/ {
        alias /wger/static/;
    }

    location /media/ {
        alias /wger/media/;
    }

    location / {
        proxy_pass http://wger;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
    }
}
Enter fullscreen mode Exit fullscreen mode

Start the stack:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

First startup takes 1-2 minutes as Django runs migrations and Celery begins syncing exercises from wger.de.

Initial Setup

  1. Access wger at http://your-server-ip
  2. Register an account (the first account has admin privileges)
  3. Go to Settings to configure your profile, units (metric/imperial), and language

Important: After creating your admin account, consider setting ALLOW_REGISTRATION=False and ALLOW_GUEST_USERS=False if this is a personal instance.

The Celery workers automatically sync the exercise database from wger.de in the background. This includes 800+ exercises with descriptions, muscles targeted, and (if enabled) images and videos. The initial sync can take 10-30 minutes.

Configuration

Setting Variable Default Notes
Registration ALLOW_REGISTRATION True Set False for private instances
Guest access ALLOW_GUEST_USERS True Set False to require accounts
Exercise sync SYNC_EXERCISES_CELERY True Weekly sync from wger.de
Image sync SYNC_EXERCISE_IMAGES_CELERY True Can use several GB of disk
Video sync SYNC_EXERCISE_VIDEOS_CELERY True Uses significant disk space
Timezone TIME_ZONE Europe/Berlin Set to your timezone
Brute force protection AXES_ENABLED True 10 failed attempts = lockout

Reverse Proxy

If placing wger behind an external reverse proxy (Nginx Proxy Manager, Caddy, Traefik), point it to port 80 on the wger Nginx container. Add these environment variables to the web service:

X_FORWARDED_PROTO_HEADER_SET: "True"
CSRF_TRUSTED_ORIGINS: "https://fitness.example.com"
Enter fullscreen mode Exit fullscreen mode

For a dedicated reverse proxy setup, see Reverse Proxy Guide.

Backup

Back up the PostgreSQL database and media volume:

# Database
docker exec wger-db pg_dump -U wger wger > wger-backup.sql

# Media files (exercise images, user uploads)
docker run --rm -v wger-media:/data -v $(pwd):/backup alpine tar czf /backup/wger-media.tar.gz /data
Enter fullscreen mode Exit fullscreen mode

For general backup strategies, see Backup Strategy.

Troubleshooting

Exercise Database Is Empty

Symptom: No exercises appear in the exercise list after setup.

Fix: The Celery worker syncs exercises asynchronously. Check if it's running: docker logs wger-celery-worker. The initial sync takes 10-30 minutes. If the worker shows errors connecting to wger.de, check outbound internet access.

Static Files Not Loading (Broken CSS/JS)

Symptom: The site loads but looks unstyled or broken.

Fix: Nginx serves static files from the shared wger-static volume. Ensure the nginx.conf paths match the volume mounts. Run docker exec wger python manage.py collectstatic --noinput to regenerate static files.

"CSRF Verification Failed" Behind Reverse Proxy

Symptom: Form submissions fail with a CSRF error when accessed through a reverse proxy.

Fix: Set CSRF_TRUSTED_ORIGINS to your external domain (e.g., https://fitness.example.com) and X_FORWARDED_PROTO_HEADER_SET=True in the web service environment.

Resource Requirements

  • RAM: ~1 GB idle (all services combined), ~1.5 GB under active use
  • CPU: 2 cores recommended (Celery workers are the main consumer)
  • Disk: 2 GB base, 10+ GB if syncing exercise images and videos

The latest tag on Docker Hub points to 2.5-dev (development). Always pin to 2.4 for stable deployments.

Verdict

wger is the most comprehensive self-hosted fitness tracker available. The exercise database, workout planning, nutritional tracking, and body measurement logging cover everything a serious fitness enthusiast needs. The 6-service Docker stack is heavier than simpler alternatives, but the feature depth justifies it.

If you just want basic workout logging without the complexity, Fittrackee is lighter and focused on GPS-tracked activities. wger is the answer when you want a full gym-style workout planner with nutritional tracking.

Related

Top comments (0)