FreeBSD Docker Compose Deployments with Safe Rollbacks

Why this FreeBSD tutorial is different
Docker Compose on FreeBSD is a solid choice when you want repeatable deploys and a rollback plan you can trust. This guide takes you through a production-style release on a fresh FreeBSD server, from the first SSH login to a verified rollback.
It is written for Hostperl customers who need a stable application host, not a throwaway lab. It also pairs well with Hostperl VPS when you want a compact environment for application hosting.
FreeBSD is the right path here because this tutorial uses FreeBSD-native service management, pf firewalling, and pkg package handling. Docker runs on modern FreeBSD through official packages, but you still need to follow FreeBSD file paths, rc.d startup, and networking rules. If you want a broader deployment pattern for Linux hosts, see Docker deployments with Git hooks on Hostperl VPS and Docker Compose rollouts on AlmaLinux 9 without downtime.
What you will build
You will set up a non-root administrator, install Docker and Compose, create a small web app stack, expose it safely on port 8080, add a local health check, and configure boot persistence. Then you will test a controlled update and a rollback so you can recover quickly if a release fails.
- Supported platform: FreeBSD 14.1-RELEASE or newer
- Deployment style: Docker Compose with a reverse-proxy-friendly app port
- Example app directory:
/opt/myapp - Example application port:
8000 - Example public port exposed by the host:
8080
1) Connect to the FreeBSD server
On your local computer, open your first SSH session with the following command.
ssh root@203.0.113.10203.0.113.10 is a documentation example. Replace it with the real public IP address assigned to your FreeBSD server.
If your provider gave you a default non-root login such as freebsd, use the same example IP and connect with that account instead.
ssh freebsd@203.0.113.10Keep this root session open until the new administrator login is working.
2) Confirm the operating system
On the VPS as root, confirm that the host is FreeBSD before you change anything else.
freebsd-versionYou should see a FreeBSD release string such as 14.1-RELEASE-p*. If you are not on FreeBSD, stop here and use the correct platform guide for that server.
3) Update the base system and install Docker tools
FreeBSD uses pkg for packages and freebsd-update for base system patches. Run both before you deploy.
freebsd-update fetch installThis applies any pending security or maintenance updates to the base OS. If the command reports a pending reboot, finish this step before continuing.
pkg updateThis refreshes the package repository metadata.
pkg install -y sudo docker docker-compose-plugin git ca_root_nssThis installs sudo, Docker, Compose support, Git, and CA certificates. On FreeBSD, the package name for Compose may appear as docker-compose-plugin; that is the expected result for current package sets.
docker --version && docker compose version && git --versionYou should see version output for each tool. If Docker is not yet enabled, do not proceed until the service is registered and starts cleanly in the next steps.
4) Create a non-root administrator
Production deploys should not run from root. Create a deploy account, add it to the wheel group, and keep root only for emergency access.
pw useradd deploy -m -G wheel -s /bin/shThis creates the deploy account with a home directory and shell access.
Set a password temporarily if you need console login, or lock it if you will use SSH keys only:
passwd deployNow create the SSH directory and prepare it for your public key.
mkdir -p /home/deploy/.ssh && chmod 700 /home/deploy/.ssh && touch /home/deploy/.ssh/authorized_keys && chmod 600 /home/deploy/.ssh/authorized_keys && chown -R deploy:deploy /home/deploy/.sshPaste your public key into /home/deploy/.ssh/authorized_keys using ee or vi, then save and exit. If you need to append a key from the root shell, use this pattern and replace the key text with your own public key:
echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... your-key-comment' >> /home/deploy/.ssh/authorized_keysFinally, fix ownership again after editing.
chown -R deploy:deploy /home/deploy/.sshOpen a second terminal now, then test the new login before you make any lockout-prone changes.
ssh deploy@203.0.113.10203.0.113.10 remains the example IP. Replace it with your server’s real address. Once logged in, verify sudo works:
sudo -lYou should see that deploy can run privileged commands. Leave the root session open until this test succeeds.
5) Enable Docker at boot on FreeBSD
FreeBSD service startup is handled through rc.conf. Enable Docker before you start the daemon.
sysrc docker_enable="YES"This adds the service flag to /etc/rc.conf.
service docker startIf the service starts correctly, Docker will be available for the Compose stack in the next step.
service docker statusYou should see the service reported as running. If it fails, check the system log with tail -n 50 /var/log/messages and confirm the kernel module support is available in your FreeBSD release.
6) Prepare the application directory
On the VPS as the non-root sudo user, create the deployment directory and move into it.
sudo mkdir -p /opt/myapp && sudo chown -R deploy:deploy /opt/myapp && cd /opt/myapp && pwdYou should end up in /opt/myapp as the deploy user. This directory will hold the Compose file, application source, and environment settings.
7) Create a simple production Compose stack
This example uses a small Nginx container to serve a health-check page. It stays intentionally plain so the deployment pattern is easy to follow, and you can replace the app image later with Node.js, Python, or PHP.
Open the Compose file with your editor:
ee compose.yamlPaste this full file content, then save and exit.
services:
web:
image: nginx:1.27-alpine
container_name: myapp-web
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
restart: unless-stopped
Now create the web root and a health page.
mkdir -p htmlcat > html/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hostperl Docker Compose on FreeBSD</title>
</head>
<body>
<h1>It works</h1>
<p>Docker Compose is running on FreeBSD.</p>
</body>
</html>
EOFThis gives you a visible release check that does not depend on a second application runtime.
8) Start the stack and confirm it is listening
Bring the service up from the directory that contains compose.yaml.
docker compose up -dCompose should pull the Nginx image if needed, create the container, and publish host port 8080.
docker compose psYou want the service state to show running.
sockstat -4 -l | grep 8080This checks that something is listening on port 8080. If nothing appears, the container may not have started or another service may already use the port.
From the server itself, test the app:
fetch -o - http://127.0.0.1:8080The output should include the HTML from index.html.
From your local computer, test the same URL against the server IP:
curl -I http://203.0.113.10:8080Replace 203.0.113.10 with your server’s actual public IP. A successful response returns HTTP/1.1 200 OK or similar.
9) Open the FreeBSD firewall safely
Use a firewall policy that matches the platform. FreeBSD uses pf, so open the new port before you tighten other access rules.
On the VPS as root, edit /etc/pf.conf:
ee /etc/pf.confUse this minimal ruleset, then save and exit:
ext_if = "vtnet0"
set skip on lo
pass in on $ext_if proto tcp from any to any port 22 keep state
pass in on $ext_if proto tcp from any to any port 8080 keep state
block allIf your interface name is not vtnet0, replace it with the one from ifconfig. That is the only value you should adjust here.
sysrc pf_enable="YES"service pf restartTest the syntax first if you edit the rules again later with:
pfctl -nf /etc/pf.confIf the syntax check returns no output, the ruleset is valid.
10) Make the release durable across reboots
Docker is already enabled, but you also need to confirm the container restarts after a host reboot. The restart: unless-stopped policy in Compose handles that behavior.
Save the current container state, then restart the host or restart Docker to test persistence. A lighter check is to bounce the service:
service docker restartWait a few seconds, then confirm the container comes back:
docker compose psIf the container is back in running state after the restart, your startup policy is working.
11) Update the app safely
A good deployment has a repeatable update path. For this example, change the page content, restart only the affected service, and verify the change.
cat > html/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hostperl Docker Compose on FreeBSD</title>
</head>
<body>
<h1>Release 2</h1>
<p>This is the updated deployment.</p>
</body>
</html>
EOFdocker compose up -dThen confirm the update from the server:
fetch -o - http://127.0.0.1:8080You should now see Release 2.
12) Roll back cleanly if the release breaks
Rollback is part of the deployment, not an afterthought. If a change causes bad output, replace the file with the last known good version and reapply the stack.
cat > html/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hostperl Docker Compose on FreeBSD</title>
</head>
<body>
<h1>It works</h1>
<p>Docker Compose is running on FreeBSD.</p>
</body>
</html>
EOFdocker compose up -dThis returns the app to the previous known-good state. If the container itself is unhealthy, review logs before trying another release:
docker logs myapp-web --tail 50The log output should match the current image and show no repeated startup failures.
Verification from server and client
Use both local and remote checks before you call the deployment complete.
On the VPS as root or the sudo user:
service docker statusdocker compose pssockstat -4 -l | grep 8080fetch -o - http://127.0.0.1:8080On your local computer:
curl -I http://203.0.113.10:8080Successful verification means Docker is running, the container is healthy, port 8080 is reachable, and the page responds from outside the server.
Common failures and fast fixes
Docker does not start. Check the service and the system log:
service docker status && tail -n 100 /var/log/messagesIf the log mentions missing kernel support or a startup error, re-run freebsd-update, confirm your FreeBSD release is current, and start Docker again.
Port 8080 is already in use. Find the conflict:
sockstat -4 -l | grep 8080If another process owns the port, stop that process or change the published port in compose.yaml to a free value such as 8081:80, then run docker compose up -d again.
The site works locally but not from your laptop. Recheck pf and the public IP path:
pfctl -sr && ifconfigLook for the correct external interface and a rule that allows TCP 8080. If you edited the wrong interface name, fix /etc/pf.conf, run pfctl -nf /etc/pf.conf, then reload with service pf restart.
The container starts but content does not change. Confirm the mounted file exists and has the right content:
ls -l html/index.html && cat html/index.htmlIf the file is wrong, rewrite it and restart the Compose stack.
Where this fits in a Hostperl setup
This workflow gives you a clean FreeBSD deployment path for small services, internal tools, landing pages, and customer-facing app front ends. If you outgrow a single host, Hostperl dedicated server hosting gives you more room for storage, CPU pinning, and container density, while managed VPS hosting stays a sensible choice for lighter application stacks and migration testing.
If you want a FreeBSD-friendly environment for application hosting, Hostperl can help you plan the right VPS or dedicated server shape before you cut over. For teams that need a clean path from staging to production, our VPS hosting and dedicated server hosting options are good starting points.
Ask support to help you size storage, validate firewall rules, and confirm restart behavior before launch day.
FAQ
Can I use Docker Compose on FreeBSD for production?
Yes, for services that fit the FreeBSD package support model and your container needs. Keep the workload simple, test reboots, and document your rollback path.
Should I expose the app directly or use a reverse proxy?
Use a reverse proxy if you need TLS, host-based routing, or cleaner logging. For this tutorial, direct port publishing keeps the mechanics easy to verify.
What if I need HTTPS later?
Add a reverse proxy such as Nginx or Caddy in front of the app, then terminate TLS there. Keep the Compose service private on localhost or an internal bridge network.
How do I avoid lockouts when tightening access?
Keep the root SSH session open until the new deploy user, firewall rules, and service startup all pass verification. Only then should you remove or restrict old access paths.
