Deploy Django on FreeBSD With Gunicorn, Nginx, and rc.d
Deploy Django on FreeBSD 14: install Python, PostgreSQL, and Nginx from packages, run Gunicorn as an rc.d service with daemon(8), configure pf firewall rules, get certificates with acme.sh or certbot, and optionally isolate the app in a jail.
Problem statement
Most Django deployment guides assume Linux with systemd. FreeBSD has no systemd: services are rc.d scripts, packages come from pkg, the firewall is pf, and isolation is done with jails. Django, Gunicorn, PostgreSQL, and Nginx all run well on FreeBSD, but the glue (service supervision, users, paths under /usr/local) is different enough that Linux instructions do not translate directly.
pkg install python312 py312-sqlite3 postgresql17-server nginx git.- Create a
www-style app user, a virtualenv, and install requirements plus Gunicorn. - Write an rc.d script for Gunicorn using
daemon(8)with-rfor automatic restarts, and enable it in/etc/rc.conf. - Configure Nginx in
/usr/local/etc/nginx/nginx.confto proxy to Gunicorn's socket. - Enable
pfwith rules for SSH, HTTP, and HTTPS, and issue certificates withsecurity/py-certbotoracme.sh.
Step-by-step solution
1. Install packages
pkg install -y python312 py312-sqlite3 postgresql17-server postgresql17-client nginx git
sysrc postgresql_enable=YES nginx_enable=YES
service postgresql initdb
service postgresql start
Python's standard library modules such as sqlite3 are separate packages on FreeBSD; install the ones your project needs. PostgreSQL's data directory is under /var/db/postgres.
2. Create the app environment
pw useradd shop -m -d /usr/local/www/shop -s /usr/sbin/nologin
su -m shop -c "git clone https://github.com/acme/shop.git /usr/local/www/shop/src"
su -m shop -c "python3.12 -m venv /usr/local/www/shop/venv"
su -m shop -c "/usr/local/www/shop/venv/bin/pip install -r /usr/local/www/shop/src/requirements.txt gunicorn"
Packages with C extensions, such as psycopg, may build from source on FreeBSD because PyPI wheels target Linux and macOS. Install the PostgreSQL client package first so pg_config exists, or use psycopg[c].
3. Write an rc.d script for Gunicorn
#!/bin/sh
# /usr/local/etc/rc.d/shop
# PROVIDE: shop
# REQUIRE: LOGIN postgresql
# KEYWORD: shutdown
. /etc/rc.subr
name="shop"
rcvar="shop_enable"
load_rc_config $name
: ${shop_enable:="NO"}
: ${shop_user:="shop"}
: ${shop_dir:="/usr/local/www/shop/src"}
: ${shop_env_file:="/usr/local/etc/shop.env"}
pidfile="/var/run/${name}/daemon.pid"
command="/usr/sbin/daemon"
command_args="-r -P ${pidfile} -u ${shop_user} -o /var/log/${name}.log \
/usr/local/www/shop/venv/bin/gunicorn --chdir ${shop_dir} \
--bind unix:/var/run/${name}/gunicorn.sock --workers 3 config.wsgi"
start_precmd="${name}_prestart"
shop_prestart() {
install -d -o ${shop_user} -g www -m 750 /var/run/${name}
}
run_rc_command "$1"
daemon -r restarts Gunicorn if it dies, which covers what Restart=always does under systemd. Load environment variables by setting shop_env_file and sourcing it in the prestart function, or let Django read them with django-environ. Then:
chmod +x /usr/local/etc/rc.d/shop
sysrc shop_enable=YES
service shop start
4. Configure Nginx
Edit /usr/local/etc/nginx/nginx.conf:
upstream shop { server unix:/var/run/shop/gunicorn.sock fail_timeout=0; }
server {
listen 80;
server_name shop.example.com;
location /static/ { alias /usr/local/www/shop/static/; }
location / {
proxy_pass http://shop;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Nginx runs as www, which matches the socket directory group in the rc.d script.
5. Firewall and certificates
cat > /etc/pf.conf <<'PF'
ext_if = "vtnet0"
set skip on lo0
block in all
pass out all keep state
pass in on $ext_if proto tcp to port { 22 80 443 } keep state
PF
sysrc pf_enable=YES && service pf start
pkg install -y py311-certbot py311-certbot-nginx
certbot --nginx -d shop.example.com
Check the certbot package names with pkg search certbot, since they track the ports tree's default Python. Add renewal to /etc/periodic.conf or cron.
Explanation
FreeBSD's base system and third-party software are separated: anything installed with pkg lives under /usr/local, including its configuration in /usr/local/etc and rc.d scripts in /usr/local/etc/rc.d. Services are enabled with variables in /etc/rc.conf. There is no built-in process supervisor like systemd, so daemon(8) wraps Gunicorn to restart it and manage its PID. Everything above the operating system, including Django's settings and Gunicorn's options, is identical to a Linux deployment.
Edge cases / notes
- Wheels. Expect some packages to compile from source; install compilers and headers (
pkg install rustfor cryptography if no binary package exists, or usepy312-cryptographyfrom pkg). - Log rotation. Add the log file to
/etc/newsyslog.conf.d/so it rotates. - ZFS. Snapshots of the app and database datasets before deploys make rollback straightforward.
Next steps
- Compare with Linux in Deploy Django With Gunicorn and Nginx on Ubuntu.
- Tune the proxy with Configure Nginx for Django in Production.
- Harden the host with the Django Server Hardening Checklist.
FAQ
Does Django run on FreeBSD?
Yes. Django is pure Python and runs on any platform with a supported Python version.
What replaces systemd on FreeBSD?
rc.d scripts for starting and stopping services, and daemon(8) for supervising and restarting long-running processes like Gunicorn.
Can I use Docker on FreeBSD?
Not natively. Use jails for isolation, or run Linux in a bhyve VM if you need container images.
Are Python packages installed with pip or pkg?
Use pip in a virtualenv for the app's dependencies. Use pkg for Python itself and system services.