IPv4 & IPv6 Leasing - Any RIR, Any LocationOrder Now
Hostperl

Apache Reverse Proxy for Python Apps on FreeBSD

By Raman Kumar

Share:

Updated on Sep 26, 2026

Apache Reverse Proxy for Python Apps on FreeBSD

Why this Apache reverse proxy for Python apps setup fits production on FreeBSD

This tutorial shows you how to place Apache in front of a Python app on FreeBSD and send traffic to a local app server safely. The result is a production-ready Apache reverse proxy for Python apps that can run on a Hostperl VPS or a small dedicated server without exposing the app process to the internet. If you are planning a launch, a migration, or a clean split between web serving and application logic, this pattern keeps the public edge simple and the Python service isolated.

Apache fits this role well on FreeBSD. Service control is straightforward, pf can block the backend port, and you can keep the Python runtime bound to localhost. For teams comparing deployment options on Hostperl VPS hosting, this is a practical choice when you want stable HTTP handling, easy log review, and a path to TLS later without changing app code.

We will build a small Python WSGI app behind Apache, enable the required modules, add a restricted service account, and verify that Apache proxies requests correctly. The same pattern works well for Flask, FastAPI behind a WSGI bridge, Django, or a custom internal API that should not listen on a public port.

What you need before you start

  • A FreeBSD server with root access.
  • A working domain name, or a test host entry pointing to 203.0.113.10. Replace that example IP with your real server IP.
  • Apache installed from FreeBSD packages.
  • A Python application that can listen on 127.0.0.1:8000.

We will use the reserved documentation address 203.0.113.10 in commands. Replace it with your real public IP before you run anything on your server.

Connect and confirm FreeBSD

On your local computer

ssh root@203.0.113.10

203.0.113.10 is a documentation example and must be replaced with the real public IP assigned to your server. Keep the root session open while you build the non-root service account and test the proxy.

On the VPS as root

freebsd-version

This confirms the FreeBSD release. You should see a valid version string such as 14.2-RELEASE.

Create a non-root account for the app

Running the Python process as root is a bad idea. Create a dedicated user, then give it ownership of the application files.

On the VPS as root

pw useradd deploy -m -s /bin/sh -G wheel

This creates the deploy account, makes a home directory, uses /bin/sh, and adds the user to wheel so it can use su or doas if you enable it later.

passwd deploy

Set a temporary password if you need password login during setup. If you use SSH keys only, you can lock the password after the first key-based login.

mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chown -R deploy:deploy /home/deploy/.ssh

Now copy your public key into /home/deploy/.ssh/authorized_keys. From your local computer, append the public key with:

ssh-copy-id deploy@203.0.113.10

If you do not use ssh-copy-id, paste the key manually on the server and then fix ownership and permissions:

cat > /home/deploy/.ssh/authorized_keys <<'EOF'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIReplaceWithYourRealPublicKey deploy@example.com
EOF
chown deploy:deploy /home/deploy/.ssh/authorized_keys
chmod 600 /home/deploy/.ssh/authorized_keys

Open a second terminal now and test the new login before you change anything else.

On your local computer

ssh deploy@203.0.113.10

If that works, confirm the user can become root when needed.

On the VPS as the non-root sudo user

su -
pw usershow deploy

Keep the original root session open until this is verified. If you plan to use a locked password and SSH keys only, do that after the Apache proxy is working.

Install Apache and Python support on FreeBSD

FreeBSD uses pkg for package management. Install Apache, Python, and the WSGI bridge libraries that fit this deployment pattern.

On the VPS as root

pkg update
pkg install -y apache24 py311-python py311-virtualenv

This installs Apache 2.4 and a current Python 3.11 toolchain. The exact Python package may vary by FreeBSD release, but the 3.11 line is a safe current choice in 2026.

Check the versions so you know what is active.

httpd -v
python3.11 --version

Apache should report its 2.4 build, and Python should report a 3.11.x version string.

Enable Apache modules and basic service start

Apache on FreeBSD reads its configuration from /usr/local/etc/apache24/httpd.conf. Before editing it, enable the service in rc.conf.

On the VPS as root

sysrc apache24_enable=YES

That makes Apache start after reboot. Now edit the main configuration file.

ee /usr/local/etc/apache24/httpd.conf

Add or confirm these lines in the file. Keep the rest of the default file unless your server image has unusual settings.

ServerName server.example.com:80
Listen 80

LoadModule proxy_module libexec/apache24/mod_proxy.so
LoadModule proxy_http_module libexec/apache24/mod_proxy_http.so
LoadModule headers_module libexec/apache24/mod_headers.so
LoadModule rewrite_module libexec/apache24/mod_rewrite.so

IncludeOptional etc/apache24/extra/httpd-vhosts.conf

Save and exit ee with Esc, then a for save, then q to quit. Replace server.example.com with your real hostname if you already have DNS pointed at the server.

Before starting Apache, test the configuration.

On the VPS as root

apachectl configtest

You want Syntax OK. If the file contains a typo, Apache will tell you the line number before startup.

service apache24 start

Then check that it is running.

service apache24 status

Successful output should show Apache as active.

Build a small Python app behind the proxy

We will create a minimal WSGI app that listens only on localhost. That keeps the application unreachable from the public network, which is the whole point of placing Apache in front of it.

On the VPS as the non-root sudo user

sudo -iu deploy
mkdir -p /home/deploy/myapp
cd /home/deploy/myapp
python3.11 -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install gunicorn

The app directory is /home/deploy/myapp in this example. If you prefer /opt/myapp, keep the same structure and permissions, but use that path consistently.

Create the app file.

cat > /home/deploy/myapp/app.py <<'EOF'
def app(environ, start_response):
    body = b"Hello from Python behind Apache on FreeBSD\n"
    headers = [("Content-Type", "text/plain; charset=utf-8"), ("Content-Length", str(len(body)))]
    start_response("200 OK", headers)
    return [body]
EOF

Now create a tiny Gunicorn entry point. Gunicorn will serve the WSGI callable on localhost only.

cat > /home/deploy/myapp/wsgi.py <<'EOF'
from app import app
EOF

Test the app manually on port 8000.

cd /home/deploy/myapp
. .venv/bin/activate
gunicorn --bind 127.0.0.1:8000 wsgi:app

Leave that running for a moment in one terminal. From another terminal on the server, test it locally:

fetch -o - http://127.0.0.1:8000/

You should see the hello message. Stop Gunicorn with Ctrl+C after the test.

Create a persistent FreeBSD service for the app

Use rc.d so the app survives a reboot. FreeBSD service scripts are different from Linux systemd units, and this is one place where the platform matters.

On the VPS as root

cat > /usr/local/etc/rc.d/myapp <<'EOF'
#!/bin/sh
# PROVIDE: myapp
# REQUIRE: LOGIN
# KEYWORD: shutdown

. /etc/rc.subr

name="myapp"
rcvar="myapp_enable"
pidfile="/var/run/${name}.pid"
command="/usr/local/bin/gunicorn"
command_args="--daemon --pid ${pidfile} --bind 127.0.0.1:8000 wsgi:app"
command_interpreter="/usr/local/bin/python3.11"
start_precmd="myapp_precmd"

myapp_precmd()
{
    cd /home/deploy/myapp || return 1
    su -m deploy -c "/usr/local/bin/gunicorn --daemon --pid ${pidfile} --bind 127.0.0.1:8000 wsgi:app"
}

load_rc_config $name
: ${myapp_enable:=no}
run_rc_command "$1"
EOF
chmod 555 /usr/local/etc/rc.d/myapp

This script is intentionally simple. In a larger deployment you might use daemon, supervisord, or a dedicated process manager, but for a single app this is enough to show the full path from boot to proxy.

Enable and start the service.

sysrc myapp_enable=YES
service myapp start
service myapp status

If the service does not stay up, inspect its process and logs.

ps aux | grep gunicorn
sockstat -4 -6 | grep 8000

You should see Gunicorn bound to 127.0.0.1:8000.

Configure Apache as the reverse proxy

Now create a dedicated virtual host. This is the part that exposes your application to the public web while keeping the backend private.

On the VPS as root

ee /usr/local/etc/apache24/extra/httpd-vhosts.conf

Use this complete virtual host example.

<VirtualHost *:80>
    ServerName server.example.com
    ServerAlias www.server.example.com

    ProxyPreserveHost On
    ProxyRequests Off
    RequestHeader set X-Forwarded-Proto "http"

    ProxyPass / http://127.0.0.1:8000/
    ProxyPassReverse / http://127.0.0.1:8000/

    ErrorLog "/var/log/apache24/myapp-error.log"
    CustomLog "/var/log/apache24/myapp-access.log" combined
</VirtualHost>

Replace server.example.com with your real hostname. If your DNS is not ready yet, you can still test with a local hosts entry from your laptop.

Check the config again before reloading Apache.

apachectl configtest
service apache24 reload

Use configtest before the reload so a typo does not take the site offline.

Restrict the backend with pf

The Python app should not be reachable on the network, only from Apache on the same host. FreeBSD’s packet filter can enforce that.

On the VPS as root

ee /etc/pf.conf

Use a minimal ruleset like this if pf is not already in use by another policy.

set skip on lo0

block in all
pass out all keep state
pass in on vtnet0 proto tcp from any to any port 80 keep state
pass in on vtnet0 proto tcp from any to any port 22 keep state

If your interface is not vtnet0, replace it with the real public NIC name from ifconfig. Then validate and enable pf carefully.

pfctl -nf /etc/pf.conf
sysrc pf_enable=YES
service pf start
pfctl -sr

This keeps your local backend port closed to the outside while leaving SSH and HTTP available.

Verify the proxy path from server and client

First confirm the backend still answers only locally.

On the VPS as root

fetch -o - http://127.0.0.1:8000/

You should see the hello response from the Python app.

Next, confirm Apache can reach it through the public address.

On your local computer

curl -I http://203.0.113.10/

Replace 203.0.113.10 with your real server IP. You should receive a 200 OK or a normal Apache response if DNS is still settling.

Then test the full body.

curl http://203.0.113.10/

The response should be the Python hello text served through Apache.

On the server, check the logs if anything looks wrong.

tail -n 50 /var/log/apache24/myapp-access.log
tail -n 50 /var/log/apache24/myapp-error.log

That will usually tell you whether the proxy path failed, the app stopped, or Apache returned a permission or module error.

Make the service survive a reboot

Before you call the setup complete, test persistence. Reboot the server, then confirm Apache and the app come back on their own.

On the VPS as root

service apache24 status
service myapp status
reboot

After the reboot, reconnect from your local computer and re-run the status checks.

On your local computer

ssh root@203.0.113.10

On the VPS as root

service apache24 status
service myapp status
sockstat -4 -6 | grep -E '(:80|:8000)'

You want Apache listening on port 80 and Gunicorn listening only on 127.0.0.1:8000.

Safe rollback if you need to back out

If you need to stop the deployment quickly, reverse the changes in this order so you do not lock yourself out or leave a broken service enabled.

On the VPS as root

service apache24 stop
service myapp stop
sysrc -x apache24_enable
sysrc -x myapp_enable

If you added pf rules that block access too aggressively, restore the last known good pf configuration before restarting networking.

service pf stop
cp /etc/pf.conf /root/pf.conf.bad.$(date +%F-%H%M%S)

Then put back your earlier pf.conf and validate it before re-enabling pf.

Troubleshooting the most likely failures

Apache will not start

Run:

apachectl configtest
service apache24 start

If configtest fails, fix the line number it reports. A missing quote in the virtual host file is the most common cause.

The app works on 127.0.0.1 but not through Apache

Run:

tail -n 50 /var/log/apache24/myapp-error.log
sockstat -4 -6 | grep 8000

If Apache shows connection refused, the app is not running or is bound to the wrong address. Start the Gunicorn service again and confirm it listens on 127.0.0.1:8000.

Nothing is reachable from outside

Run:

pfctl -sr
ifconfig

If pf is blocking port 80 or the interface name is wrong, fix /etc/pf.conf, validate it with pfctl -nf /etc/pf.conf, and reload pf.

Permission errors in the app directory

Run:

ls -ld /home/deploy/myapp
ls -l /home/deploy/myapp

Files should belong to deploy:deploy. Correct them with chown -R deploy:deploy /home/deploy/myapp.

Next step: add TLS after the proxy is stable

Once the plain HTTP path is working, add Let's Encrypt or another certificate method on top of Apache. That keeps the change set smaller and troubleshooting easier. If you later move this app to a larger Hostperl environment, the same proxy pattern scales cleanly on a dedicated server or a managed VPS without changing the application code.

If you want a straightforward, supportable path for Python apps, Hostperl VPS hosting gives you enough control to run Apache, FreeBSD services, and isolated backends without wasting time on edge cases. For higher traffic, Hostperl dedicated server hosting adds more headroom for TLS, logging, and future app growth.

Start with the right base platform, then keep the app private and the public edge predictable. That approach saves time during launches and migrations.

FAQ

Can I use this Apache reverse proxy pattern for Django or Flask?

Yes. The public-facing Apache layer stays the same. You only replace the Python app entry point with your real WSGI callable.

Why keep the Python app on 127.0.0.1 instead of a public port?

It reduces exposure. Apache handles the public connection, while the backend stays unreachable from the network.

Do I need pf if I already bind the app to localhost?

Binding to localhost is good, but pf gives you a second control layer. It is useful if a process changes its bind address later.

What should I check first if the proxy returns 502?

Check the Gunicorn process, then inspect Apache’s error log. A 502 usually means Apache cannot reach the backend.

Can I add TLS on the same FreeBSD server later?

Yes. Once the proxy is working, you can add certificates, redirect HTTP to HTTPS, and keep the backend unchanged.