Skip to content

Maintenance

This page covers the recurring tasks of running a Cocotte instance: updating to a new version, backing up and restoring data, reading logs, re-running the seed commands, rotating the secret key and checking that the service is healthy.

Commands are given for the Docker (standalone) and Classic variants described in Production deployment. The Docker examples assume you are in the directory that contains docker-compose.prod.yml and .env.prod.

Tip

In the shared-proxy mode, add -f docker-compose.prod.proxy.yml after -f docker-compose.prod.yml in every Docker command on this page. A shell alias saves typing:

alias dc='docker compose -f docker-compose.prod.yml --env-file .env.prod'
# shared-proxy mode:
alias dc='docker compose -f docker-compose.prod.yml -f docker-compose.prod.proxy.yml --env-file .env.prod'

Update to a new version

Back up the database first (see Back up and restore), then read the Changelog for anything that needs manual action.

  1. Fetch the new code:

    git pull
    
  2. Rebuild the images and restart the containers:

    docker compose -f docker-compose.prod.yml --env-file .env.prod up --build -d
    

    When the new backend container starts, its entrypoint runs migrate and collectstatic before launching Gunicorn. The new SPA build is baked into the new frontend image. The database, uploaded files, static files and TLS certificates are kept in named volumes.

  3. Check the logs and the health endpoint (see Check health).

  4. Optionally, reclaim the disk space used by old images:

    docker image prune
    
  1. Fetch the new code:

    cd /opt/cocotte/app
    sudo -u cocotte git pull
    
  2. Update the backend dependencies, apply migrations and collect static files:

    cd /opt/cocotte/app/backend
    sudo -u cocotte uv sync --frozen --no-group dev --extra prod
    sudo -u cocotte env DJANGO_SETTINGS_MODULE=config.settings.prod .venv/bin/python manage.py migrate --noinput
    sudo -u cocotte env DJANGO_SETTINGS_MODULE=config.settings.prod .venv/bin/python manage.py collectstatic --noinput
    
  3. Rebuild the frontend:

    cd /opt/cocotte/app/frontend
    sudo -u cocotte npm ci
    sudo -u cocotte npm run build
    
  4. Restart the backend:

    sudo systemctl restart cocotte
    

    The web server serves the new frontend/dist immediately; it only needs a reload if you changed its configuration.

  5. Check the logs and the health endpoint (see Check health).

Note

Cocotte is a PWA: users who keep the app open, or installed it, get the new frontend once the service worker has fetched the update, usually on their next visit or reload.

Back up and restore

A complete backup has two parts:

What Docker Classic
Database db service, volume postgres_data Your PostgreSQL database cocotte
Uploaded files (recipe and thematic page images) Volume media_data backend/media/
Configuration .env.prod backend/.env, systemd unit, web server configuration

Static files (static_data volume, backend/staticfiles/) are regenerated by collectstatic and the TLS certificates (caddy_data volume) are re-issued automatically, so they don't need a backup.

Docker Compose prefixes volume names with the project name, which defaults to the name of the directory containing docker-compose.prod.yml. For a checkout in /opt/cocotte, the media volume is cocotte_media_data. List them to be sure:

docker volume ls --filter name=media_data

Back up the database

docker compose -f docker-compose.prod.yml --env-file .env.prod exec -T db \
    pg_dump -U postgres -Fc cocotte > cocotte-$(date +%F).dump

-T disables the pseudo-terminal so that the binary dump is written cleanly to the file on the host.

sudo -u postgres pg_dump -Fc cocotte > cocotte-$(date +%F).dump

Back up uploaded files

docker run --rm -v cocotte_media_data:/data:ro -v "$PWD":/backup alpine \
    tar czf /backup/cocotte-media-$(date +%F).tar.gz -C /data .

Replace cocotte_media_data with the name shown by docker volume ls.

sudo tar czf cocotte-media-$(date +%F).tar.gz -C /opt/cocotte/app/backend/media .

Copy both files, and your configuration file, off the server. Automate this with a cron job or systemd timer, and test a restore from time to time.

Restore

  1. Stop the backend so that nothing writes to the database during the restore:

    docker compose -f docker-compose.prod.yml --env-file .env.prod stop backend
    
    sudo systemctl stop cocotte
    
  2. Restore the database. --clean --if-exists drops the existing objects before recreating them:

    docker compose -f docker-compose.prod.yml --env-file .env.prod exec -T db \
        pg_restore -U postgres -d cocotte --clean --if-exists < cocotte-2026-01-31.dump
    
    sudo -u postgres pg_restore -d cocotte --clean --if-exists < cocotte-2026-01-31.dump
    
  3. Restore the uploaded files:

    docker run --rm -v cocotte_media_data:/data -v "$PWD":/backup alpine \
        sh -c "cd /data && tar xzf /backup/cocotte-media-2026-01-31.tar.gz"
    
    sudo tar xzf cocotte-media-2026-01-31.tar.gz -C /opt/cocotte/app/backend/media
    sudo chown -R cocotte:cocotte /opt/cocotte/app/backend/media
    
  4. Start the backend again. In Docker, migrations run on start; in Classic, run migrate if you restored a dump taken from an older version.

    docker compose -f docker-compose.prod.yml --env-file .env.prod up -d
    
    sudo systemctl start cocotte
    

Tip

To move an instance to a new server, install Cocotte there, restore the database dump and the media archive as above, copy your configuration file, and update DNS.

Note

Users can also export their own data from My account (see Your account), and a staff member can export every recipe as a portable archive (see Moderation). These archives are useful for sharing recipes between instances, but they are not a substitute for a database backup: accounts, plannings and settings are not included.

Personal data and retention

Accounts that have not logged in for a long time are warned, then deleted, by the purge_inactive_users command. Backups contain personal data too: keep them for a bounded time. See GDPR compliance.

Read the logs

Gunicorn writes its access log and error log to standard output, so they end up in Docker's logs or in the systemd journal.

# All services, following new lines
docker compose -f docker-compose.prod.yml --env-file .env.prod logs -f

# One service, last 200 lines
docker compose -f docker-compose.prod.yml --env-file .env.prod logs --tail 200 backend

Services are backend (Django, Gunicorn, migrations, emails printed by the console backend), frontend (Caddy: TLS certificate issuance and renewal, proxy errors) and db (PostgreSQL).

# Backend (Gunicorn and Django)
sudo journalctl -u cocotte -f

# Since a given time
sudo journalctl -u cocotte --since "1 hour ago"

# Web server
sudo journalctl -u caddy -f          # Caddy
sudo tail -f /var/log/nginx/error.log  # nginx

Re-run the seed commands

The seed commands can be run again at any time, for example after an update that extends the ingredient library. They never create duplicates, but each one overwrites the records it manages with the seeded values:

Command What a re-run does
seed_nutrient_requirements Resets every daily minimum to its seeded value, undoing edits made in the Django admin.
seed_common_ingredients For each seeded ingredient (matched by name, case-insensitively), overwrites category, default unit, season, translations, nutrition, carbon footprint and allergens, and marks allergens as reviewed. Ingredients that are not part of the seed are left untouched.
seed_cookware Creates the seeded cookware items that are missing (matched by name, case-insensitively), sets their English name, and fills in the emoji and photo only when they are empty (only missing photos are downloaded from Wikimedia Commons). It never deletes or renames anything.
seed_allergens Resets the name of each of the 15 reference allergens. Allergens you added yourself are left untouched.
seed_thematic_pages For the three seeded pages (matched by title), resets icon, description, filters and display order. It never replaces an image you uploaded, and does not change the Visible on the homepage setting.

Warning

If you have corrected a seeded ingredient or threshold, re-running the corresponding seed command reverts your correction.

docker compose -f docker-compose.prod.yml --env-file .env.prod exec backend python manage.py seed_common_ingredients
cd /opt/cocotte/app/backend
sudo -u cocotte env DJANGO_SETTINGS_MODULE=config.settings.prod .venv/bin/python manage.py seed_common_ingredients

Rotate the secret key

Change DJANGO_SECRET_KEY if it may have leaked. Generate a new value (see Generate a secret key), put it in .env.prod or backend/.env, and restart the backend.

Rotating the key invalidates everything signed with the old one:

  • every user is logged out of the app (login tokens are signed with the key) and must log in again;
  • Django admin sessions end;
  • password-reset links already sent stop working.

Calendar subscription links are random tokens stored in the database and keep working. Cocotte does not expose Django's SECRET_KEY_FALLBACKS setting, so there is no transition period.

Check health

The backend exposes an unauthenticated health endpoint that answers {"status": "ok"} as long as Django is running. It does not query the database.

curl https://cocotte.example.org/api/health/

To query Gunicorn directly on the server, bypassing the web server, send the headers the proxy would send; otherwise Django rejects the request (unknown host) or redirects it to HTTPS:

curl -H "Host: cocotte.example.org" -H "X-Forwarded-Proto: https" http://127.0.0.1:8000/api/health/

(Classic only: in Docker, port 8000 is not published on the host.)

Other quick checks:

docker compose -f docker-compose.prod.yml --env-file .env.prod ps
docker compose -f docker-compose.prod.yml --env-file .env.prod exec db pg_isready -U postgres

Note

ps may list backend as unhealthy: its built-in healthcheck calls the health endpoint on localhost over plain HTTP, which the production settings reject. The public URL check above is the reliable one.

systemctl status cocotte postgresql
sudo -u postgres pg_isready

To check the database connection and pending migrations from Django:

docker compose -f docker-compose.prod.yml --env-file .env.prod exec backend python manage.py showmigrations --plan | grep -v "\[X\]"
cd /opt/cocotte/app/backend
sudo -u cocotte env DJANGO_SETTINGS_MODULE=config.settings.prod .venv/bin/python manage.py showmigrations --plan | grep -v "\[X\]"

An empty output means the database is reachable and every migration is applied.