Engineering teams building products for global markets face a hidden bottleneck: translations. When product copy, error messages, and documentation are scattered across spreadsheets, emailed to agencies, and pasted back by hand, consistency breaks and releases slip. Weblate is an open-source web-based continuous localization platform that brings translators, reviewers, and developers into the same workflow. It integrates directly with Git repositories, tracks translation progress per language, and raises pull requests back to your codebase so localized strings ship with every release.
In this guide, we will deploy Weblate on a single Ubuntu host with Docker Compose, publish it through nginx with automatic HTTPS, connect it to PostgreSQL for data durability, add Redis for caching and locking, and configure the operational controls that make it safe for production: secret management, database backups, health checks, and a clear acceptance checklist.
Architecture and flow overview
The deployment uses four containers on a dedicated bridge network: Weblate, PostgreSQL, Redis, and nginx. Weblate serves the Django application and Celery workers on an internal port. PostgreSQL persists translations, projects, user accounts, and version control metadata. Redis handles caching, session storage, and Celery task queues. nginx terminates TLS, handles certificate renewal, and proxies requests to Weblate. Persistent data lives in named Docker volumes for the database and Redis, and in a bind mount for Weblate data and static files. A nightly cron job dumps the PostgreSQL database to an encrypted archive on the host.
When a translator opens Weblate in a browser, the request flows through nginx on port 443, is decrypted, and forwarded to the Weblate container on the internal Docker network. Weblate queries PostgreSQL for the requested project or translation, renders the editor, and returns the HTML. When a developer pushes new source strings to a connected Git repository, Weblate detects the change, updates the translation status, and optionally opens a pull request with completed translations. nginx manages ACME challenges and automatically renews the TLS certificate before expiry.
- nginx handles public HTTPS on port 443 and auto-redirects HTTP to HTTPS.
- Weblate runs the Django application server with the built-in web UI, REST API, and Celery background workers.
- PostgreSQL stores projects, components, translations, user accounts, and version control metadata.
- Redis provides caching, session storage, and Celery task broker functionality.
- Docker volumes persist the database files, Redis data, and Weblate application data across restarts.
- Host cron runs
pg_dumpnightly and rotates encrypted backups locally or to S3.
Prerequisites
- Ubuntu 22.04 or 24.04 LTS with SSH access and sudo privileges.
- A DNS A record pointing
weblate.yourdomain.comto the server public IP. - Docker Engine 24.x+ and Docker Compose plugin installed.
- Ports 80 and 443 open to nginx for ACME challenges and HTTPS traffic.
- At least 2 GB RAM and 20 GB disk for repositories, translation files, and backups.
- An SMTP provider account or local relay for password-reset and notification emails.
Step-by-step deployment
1) Install Docker, Compose, nginx, and firewall basics
Update the package index, install Docker and the Compose plugin, and enable the firewall with only the ports nginx needs:
sudo apt update && sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
sudo apt install -y nginx
sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
2) Create directories and environment file
Create a dedicated directory for Weblate, generate strong passwords, and prepare the environment file:
sudo mkdir -p /opt/weblate/{data,backups}
sudo chown -R $USER:$USER /opt/weblate
cd /opt/weblate
DB_PASS=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)
ADMIN_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
REDIS_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
cat > .env <
3) Define Compose services
The Compose file defines four services on a shared bridge network. Weblate loads environment variables from the external .env file and mounts the local data directory for persistent files. PostgreSQL uses a named volume for durability, and Redis uses a named volume with authentication:
cat > compose.yml <<'COMPOSE'
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: weblate
POSTGRES_USER: weblate
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U weblate -d weblate"]
interval: 10s
timeout: 5s
retries: 5
networks:
- weblatenet
cache:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass "${REDIS_PASSWORD}"
volumes:
- redis_data:/data
networks:
- weblatenet
weblate:
image: weblate/weblate:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
env_file:
- .env
environment:
WEBLATE_SITE_DOMAIN: weblate.yourdomain.com
volumes:
- ./data:/app/data
- weblate_cache:/app/cache
networks:
- weblatenet
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./certbot-data:/etc/letsencrypt
- ./certbot-www:/var/www/certbot
networks:
- weblatenet
depends_on:
- weblate
volumes:
db_data:
redis_data:
weblate_cache:
networks:
weblatenet:
driver: bridge
COMPOSE
4) Configure nginx reverse proxy
nginx handles TLS termination and proxies requests to the Weblate container. Create the nginx configuration with the reverse proxy block, security headers, and Certbot webroot support:
cat > nginx.conf <<'NGINX'
server {
listen 80;
server_name weblate.yourdomain.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name weblate.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/weblate.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/weblate.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
location / {
proxy_pass http://weblate:8080;
proxy_set_header Host $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 $scheme;
proxy_redirect off;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
NGINX
5) Obtain TLS certificate with Certbot
Run Certbot in standalone mode to obtain the initial certificate before starting the full stack:
sudo apt install -y certbot
sudo certbot certonly --standalone -d weblate.yourdomain.com --agree-tos -m [email protected] --non-interactive
sudo mkdir -p /opt/weblate/certbot-data/live/weblate.yourdomain.com
sudo cp /etc/letsencrypt/live/weblate.yourdomain.com/fullchain.pem /opt/weblate/certbot-data/live/weblate.yourdomain.com/
sudo cp /etc/letsencrypt/live/weblate.yourdomain.com/privkey.pem /opt/weblate/certbot-data/live/weblate.yourdomain.com/
sudo chown -R $USER:$USER /opt/weblate/certbot-data
6) Start services and verify health
Pull the images, start the stack, and confirm that PostgreSQL and Weblate reach a healthy state:
docker compose pull
docker compose up -d
sleep 30
docker compose ps
docker compose logs --tail 50 weblate
7) First-time admin setup
Open https://weblate.yourdomain.com in a browser. Log in with the admin credentials from your .env file. After login, create your first project, connect a Git repository, and define a component for translation. Verify that source strings are imported and that the translation editor loads without errors.
8) Backup script
Create a nightly backup script that dumps the PostgreSQL database and archives the Weblate data directory. Store the script in /opt/weblate/backups/backup.sh:
cat > /opt/weblate/backups/backup.sh <<'BKP'
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/opt/weblate/backups"
DATE=$(date +%Y%m%d_%H%M%S)
source /opt/weblate/.env
# Database dump
docker compose -f /opt/weblate/compose.yml exec -T db pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" | gzip > "$BACKUP_DIR/weblate_db_$DATE.sql.gz"
# Data directory archive
tar czf "$BACKUP_DIR/weblate_data_$DATE.tar.gz" -C /opt/weblate data
# Rotate to keep last 14 days
find "$BACKUP_DIR" -name 'weblate_*' -mtime +14 -delete
BKP
chmod +x /opt/weblate/backups/backup.sh
# Schedule nightly at 02:00
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/weblate/backups/backup.sh >> /opt/weblate/backups/backup.log 2>&1") | crontab -
9) Acceptance checklist execution
Run through the checklist to confirm the deployment is production-ready:
- DNS resolves
weblate.yourdomain.comto the server public IP. curl -I https://weblate.yourdomain.comreturns HTTP 200 with HSTS headers.- Login succeeds with the admin credentials defined in
.env. - Create a test project, connect a Git repository, and import source strings successfully.
- Verify that translation suggestions and comments are saved without errors.
- A backup archive exists in
/opt/weblate/backups/after the first scheduled run.
Configuration and secrets handling
Never commit the .env file to version control. Store it in a secrets manager or encrypted backup. The admin password grants full access to all projects and translations; rotate it quarterly and require MFA for administrative accounts. For database credentials, create a dedicated PostgreSQL user with limited privileges rather than using the superuser account.
Backup archives should be encrypted at rest if they are copied to object storage. Consider restricting Weblate admin access to a VPN or IP allowlist. Translation data may contain proprietary product strings; review your data retention policy and scrub sensitive content before sharing exports with external teams. If you connect Weblate to private Git repositories, use deploy keys or dedicated service accounts rather than personal access tokens.
Verification
- Run
curl -I https://weblate.yourdomain.comand confirm HTTP 200 withstrict-transport-securityheader. - Log in with the admin credentials and verify the dashboard loads.
- Create a project, connect a Git repository, and confirm source strings import correctly.
- Inspect the translation editor and confirm edits are saved and reflected in the database.
- Check
/opt/weblate/backups/for a recent.sql.gzarchive. - Verify that nginx has a valid certificate with
docker compose logs nginx.
Common issues and fixes
Weblate shows a database connection error on first start: Ensure PostgreSQL has finished initializing before Weblate starts. The depends_on condition service_healthy handles this, but if you see repeated connection failures, run docker compose restart weblate after confirming docker compose logs db shows database system is ready.
Git repository push fails with authentication error: Verify that the deploy key or personal access token configured in the Weblate project settings has write access to the target repository. For GitHub, ensure the token has repo scope and that two-factor authentication is not blocking basic auth.
nginx returns 502 Bad Gateway: Verify that the nginx.conf proxy target matches the Compose service name weblate on port 8080. Check docker compose logs weblate to confirm the application is listening.
Out-of-memory errors during large repository imports: Weblate imports and parses translation files in memory. Increase the container memory limit or host RAM, and consider splitting large monolithic translation files into smaller components.
Certificate renewal fails behind a restrictive firewall: Certbot needs outbound HTTPS to the Let's Encrypt ACME servers. Whitelist acme-v02.api.letsencrypt.org and the relevant OCSP responders.
Email notifications fail to send: Confirm that WEBLATE_EMAIL_HOST, WEBLATE_EMAIL_HOST_USER, and WEBLATE_EMAIL_HOST_PASSWORD are correct in the .env file. Test SMTP connectivity with openssl s_client -connect smtp.yourdomain.com:587 -starttls smtp.
FAQ
Can I use MySQL or MariaDB instead of PostgreSQL?
Weblate officially supports PostgreSQL and MySQL. To use MariaDB, change the database service image, update the environment variables, and adjust the healthcheck command. PostgreSQL is recommended for large translation projects and long-term stability.
How do I add a new translation language to a project?
In the Weblate project settings, open the component and click Add new translation. Select the target language from the dropdown or enter a custom language code. Weblate will create the translation file in the repository and track progress automatically.
Does Weblate support SAML and SSO authentication?
Yes. Weblate supports OAuth2, OpenID Connect, SAML, LDAP, and Azure AD authentication out of the box. Configure the social authentication providers in the .env file or through the Django admin panel. Enterprise plans include advanced group synchronization and SCIM provisioning.
How do I configure automatic pull requests to GitHub?
In the component settings, enable the Version control integration and set the push URL to your GitHub repository. Add a deploy key or personal access token with repo scope. Enable Push on commit to automatically open pull requests when translations reach 100 percent completion.
What is the difference between Weblate Community and Enterprise?
Weblate Community is free, open-source, and includes unlimited projects, components, and languages. Weblate Enterprise adds advanced user management, priority support, hosted backups, and SLA guarantees. This guide uses the Community edition.
Can I run Weblate without Redis?
Weblate can use the Django database cache as a fallback, but Redis is strongly recommended for production. Redis handles Celery task brokering, session storage, and rate limiting. Without Redis, large imports and background tasks will run synchronously and may timeout.
How do I update Weblate?
Pull the latest image, recreate the container, and run database migrations automatically on startup. Because the data directory and database are persistent, the update is non-destructive. Always take a backup before major upgrades.
docker compose pull weblate
docker compose up -d weblate
Internal links
- Production Guide: Deploy ToolJet with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy n8n with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy Vaultwarden with Docker Compose + Caddy on Ubuntu
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.
Header image: Original SysBrix generated header, no watermark.