Running your own apps on a VPS puts you on the open internet, where bots and bad IPs try to break in all day. A regular firewall just opens or closes ports; it can't tell if traffic on port 443 is a real visitor or an attack. That's where protect self-hosted apps with CrowdSec comes in. It sits between your reverse proxy and the outside world, reads behavior patterns, and automatically bans offenders in real time.
This guide teaches you to deploy CrowdSec with Docker Compose, connect it to both Traefik and Caddy using the right bouncer for each.
What Is CrowdSec and Why Do You Need It?
CrowdSec is a free, open-source security tool that watches your logs and blocks attacks. Unlike a regular firewall, which only follows fixed rules, CrowdSec learns attack patterns and shares them with a global community. So if one server spots a bad IP, every other CrowdSec user gets protected from it.
It works in two parts:
- Security Engine (Agent): Reads logs, detects threats, and makes decisions.
- Remediation Component (Bouncer): Enforces those decisions at the entry point, in this case, inside Traefik or Caddy.
If you run a Linux VPS Server with apps exposed to the internet, pairing Traefik or Caddy with CrowdSec is one of the best security setups you can use.
Prerequisites to Protect Self-Hosted Apps with CrowdSec
Before starting to protect self-hosted apps with CrowdSec, make sure you have:
- A Linux VPS running Ubuntu 24.04.
- Docker and Docker Compose are installed.
- Either Traefik or Caddy is already planned as your reverse proxy.
- Root or Sudo access to the server.
If you need to get Traefik running, you can check this guide on Setting up Traefik with Docker Compose.
If you need Caddy as a reverse proxy, check this guide on Deploying a Reverse Proxy with Caddy.
Folder layout of CrowdSec Setup
First, you must create the project folder. To do this, run the commands below:
mkdir -p ~/crowdseccd ~/crowdsecmkdir -p crowdsec-config/acquis.dmkdir -p crowdsec-config/parsers/s02-enrichmkdir -p crowdsec-dbmkdir -p logs/traefikmkdir -p logs/caddymkdir -p dynamic
Create the shared Docker network if you do not already have one:
docker network create proxy
After this, your project should look like this:
~/crowdsec/├── crowdsec-config/│ ├── acquis.d/│ └── parsers/│ └── s02-enrich/├── crowdsec-db/├── dynamic/├── logs/│ ├── caddy/│ └── traefik/└── docker-compose.yml
Part 1. Deploy CrowdSec with Docker Compose
This is the core engine to which everything else connects. To create the base CrowdSec Docker Compose YAML file, use the command below:
nano ~/crowdsec/docker-compose.yml
1services:2 crowdsec:3 image: crowdsecurity/crowdsec:latest4 container_name: crowdsec5 restart: unless-stopped6 environment:7 COLLECTIONS: "crowdsecurity/linux crowdsecurity/http-cve crowdsecurity/http-dos"8 expose:9 - "8080"10 volumes:11 - ./crowdsec-config:/etc/crowdsec12 - ./crowdsec-db:/var/lib/crowdsec/data13 - /var/log/auth.log:/var/log/auth.log:ro14 networks:15 - proxy16 17networks:18 proxy:19 external: true
Save and exit.
Once you are done, start CrowdSec:
cd ~/crowdsecdocker compose up -d crowdsec
Check that it is running:
docker compose logs -f crowdsec
You can also check metrics later with:
docker exec crowdsec cscli metrics
At this point, protect self-hosted apps with CrowdSec is not complete yet because CrowdSec still needs logs from Traefik or Caddy.
Part 2. Protect Self-Hosted Apps with CrowdSec Using Traefik
This section covers integrating CrowdSec with Traefik using the official Traefik plugin. Use this if Traefik is your reverse proxy.
Important Note: Use the maxlerebourg/crowdsec-bouncer-traefik-plugin, not the older fbonalair/traefik-crowdsec-bouncer. The older one does not support the AppSec component and is no longer actively maintained.
Create Traefik YAML File
This file tells CrowdSec to read Traefik access logs. Create the file with the command below:
nano ~/crowdsec/crowdsec-config/acquis.d/traefik.yaml
Paste this content into the file:
1filenames:2 - /var/log/traefik/access.log3poll_without_inotify: true4labels:5 type: traefik
Now update the CrowdSec Compose file so it includes Traefik too.
nano ~/crowdsec/docker-compose.yml
You can replace it with this full Traefik stack:
1services:2 crowdsec:3 image: crowdsecurity/crowdsec:latest4 container_name: crowdsec5 restart: unless-stopped6 environment:7 COLLECTIONS: "crowdsecurity/linux crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/http-dos"8 expose:9 - "8080"10 volumes:11 - ./crowdsec-config:/etc/crowdsec12 - ./crowdsec-db:/var/lib/crowdsec/data13 - ./logs/traefik:/var/log/traefik:ro14 - /var/log/auth.log:/var/log/auth.log:ro15 networks:16 - proxy17 18 traefik:19 image: traefik:latest20 container_name: traefik21 restart: unless-stopped22 command:23 - --providers.docker=true24 - --providers.docker.exposedbydefault=false25 - --providers.file.directory=/dynamic26 - --providers.file.watch=true27 - --entrypoints.web.address=:8028 - --entrypoints.websecure.address=:44329 - --api.dashboard=true30 - --accesslog=true31 - --accesslog.filepath=/var/log/traefik/access.log32 - --experimental.plugins.crowdsec-bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin33 - --experimental.plugins.crowdsec-bouncer.version=v1.3.034 ports:35 - "80:80"36 - "443:443"37 volumes:38 - /var/run/docker.sock:/var/run/docker.sock:ro39 - ./dynamic:/dynamic40 - ./logs/traefik:/var/log/traefik41 networks:42 - proxy43 depends_on:44 - crowdsec45 46networks:47 proxy:48 external: true
Define the Middleware
You can define the CrowdSec middleware as Docker labels on Traefik itself, or as a dynamic config file. The label method is the most common for Docker-based setups.
First, create the bouncer key:
docker exec crowdsec cscli bouncers add traefik-bouncer
Copy the API key output.
Now create the Middleware file:
nano ~/crowdsec/dynamic/crowdsec-middleware.yml
Paste this and replace with your Bouncer Key:
1http:2 middlewares:3 crowdsec-bouncer:4 plugin:5 crowdsec-bouncer:6 enabled: true7 crowdsecLapiKey: PASTE_YOUR_KEY_HERE8 crowdsecLapiHost: crowdsec:80809 defaultDecisionSeconds: 60
If you put Cloudflare or another proxy in front of Traefik, add forwardedHeadersTrustedIPs to the middleware config, so CrowdSec sees the real visitor IP instead of the proxy's IP:
1http:2 middlewares:3 crowdsec-bouncer:4 plugin:5 crowdsec-bouncer:6 enabled: true7 crowdsecLapiKey: PASTE_YOUR_KEY_HERE8 crowdsecLapiHost: crowdsec:80809 defaultDecisionSeconds: 6010 forwardedHeadersTrustedIPs:11 - "173.245.48.0/20"
Apply the Middleware to Your App
For every service you want to protect, add the middleware to its router labels. For example, if your app is in ~/myapp/docker-compose.yml, open that file:
nano ~/myapp/docker-compose.yml
Add labels like this inside the app service:
1labels:2 - "traefik.enable=true"3 - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"4 - "traefik.http.routers.myapp.entrypoints=websecure"5 - "traefik.http.routers.myapp.tls=true"6 - "traefik.http.routers.myapp.middlewares=crowdsec-bouncer@file"7networks:8 - proxy9 10networks:11 proxy:12 external: true
Note: Make sure the app is also attached to the same proxy network as Traefik; routing will not work.
Save the file and restart the app. Use the command below to start the Traefik stack:
cd ~/crowdsecdocker compose up -d
Then restart your protected app stack if needed.
At this point, protect self-hosted apps with CrowdSec is active for any Traefik router that uses crowdsec-bouncer@file.
Part 3. Protect Self-Hosted Apps with CrowdSec Using Caddy
Use this section only if you want Caddy instead of Traefik.
Caddy requires a custom build that includes the CrowdSec bouncer module, because Caddy does not support runtime plugins as Traefik does.
Use the command below to create the Caddy YAML file:
nano ~/crowdsec/crowdsec-config/acquis.d/caddy.yaml
1filenames:2 - /var/log/caddy/access.log3poll_without_inotify: true4labels:5 type: caddy
Then, update the CrowdSec Compose file with this Caddy config:
Note: ghcr.io/buildplan/cs-caddy is a community-maintained image, not an official Caddy or CrowdSec build. If you prefer an official path, build your own image with xcaddy and the github.com/hslatman/caddy-crowdsec-bouncer module instead.
1services:2 crowdsec:3 image: crowdsecurity/crowdsec:latest4 container_name: crowdsec5 restart: unless-stopped6 environment:7 COLLECTIONS: "crowdsecurity/linux crowdsecurity/caddy crowdsecurity/http-cve crowdsecurity/http-dos"8 expose:9 - "8080"10 volumes:11 - ./crowdsec-config:/etc/crowdsec12 - ./crowdsec-db:/var/lib/crowdsec/data13 - ./logs/caddy:/var/log/caddy:ro14 - /var/log/auth.log:/var/log/auth.log:ro15 networks:16 - proxy17 18 caddy:19 image: ghcr.io/buildplan/cs-caddy:latest20 container_name: caddy21 restart: unless-stopped22 ports:23 - "80:80"24 - "443:443"25 volumes:26 - ./Caddyfile:/etc/caddy/Caddyfile27 - ./logs/caddy:/var/log/caddy28 - ./caddy_data:/data29 - ./caddy_config:/config30 networks:31 - proxy32 depends_on:33 - crowdsec34 35networks:36 proxy:37 external: true
Now you must create the bouncer key:
docker exec crowdsec cscli bouncers add caddy-bouncer
Copy the key.
Use the following command to create the Caddyfile:
nano ~/crowdsec/Caddyfile
Paste this and replace the key and domain:
{ log { output file /var/log/caddy/access.log format json } crowdsec { api_url http://crowdsec:8080 api_key PASTE_YOUR_KEY_HERE ticker_interval 15s }} app.example.com { log route { crowdsec reverse_proxy app:3000 }}
Once you are done, start the Caddy stack:
cd ~/crowdsecdocker compose up -d
Now protect self-hosted apps with CrowdSec is active for that Caddy site block.
Part 4. Add AppSec Firewall Layer
AppSec is CrowdSec's web application firewall layer. It can help block exploit payloads and bad requests. To create the file, you can use the command below:
nano ~/crowdsec/crowdsec-config/acquis.d/appsec.yaml
1listen_addr: 0.0.0.0:74222appsec_config: crowdsecurity/appsec-default3name: appsec4source: appsec5labels:6 type: appsec
Then, use the commands below to install AppSec collections:
docker exec crowdsec cscli collections install crowdsecurity/appsec-virtual-patchingdocker exec crowdsec cscli collections install crowdsecurity/appsec-generic-rules
Update Files for AppSec
In the CrowdSec service, you must add port 7422 to expose:. Edit the file:
nano ~/crowdsec/docker-compose.yml
It should look like this:
1expose:2 - "8080"3 - "7422"
cd ~/crowdsecdocker compose up -d --force-recreate crowdsec
If you use Traefik, edit:
nano ~/crowdsec/dynamic/crowdsec-middleware.yml
1http:2 middlewares:3 crowdsec-bouncer:4 plugin:5 crowdsec-bouncer:6 enabled: true7 crowdsecLapiKey: PASTE_YOUR_KEY_HERE8 crowdsecLapiHost: crowdsec:80809 crowdsecAppsecEnabled: true10 crowdsecAppsecHost: crowdsec:742211 crowdsecAppsecFailureBlock: true
cd ~/crowdsecdocker compose restart traefik
nano ~/crowdsec/Caddyfile
Add appsec_url in the global crowdsec block, then add appsec inside the route.
{ log { output file /var/log/caddy/access.log format json } crowdsec { api_url http://crowdsec:8080 api_key PASTE_YOUR_KEY_HERE ticker_interval 15s appsec_url http://crowdsec:7422 }} app.example.com { log route { crowdsec appsec reverse_proxy app:3000 }}
cd ~/crowdsecdocker compose restart caddy
This step makes protect self-hosted apps with CrowdSec stronger because requests are checked by IP reputation and by request content.
Part 5. Create a CrowdSec Whitelist
This is a safe default for single-server setups. It helps prevent internal or private IPs from being blocked.
Use the command below to create the whitelist file:
nano ~/crowdsec/crowdsec-config/parsers/s02-enrich/whitelists.yaml
1name: crowdsecurity/whitelists2description: "Whitelist local and private IPs"3whitelist:4 reason: "private ranges"5 ip:6 - "127.0.0.1"7 - "::1"8 cidr:9 - "10.0.0.0/8"10 - "172.16.0.0/12"11 - "192.168.0.0/16"
cd ~/crowdsecdocker compose restart crowdsec
This is one of the best small changes you can make when you protect self-hosted apps with CrowdSec on one VPS.
Part 6. Verify Everything Works Correctly with CrowdSec
It is recommended to check everything after your setup.
First, check that logs are being read:
docker exec crowdsec cscli metrics
Look for acquisition metrics. You should see lines for Traefik or Caddy logs.
Then, check if the bouncer is connected:
docker exec crowdsec cscli bouncers list
You should see traefik-bouncer or caddy-bouncer with a recent last pull time.
Now you can manually ban your IP. Find your public IP:
docker exec crowdsec cscli decisions add --ip YOUR.PUBLIC.IP --duration 5m
Open your app in the browser. You should get blocked.
Check active bans:
docker exec crowdsec cscli decisions list
Delete the test ban after that:
docker exec crowdsec cscli decisions delete --ip YOUR.PUBLIC.IP
When you protect self-hosted apps with CrowdSec, this quick test is the easiest way to prove the setup really works.
When CrowdSec Helps More Than a Firewall
A basic firewall is still important, but it cannot read web logs. CrowdSec helps more in cases like these:
- Login brute-force attacks against Vaultwarden, Nextcloud, WordPress, or Authentik.
- Bots scanning for .env, /wp-login.php, or old CVE paths.
- Repeat bad traffic coming from IPs already known to the CrowdSec community.
- Basic request floods that should be blocked before they waste app resources.
If you also use a login gateway, installing Authentik with Docker Compose is a good next step after you protect self-hosted apps with CrowdSec.
Conclusion
Traefik and Caddy are great at routing traffic and handling HTTPS, but they do not act like a smart security layer by themselves. CrowdSec adds that missing layer by reading logs, detecting attacks, and blocking bad traffic before it reaches your apps.
If you want a simple and practical way to protect self-hosted apps with CrowdSec, this setup is one of the best options for a single VPS. It is lightweight, works well with Docker, and is easy to test once the files are in the right place.
Harden your exposed apps on a PerLod Linux VPS before brute-force and bot traffic become real incidents.
We hope you enjoy this guide. For more information, you can check the CrowdSec official docs.