Deploy on bare metal¶
This guide runs Eskoz directly on a Linux host: PostgreSQL for the database,
Gunicorn to serve the app, systemd to keep it running, and a reverse proxy
(Nginx in the example, Caddy as an alternative) in front.
It mirrors exactly what the Docker entrypoint does — wait for the DB, migrate, collect static, compile translations, then start Gunicorn — just done by hand.
Assumptions
A Debian/Ubuntu host with sudo, Python 3.13, and a domain pointing at the
server. Adjust package commands for other distributions.
1. System dependencies¶
gettext is required for manage.py compilemessages.
2. Create a service user¶
Run the app under a dedicated, unprivileged user:
3. PostgreSQL¶
Create the database and role (match these to your .env):
sudo -u postgres psql <<'SQL'
CREATE DATABASE eskoz;
CREATE USER eskoz WITH PASSWORD 'change-me';
GRANT ALL PRIVILEGES ON DATABASE eskoz TO eskoz;
ALTER DATABASE eskoz OWNER TO eskoz;
SQL
4. Get the code and install dependencies¶
sudo -u eskoz -i
git clone https://github.com/DonAsako/eskoz.git /opt/eskoz/app
cd /opt/eskoz/app
python3.13 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements/production.txt
5. Configure .env¶
Edit .env for a bare-metal install — note DB_HOST=localhost:
DEBUG=0
DJANGO_SECRET_KEY=<generate one, see below>
DJANGO_ALLOWED_HOSTS=example.com
ADMIN_URL=admin
THEME=Eskoz
LANGUAGE_CODE=fr
POSTGRES_DB=eskoz
POSTGRES_USER=eskoz
POSTGRES_PASSWORD=change-me
DB_HOST=localhost
DB_PORT=5432
Generate the secret key:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
See Configuration for every variable.
6. Initialize the application¶
With the virtualenv active and DJANGO_SETTINGS_MODULE pointing at production:
export DJANGO_SETTINGS_MODULE=eskoz.settings.production
python manage.py migrate --noinput
python manage.py collectstatic --noinput
python manage.py compilemessages
python manage.py createsuperuser
Exit the eskoz shell when done: exit.
7. Run Gunicorn under systemd¶
Create /etc/systemd/system/eskoz.service:
[Unit]
Description=Eskoz (Gunicorn)
After=network.target postgresql.service
Requires=postgresql.service
[Service]
User=eskoz
Group=eskoz
WorkingDirectory=/opt/eskoz/app
Environment=DJANGO_SETTINGS_MODULE=eskoz.settings.production
ExecStart=/opt/eskoz/app/.venv/bin/gunicorn eskoz.wsgi:application \
--bind 127.0.0.1:8000 \
--workers 4 \
--threads 2 \
--timeout 60 \
--access-logfile - \
--error-logfile -
Restart=on-failure
[Install]
WantedBy=multi-user.target
Enable and start it:
Note
Other variables (DJANGO_SECRET_KEY, database credentials, …) are read from
.env automatically because the working directory is the project root. Only
DJANGO_SETTINGS_MODULE needs to be set in the unit file.
8. Reverse proxy¶
Gunicorn listens on 127.0.0.1:8000. Put a reverse proxy in front to terminate
TLS and serve static/media files.
Two options are described below — pick one:
- Nginx + Certbot — the most common setup; Certbot manages the certificates.
- Caddy — fewer moving parts; obtains and renews TLS certificates automatically out of the box.
Create /etc/nginx/sites-available/eskoz:
server {
listen 80;
server_name example.com;
location /static/ {
alias /opt/eskoz/app/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /opt/eskoz/app/media/;
expires 1d;
}
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
Enable it and obtain a certificate:
sudo ln -s /etc/nginx/sites-available/eskoz /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
Certbot rewrites the server block for HTTPS and sets up auto-renewal.
Caddy obtains and renews TLS certificates automatically — no Certbot needed.
Install it (official repository on Debian/Ubuntu):
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy
Replacing Nginx
If you installed Nginx in step 1, stop it first so Caddy can bind ports
80/443: sudo systemctl disable --now nginx.
Replace /etc/caddy/Caddyfile with:
{
email you@example.com
}
example.com {
encode zstd gzip
handle_path /static/* {
root * /opt/eskoz/app/staticfiles
file_server
}
handle_path /media/* {
root * /opt/eskoz/app/media
file_server
}
handle {
reverse_proxy 127.0.0.1:8000 {
header_up X-Forwarded-Proto {scheme}
}
}
}
Reload Caddy:
Same as the Docker setup
This mirrors the CaddyFile shipped for Docker Compose — the bundled
stack already uses Caddy as its reverse proxy.
SECURE_SSL_REDIRECT
Production settings enable SECURE_SSL_REDIRECT and HSTS, and trust the
X-Forwarded-Proto header. Make sure your proxy sets that header (both
examples above do), otherwise you'll hit a redirect loop.
Upgrading¶
sudo -u eskoz -i
cd /opt/eskoz/app
git pull
source .venv/bin/activate
pip install -r requirements/production.txt
export DJANGO_SETTINGS_MODULE=eskoz.settings.production
python manage.py migrate --noinput
python manage.py collectstatic --noinput
python manage.py compilemessages
exit
sudo systemctl restart eskoz